basuicn 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/registry.json CHANGED
@@ -153,7 +153,7 @@
153
153
  "files": [
154
154
  {
155
155
  "path": "src/components/ui/button/Button.tsx",
156
- "content": "'use client';\r\nimport * as React from 'react';\r\nimport { Button as BaseButton } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Spinner } from '../spinner/Spinner';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst buttonVariants = tv({\r\n base: 'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-30 disabled:hover:bg-transparent data-open:bg-muted cursor-pointer disabled:cursor-not-allowed',\r\n variants: {\r\n variant: {\r\n // Kraken Primary Purple\r\n solid: 'bg-primary text-primary-foreground hover:bg-primary/80 shadow-[rgba(0,0,0,0.08)_0px_1px_4px]',\r\n // Kraken Purple Outlined — border + text use primary colour\r\n outline: 'border border-primary/40 bg-transparent text-primary hover:bg-primary/5 hover:border-primary/70',\r\n // Kraken Secondary Gray — subtle bg, neutral text\r\n ghost: 'hover:bg-accent hover:text-accent-foreground',\r\n // Kraken Purple Subtle — secondary surface\r\n secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/70 shadow-[rgba(0,0,0,0.04)_0px_1px_4px]',\r\n danger: 'bg-danger text-danger-foreground hover:bg-danger/80 shadow-[rgba(0,0,0,0.08)_0px_1px_4px]',\r\n link: 'text-primary underline-offset-4 hover:underline h-auto px-0 py-0 font-normal',\r\n // Kính mờ tối — trên nền tối\r\n glass: 'bg-white/15 backdrop-blur-md border border-white/30 text-accent hover:bg-white/25 hover:border-white/50 shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_4px_20px_rgba(0,0,0,0.2)] transition-all',\r\n // ─── Glossy Bubble Variants ───────────────────────────────────────────────\r\n // Gradient from white highlight (top-left) → tinted color (bottom-right)\r\n // + inset top border = hiệu ứng gương bong bóng xà phòng\r\n 'glass-white': 'bg-gradient-to-br from-white/70 to-slate-100/60 backdrop-blur-md border border-black/5 text-slate-700 hover:from-white/85 hover:to-slate-100/70 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-amber': 'bg-gradient-to-br from-white/70 to-amber-300/40 backdrop-blur-sm border border-amber-100/80 text-amber-700 hover:from-white/85 hover:to-amber-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-green': 'bg-gradient-to-br from-white/70 to-emerald-300/40 backdrop-blur-sm border border-emerald-100/80 text-emerald-700 hover:from-white/85 hover:to-emerald-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-purple': 'bg-gradient-to-br from-white/70 to-violet-300/40 backdrop-blur-sm border border-violet-100/80 text-violet-700 hover:from-white/85 hover:to-violet-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-pink': 'bg-gradient-to-br from-white/70 to-pink-300/40 backdrop-blur-sm border border-pink-100/80 text-pink-700 hover:from-white/85 hover:to-pink-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n },\r\n size: {\r\n sm: 'h-8 px-3 text-xs',\r\n md: 'h-10 px-4 py-2',\r\n lg: 'h-11 px-8',\r\n icon: 'h-10 w-10',\r\n 'icon-sm': 'h-8 w-8',\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'solid',\r\n size: 'md',\r\n },\r\n});\r\n\r\n/** Props for the Button component */\r\nexport interface ButtonProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseButton>, 'className'>,\r\n VariantProps<typeof buttonVariants> {\r\n /** Icon rendered before the button label */\r\n leftIcon?: React.ReactNode;\r\n /** Icon rendered after the button label */\r\n rightIcon?: React.ReactNode;\r\n /** Shows a loading spinner and disables interaction */\r\n isLoading?: boolean;\r\n className?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst Button = React.forwardRef<React.ElementRef<typeof BaseButton>, ButtonProps>(\r\n ({ className, variant, size, leftIcon, rightIcon, isLoading, children, ...props }, ref) => {\r\n return (\r\n <BaseButton\r\n ref={ref}\r\n className={buttonVariants({ variant, size, className: className || '' })}\r\n disabled={isLoading || props.disabled}\r\n {...props}\r\n >\r\n {isLoading && <Spinner size=\"xs\" className={cn('mr-2')} />}\r\n <div className=\"flex items-center gap-2\">\r\n {!isLoading && leftIcon && <span>{leftIcon}</span>}\r\n {children}\r\n {!isLoading && rightIcon && <span>{rightIcon}</span>}\r\n </div>\r\n </BaseButton>\r\n );\r\n }\r\n);\r\nButton.displayName = 'Button';\r\n\r\nexport { Button };\r\n"
156
+ "content": "import * as React from 'react';\r\nimport { Button as BaseButton } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Spinner } from '../spinner/Spinner';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst buttonVariants = tv({\r\n base: 'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-30 disabled:hover:bg-transparent data-open:bg-muted cursor-pointer disabled:cursor-not-allowed data-loading:opacity-50 data-loading:cursor-not-allowed data-loading:pointer-events-none',\r\n variants: {\r\n variant: {\r\n // Kraken Primary Purple\r\n solid:\r\n \"bg-primary text-primary-foreground hover:bg-primary/80 shadow-[rgba(0,0,0,0.08)_0px_1px_4px]\",\r\n // Kraken Purple Outlined — border + text use primary colour\r\n outline:\r\n \"border border-primary/40 bg-transparent text-primary hover:bg-primary/5 hover:border-primary/70\",\r\n // Kraken Secondary Gray — subtle bg, neutral text\r\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\r\n // Kraken Purple Subtle — secondary surface\r\n secondary:\r\n \"bg-secondary text-secondary-foreground hover:bg-secondary/70 shadow-[rgba(0,0,0,0.04)_0px_1px_4px]\",\r\n danger:\r\n \"bg-danger text-danger-foreground hover:bg-danger/80 shadow-[rgba(0,0,0,0.08)_0px_1px_4px]\",\r\n link: \"text-primary underline-offset-4 hover:underline h-auto px-0 py-0 font-normal\",\r\n // Kính mờ tối — trên nền tối\r\n glass:\r\n \"bg-white/15 backdrop-blur-md border border-white/30 text-accent hover:bg-white/25 hover:border-white/50 shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_4px_20px_rgba(0,0,0,0.2)] transition-all\",\r\n // ─── Glossy Bubble Variants ───────────────────────────────────────────────\r\n // Gradient from white highlight (top-left) → tinted color (bottom-right)\r\n // + inset top border = hiệu ứng gương bong bóng xà phòng\r\n \"glass-white\":\r\n \"bg-gradient-to-br from-white/70 to-slate-100/60 backdrop-blur-md border border-black/5 text-slate-700 hover:from-white/85 hover:to-slate-100/70 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all\",\r\n \"glass-amber\":\r\n \"bg-gradient-to-br from-white/70 to-amber-300/40 backdrop-blur-sm border border-amber-100/80 text-amber-700 hover:from-white/85 hover:to-amber-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all\",\r\n \"glass-green\":\r\n \"bg-gradient-to-br from-white/70 to-emerald-300/40 backdrop-blur-sm border border-emerald-100/80 text-emerald-700 hover:from-white/85 hover:to-emerald-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all\",\r\n \"glass-purple\":\r\n \"bg-gradient-to-br from-white/70 to-violet-300/40 backdrop-blur-sm border border-violet-100/80 text-violet-700 hover:from-white/85 hover:to-violet-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all\",\r\n \"glass-pink\":\r\n \"bg-gradient-to-br from-white/70 to-pink-300/40 backdrop-blur-sm border border-pink-100/80 text-pink-700 hover:from-white/85 hover:to-pink-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all\",\r\n\r\n \"danger-outline\":\r\n \"border border-danger/40 text-danger hover:bg-danger/10\",\r\n \"success-outline\":\r\n \"border border-success/40 text-success hover:bg-success/10\",\r\n \"warning-outline\":\r\n \"border border-warning/40 text-warning hover:bg-warning/10\",\r\n \"info-outline\": \"border border-info/40 text-info hover:bg-info/10\",\r\n },\r\n size: {\r\n xs: \"h-7 px-2.5 py-1.5 text-xs\",\r\n sm: \"h-8 px-3 text-sm\",\r\n md: \"h-10 px-4 py-2\",\r\n lg: \"h-11 px-8\",\r\n icon: \"h-10 w-10\",\r\n \"icon-sm\": \"h-8 w-8\",\r\n },\r\n },\r\n defaultVariants: {\r\n variant: \"solid\",\r\n size: \"md\",\r\n },\r\n});\r\n\r\n/** Props for the Button component */\r\nexport interface ButtonProps\r\n extends\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseButton>, \"className\">,\r\n VariantProps<typeof buttonVariants> {\r\n /** Icon rendered before the button label */\r\n leftIcon?: React.ReactNode;\r\n /** Icon rendered after the button label */\r\n rightIcon?: React.ReactNode;\r\n /** Shows a loading spinner and disables interaction */\r\n isLoading?: boolean;\r\n className?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst Button = React.forwardRef<React.ElementRef<typeof BaseButton>, ButtonProps>(\r\n ({ className, variant, size, leftIcon, rightIcon, isLoading, children, ...props }, ref) => {\r\n return (\r\n <BaseButton\r\n ref={ref}\r\n className={buttonVariants({ variant, size, className: className || '' })}\r\n disabled={isLoading || props.disabled}\r\n data-loading={isLoading || undefined}\r\n {...props}\r\n >\r\n {isLoading && <Spinner size=\"xs\" className={cn('mr-2 text-muted')} />}\r\n <div className=\"flex items-center gap-2\">\r\n {!isLoading && leftIcon && <span>{leftIcon}</span>}\r\n {children}\r\n {!isLoading && rightIcon && <span>{rightIcon}</span>}\r\n </div>\r\n </BaseButton>\r\n );\r\n }\r\n);\r\nButton.displayName = 'Button';\r\n\r\nexport { Button };\r\n"
157
157
  }
158
158
  ]
159
159
  },
@@ -167,7 +167,7 @@
167
167
  "files": [
168
168
  {
169
169
  "path": "src/components/ui/calendar/Calendar.tsx",
170
- "content": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport { DayPicker, type DateRange, type Matcher } from 'react-day-picker';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport * as locales from 'react-day-picker/locale';\r\n\r\nimport 'react-day-picker/dist/style.css';\r\n\r\nconst calendarVariants = tv({\r\n base: 'rdp-custom',\r\n variants: {\r\n size: {\r\n sm: '[&_.rdp-day]:h-7 [&_.rdp-day]:w-7 [&_.rdp-day]:text-xs',\r\n md: '[&_.rdp-day]:h-9 [&_.rdp-day]:w-9 [&_.rdp-day]:text-sm',\r\n lg: '[&_.rdp-day]:h-11 [&_.rdp-day]:w-11 [&_.rdp-day]:text-base',\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\nconst wrapperVariants = tv({\r\n base: 'inline-block rounded-xl border border-border bg-background p-3 shadow-sm',\r\n});\r\n\r\nexport type CalendarMode = 'single' | 'range' | 'multiple';\r\n\r\n/** Props for the Calendar component */\r\nexport interface CalendarProps extends VariantProps<typeof calendarVariants> {\r\n /** Selection mode: single date, date range, or multiple dates */\r\n mode?: CalendarMode;\r\n /** Currently selected value (Date, DateRange, or Date[] depending on mode) */\r\n selected?: Date | DateRange | Date[];\r\n /** Callback fired when the selection changes */\r\n onSelect?: (value: Date | DateRange | Date[] | undefined) => void;\r\n /** Disable all dates before today */\r\n disablePastDates?: boolean;\r\n /** Disable all dates after today */\r\n disableFutureDates?: boolean;\r\n /** Disable the entire calendar */\r\n disabled?: boolean;\r\n /** Locale key from react-day-picker/locale (defaults to 'enUS') */\r\n locale?: keyof typeof locales;\r\n className?: string;\r\n /** Additional class name for the outer wrapper */\r\n wrapperClassName?: string;\r\n /** Number of months to display side by side */\r\n numberOfMonths?: number;\r\n /** Show days from adjacent months */\r\n showOutsideDays?: boolean;\r\n}\r\n\r\nconst Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(({\r\n mode = 'single',\r\n selected,\r\n onSelect,\r\n disablePastDates = false,\r\n disableFutureDates = false,\r\n disabled = false,\r\n locale = 'enUS',\r\n className,\r\n wrapperClassName,\r\n size,\r\n numberOfMonths = 1,\r\n showOutsideDays = true,\r\n}, ref) => {\r\n const getDisabled = (): Matcher | Matcher[] | undefined => {\r\n if (disabled) return true;\r\n if (disablePastDates && disableFutureDates) return () => true;\r\n if (disablePastDates) return { before: new Date() };\r\n if (disableFutureDates) return { after: new Date() };\r\n return undefined;\r\n };\r\n\r\n const commonProps = {\r\n locale: locales[locale as keyof typeof locales],\r\n disabled: getDisabled(),\r\n numberOfMonths,\r\n showOutsideDays,\r\n className: calendarVariants({ size, className }),\r\n };\r\n\r\n return (\r\n <div ref={ref} className={wrapperVariants({ className: wrapperClassName })}>\r\n {mode === 'range' ? (\r\n <DayPicker\r\n {...commonProps}\r\n mode=\"range\"\r\n selected={selected as DateRange | undefined}\r\n onSelect={(d) => onSelect?.(d)}\r\n />\r\n ) : mode === 'multiple' ? (\r\n <DayPicker\r\n {...commonProps}\r\n mode=\"multiple\"\r\n selected={selected as Date[] | undefined}\r\n onSelect={(d) => onSelect?.(d)}\r\n />\r\n ) : (\r\n <DayPicker\r\n {...commonProps}\r\n mode=\"single\"\r\n selected={selected as Date | undefined}\r\n onSelect={(d) => onSelect?.(d)}\r\n />\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nCalendar.displayName = 'Calendar';\r\n\r\nexport { Calendar };\r\n"
170
+ "content": "'use client';\n\nimport * as React from 'react';\nimport { DayPicker, type DateRange, type Matcher } from 'react-day-picker';\nimport { tv, type VariantProps } from 'tailwind-variants';\nimport * as locales from 'react-day-picker/locale';\n\nimport 'react-day-picker/dist/style.css';\n\nconst calendarVariants = tv({\n base: 'rdp-custom',\n variants: {\n size: {\n sm: '[&_.rdp-day]:h-7 [&_.rdp-day]:w-7 [&_.rdp-day]:text-xs',\n md: '[&_.rdp-day]:h-9 [&_.rdp-day]:w-9 [&_.rdp-day]:text-sm',\n lg: '[&_.rdp-day]:h-11 [&_.rdp-day]:w-11 [&_.rdp-day]:text-base',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n});\n\nconst wrapperVariants = tv({\n base: 'inline-block rounded-xl border border-border bg-background p-3 shadow-sm',\n});\n\nexport type CalendarMode = 'single' | 'range' | 'multiple';\n\n/** Props for the Calendar component */\nexport interface CalendarProps extends VariantProps<typeof calendarVariants> {\n /** Selection mode: single date, date range, or multiple dates */\n mode?: CalendarMode;\n /** Currently selected value (Date, DateRange, or Date[] depending on mode) */\n selected?: Date | DateRange | Date[];\n /** Callback fired when the selection changes */\n onSelect?: (value: Date | DateRange | Date[] | undefined) => void;\n /** Disable all dates before today */\n disablePastDates?: boolean;\n /** Disable all dates after today */\n disableFutureDates?: boolean;\n /** Disable the entire calendar */\n disabled?: boolean;\n /** Locale key from react-day-picker/locale (defaults to 'enUS') */\n locale?: keyof typeof locales;\n className?: string;\n /** Additional class name for the outer wrapper */\n wrapperClassName?: string;\n /** Number of months to display side by side */\n numberOfMonths?: number;\n /** Show days from adjacent months */\n showOutsideDays?: boolean;\n}\n\nconst Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(({\n mode = 'single',\n selected,\n onSelect,\n disablePastDates = false,\n disableFutureDates = false,\n disabled = false,\n locale = 'enUS',\n className,\n wrapperClassName,\n size,\n numberOfMonths = 1,\n showOutsideDays = true,\n}, ref) => {\n const getDisabled = (): Matcher | Matcher[] | undefined => {\n if (disabled) return true;\n if (disablePastDates && disableFutureDates) return () => true;\n if (disablePastDates) return { before: new Date() };\n if (disableFutureDates) return { after: new Date() };\n return undefined;\n };\n\n const commonProps = {\n locale: locales[locale as keyof typeof locales],\n disabled: getDisabled(),\n numberOfMonths,\n showOutsideDays,\n className: calendarVariants({ size, className }),\n };\n\n return (\n <div ref={ref} className={wrapperVariants({ className: wrapperClassName })}>\n {mode === 'range' ? (\n <DayPicker\n {...commonProps}\n mode=\"range\"\n selected={selected as DateRange | undefined}\n onSelect={(d) => onSelect?.(d)}\n />\n ) : mode === 'multiple' ? (\n <DayPicker\n {...commonProps}\n mode=\"multiple\"\n selected={selected as Date[] | undefined}\n onSelect={(d) => onSelect?.(d)}\n />\n ) : (\n <DayPicker\n {...commonProps}\n mode=\"single\"\n selected={selected as Date | undefined}\n onSelect={(d) => onSelect?.(d)}\n />\n )}\n </div>\n );\n});\n\nCalendar.displayName = 'Calendar';\n\nexport { Calendar };\n"
171
171
  }
172
172
  ]
173
173
  },
@@ -195,7 +195,7 @@
195
195
  "files": [
196
196
  {
197
197
  "path": "src/components/ui/carousel/Carousel.tsx",
198
- "content": "import * as React from 'react';\r\nimport { useKeenSlider, type KeenSliderOptions, type KeenSliderPlugin, type KeenSliderInstance } from 'keen-slider/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\r\nimport 'keen-slider/keen-slider.min.css';\r\n\r\n// ─── Variants ─────────────────────────────────────────────────────────────────\r\n\r\nconst carouselVariants = tv({\r\n slots: {\r\n root: 'relative w-full select-none',\r\n viewport: 'keen-slider overflow-hidden rounded-xl',\r\n slide: 'keen-slider__slide',\r\n arrow: [\r\n 'absolute top-1/2 -translate-y-1/2 z-10',\r\n 'flex items-center justify-center',\r\n 'h-9 w-9 rounded-full',\r\n 'bg-background/80 backdrop-blur-sm border border-border shadow-md',\r\n 'text-foreground transition-all duration-150',\r\n 'hover:bg-background hover:scale-105',\r\n 'disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:scale-100',\r\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',\r\n ],\r\n dotsWrapper: 'flex justify-center gap-1.5 mt-3',\r\n dot: [\r\n 'h-1.5 rounded-full bg-border transition-all duration-300',\r\n 'hover:bg-muted-foreground cursor-pointer',\r\n ],\r\n },\r\n});\r\n\r\nconst { root, viewport, slide, arrow, dotsWrapper, dot } = carouselVariants();\r\n\r\n// ─── Context ──────────────────────────────────────────────────────────────────\r\n\r\ninterface CarouselContextValue {\r\n instanceRef: React.MutableRefObject<KeenSliderInstance | null>;\r\n currentSlide: number;\r\n slideCount: number;\r\n loop: boolean;\r\n}\r\n\r\nconst CarouselContext = React.createContext<CarouselContextValue | null>(null);\r\n\r\nfunction useCarousel() {\r\n const ctx = React.useContext(CarouselContext);\r\n if (!ctx) throw new Error('useCarousel must be used within <Carousel>');\r\n return ctx;\r\n}\r\n\r\n// ─── AutoPlay plugin ──────────────────────────────────────────────────────────\r\n\r\nexport function AutoPlayPlugin(interval = 3000): KeenSliderPlugin {\r\n return (slider) => {\r\n let timeout: ReturnType<typeof setTimeout>;\r\n let mouseOver = false;\r\n\r\n const clearNext = () => clearTimeout(timeout);\r\n const next = () => {\r\n clearNext();\r\n timeout = setTimeout(() => {\r\n if (!mouseOver) slider.next();\r\n }, interval);\r\n };\r\n\r\n slider.on('created', () => {\r\n slider.container.addEventListener('mouseover', () => { mouseOver = true; clearNext(); });\r\n slider.container.addEventListener('mouseout', () => { mouseOver = false; next(); });\r\n next();\r\n });\r\n slider.on('dragStarted', clearNext);\r\n slider.on('animationEnded', next);\r\n slider.on('updated', next);\r\n slider.on('destroyed', clearNext);\r\n };\r\n}\r\n\r\n// ─── WheelControls plugin ────────────────────────────────────────────────────\r\n\r\nexport const WheelControlsPlugin: KeenSliderPlugin = (slider) => {\r\n let touchTimeout: ReturnType<typeof setTimeout>;\r\n let position = { x: 0, y: 0 };\r\n let wheelActive = false;\r\n\r\n const dispatch = (e: WheelEvent, name: string) => {\r\n position.x -= e.deltaX;\r\n position.y -= e.deltaY;\r\n slider.container.dispatchEvent(new CustomEvent(name, { detail: { x: position.x, y: position.y } }));\r\n };\r\n\r\n const wheelStart = (e: WheelEvent) => {\r\n position = { x: e.pageX, y: e.pageY };\r\n dispatch(e, 'ksDragStart');\r\n };\r\n\r\n const wheel = (e: WheelEvent) => {\r\n dispatch(e, 'ksDrag');\r\n };\r\n\r\n const wheelEnd = (e: WheelEvent) => {\r\n dispatch(e, 'ksDragEnd');\r\n };\r\n\r\n const eventWheel = (e: WheelEvent) => {\r\n if (!wheelActive) {\r\n wheelStart(e);\r\n wheelActive = true;\r\n }\r\n wheel(e);\r\n clearTimeout(touchTimeout);\r\n touchTimeout = setTimeout(() => {\r\n wheelActive = false;\r\n wheelEnd(e);\r\n }, 50);\r\n };\r\n\r\n slider.on('created', () => {\r\n slider.container.addEventListener('wheel', eventWheel, { passive: true });\r\n });\r\n slider.on('destroyed', () => {\r\n slider.container.removeEventListener('wheel', eventWheel);\r\n });\r\n};\r\n\r\n// ─── MutationPlugin ───────────────────────────────────────────────────────────\r\n\r\nexport const MutationPlugin: KeenSliderPlugin = (slider) => {\r\n const observer = new MutationObserver((mutations) => {\r\n mutations.forEach(() => slider.update());\r\n });\r\n slider.on('created', () => {\r\n observer.observe(slider.container, { childList: true });\r\n });\r\n slider.on('destroyed', () => observer.disconnect());\r\n};\r\n\r\n// ─── Carousel3DPlugin ─────────────────────────────────────────────────────────\r\n\r\n/**\r\n * 3-D rotating carousel plugin.\r\n * Wrap the `keen-slider` div inside a perspective scene:\r\n * <div style={{ perspective: '1000px' }}>\r\n * <div ref={sliderRef} style={{ transformStyle: 'preserve-3d' }} …>\r\n * {slides}\r\n * </div>\r\n * </div>\r\n *\r\n * @param depth translateZ radius in px (default 280). For N slides of width W:\r\n * depth ≈ (W / 2) / Math.tan(Math.PI / N)\r\n */\r\nexport function Carousel3DPlugin(depth = 280): KeenSliderPlugin {\r\n return (slider) => {\r\n function applyRotation() {\r\n const deg = 360 * slider.track.details.progress;\r\n slider.container.style.transform = `translateZ(-${depth}px) rotateY(${-deg}deg)`;\r\n }\r\n\r\n slider.on('created', () => {\r\n const perSlide = 360 / slider.slides.length;\r\n slider.slides.forEach((el, i) => {\r\n el.style.transform = `rotateY(${perSlide * i}deg) translateZ(${depth}px)`;\r\n });\r\n applyRotation();\r\n });\r\n\r\n slider.on('detailsChanged', applyRotation);\r\n };\r\n}\r\n\r\n// ─── ResizePlugin ─────────────────────────────────────────────────────────────\r\n\r\nexport const ResizePlugin: KeenSliderPlugin = (slider) => {\r\n const observer = new ResizeObserver(() => slider.update());\r\n slider.on('created', () => observer.observe(slider.container));\r\n slider.on('destroyed', () => observer.disconnect());\r\n};\r\n\r\n// ─── Root ─────────────────────────────────────────────────────────────────────\r\n\r\nexport interface CarouselProps {\r\n children: React.ReactNode;\r\n /** Loop back to start after last slide */\r\n loop?: boolean;\r\n /** Auto-advance interval in ms; `false` to disable */\r\n autoPlay?: number | false;\r\n /** Initial slide index */\r\n initial?: number;\r\n /** Slides to show per view */\r\n slidesPerView?: number;\r\n /** Gap between slides in px */\r\n spacing?: number;\r\n /** Drag / swipe enabled */\r\n drag?: boolean;\r\n /** Vertical orientation — must also provide `height` */\r\n vertical?: boolean;\r\n /** Explicit height for the viewport (required for vertical mode) e.g. \"320px\" */\r\n height?: string;\r\n /** Scroll mode */\r\n mode?: 'snap' | 'free' | 'free-snap';\r\n /** Enable mouse-wheel navigation */\r\n wheelControls?: boolean;\r\n /** Watch DOM mutations and auto-update */\r\n mutationObserver?: boolean;\r\n /** Breakpoint overrides — keyed by media query string */\r\n breakpoints?: KeenSliderOptions['breakpoints'];\r\n /** Called when the active slide changes */\r\n onSlideChange?: (index: number) => void;\r\n /** Called on every position change — provides progress 0-1 */\r\n onDetailsChanged?: (progress: number, rel: number) => void;\r\n /** Called once the slider is ready */\r\n onCreated?: (instance: CarouselContextValue['instanceRef']['current']) => void;\r\n /** Extra className applied to the inner viewport div */\r\n viewportClassName?: string;\r\n className?: string;\r\n}\r\n\r\nconst Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(\r\n (\r\n {\r\n children,\r\n loop = false,\r\n autoPlay = false,\r\n initial = 0,\r\n slidesPerView = 1,\r\n spacing = 16,\r\n drag = true,\r\n vertical = false,\r\n height,\r\n mode = 'snap',\r\n wheelControls = false,\r\n mutationObserver = false,\r\n breakpoints,\r\n onSlideChange,\r\n onDetailsChanged,\r\n onCreated,\r\n viewportClassName,\r\n className,\r\n },\r\n ref\r\n ) => {\r\n const [currentSlide, setCurrentSlide] = React.useState(initial);\r\n const [slideCount, setSlideCount] = React.useState(0);\r\n\r\n const plugins: KeenSliderPlugin[] = [];\r\n if (autoPlay !== false) plugins.push(AutoPlayPlugin(autoPlay));\r\n if (wheelControls) plugins.push(WheelControlsPlugin);\r\n if (mutationObserver) plugins.push(MutationPlugin);\r\n plugins.push(ResizePlugin);\r\n\r\n const [sliderRef, instanceRef] = useKeenSlider<HTMLDivElement>(\r\n {\r\n loop,\r\n initial,\r\n drag,\r\n vertical,\r\n mode,\r\n breakpoints,\r\n slides: { perView: slidesPerView, spacing },\r\n slideChanged(s) {\r\n setCurrentSlide(s.track.details.rel);\r\n onSlideChange?.(s.track.details.rel);\r\n },\r\n detailsChanged(s) {\r\n onDetailsChanged?.(s.track.details.progress, s.track.details.rel);\r\n },\r\n created(s) {\r\n setSlideCount(s.track.details.slides.length);\r\n onCreated?.(s);\r\n },\r\n updated(s) {\r\n setSlideCount(s.track.details.slides.length);\r\n },\r\n },\r\n plugins\r\n );\r\n\r\n // Separate CarouselSlide children from navigation children\r\n const slides: React.ReactNode[] = [];\r\n const navigation: React.ReactNode[] = [];\r\n React.Children.forEach(children, (child) => {\r\n if (React.isValidElement(child) && (child.type as { displayName?: string }).displayName === 'CarouselSlide') {\r\n slides.push(child);\r\n } else {\r\n navigation.push(child);\r\n }\r\n });\r\n\r\n return (\r\n <CarouselContext.Provider value={{ instanceRef, currentSlide, slideCount, loop }}>\r\n <div ref={ref} className={root({ className })} data-testid=\"carousel\">\r\n <div\r\n ref={sliderRef}\r\n className={cn(viewport(), viewportClassName)}\r\n style={height ? { height } : undefined}\r\n >\r\n {slides}\r\n </div>\r\n {navigation}\r\n </div>\r\n </CarouselContext.Provider>\r\n );\r\n }\r\n);\r\nCarousel.displayName = 'Carousel';\r\n\r\n// ─── CarouselSlide ────────────────────────────────────────────────────────────\r\n\r\nexport interface CarouselSlideProps extends React.HTMLAttributes<HTMLDivElement> {\r\n className?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst CarouselSlide = React.forwardRef<HTMLDivElement, CarouselSlideProps>(\r\n ({ className, children, ...props }, ref) => (\r\n <div ref={ref} className={slide({ className })} {...props}>\r\n {children}\r\n </div>\r\n )\r\n);\r\nCarouselSlide.displayName = 'CarouselSlide';\r\n\r\n// ─── CarouselPrev / CarouselNext ──────────────────────────────────────────────\r\n\r\nexport interface CarouselArrowProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n className?: string;\r\n}\r\n\r\nconst CarouselPrev = React.forwardRef<HTMLButtonElement, CarouselArrowProps>(\r\n ({ className, ...props }, ref) => {\r\n const { instanceRef, currentSlide, loop } = useCarousel();\r\n const disabled = !loop && currentSlide === 0;\r\n\r\n return (\r\n <button\r\n ref={ref}\r\n aria-label=\"Previous slide\"\r\n disabled={disabled}\r\n onClick={() => instanceRef.current?.prev()}\r\n className={arrow({ className: cn('left-2', className) })}\r\n {...props}\r\n >\r\n <ChevronLeft className=\"h-4 w-4\" />\r\n </button>\r\n );\r\n }\r\n);\r\nCarouselPrev.displayName = 'CarouselPrev';\r\n\r\nconst CarouselNext = React.forwardRef<HTMLButtonElement, CarouselArrowProps>(\r\n ({ className, ...props }, ref) => {\r\n const { instanceRef, currentSlide, slideCount, loop } = useCarousel();\r\n const disabled = !loop && currentSlide === slideCount - 1;\r\n\r\n return (\r\n <button\r\n ref={ref}\r\n aria-label=\"Next slide\"\r\n disabled={disabled}\r\n onClick={() => instanceRef.current?.next()}\r\n className={arrow({ className: cn('right-2', className) })}\r\n {...props}\r\n >\r\n <ChevronRight className=\"h-4 w-4\" />\r\n </button>\r\n );\r\n }\r\n);\r\nCarouselNext.displayName = 'CarouselNext';\r\n\r\n// ─── CarouselDots ─────────────────────────────────────────────────────────────\r\n\r\nexport interface CarouselDotsProps extends React.HTMLAttributes<HTMLDivElement> {\r\n className?: string;\r\n}\r\n\r\nconst CarouselDots = React.forwardRef<HTMLDivElement, CarouselDotsProps>(\r\n ({ className, ...props }, ref) => {\r\n const { instanceRef, currentSlide, slideCount } = useCarousel();\r\n\r\n if (slideCount === 0) return null;\r\n\r\n return (\r\n <div ref={ref} className={dotsWrapper({ className })} role=\"tablist\" aria-label=\"Carousel navigation\" {...props}>\r\n {Array.from({ length: slideCount }).map((_, i) => (\r\n <button\r\n key={i}\r\n role=\"tab\"\r\n aria-selected={i === currentSlide}\r\n aria-label={`Go to slide ${i + 1}`}\r\n onClick={() => instanceRef.current?.moveToIdx(i)}\r\n className={dot({\r\n className: cn(\r\n i === currentSlide\r\n ? 'w-6 bg-primary'\r\n : 'w-1.5 bg-border hover:bg-muted-foreground'\r\n ),\r\n })}\r\n />\r\n ))}\r\n </div>\r\n );\r\n }\r\n);\r\nCarouselDots.displayName = 'CarouselDots';\r\n\r\n// ─── CarouselProgress ─────────────────────────────────────────────────────────\r\n\r\nexport interface CarouselProgressProps extends React.HTMLAttributes<HTMLDivElement> {\r\n className?: string;\r\n}\r\n\r\nconst CarouselProgress = React.forwardRef<HTMLDivElement, CarouselProgressProps>(\r\n ({ className, ...props }, ref) => {\r\n const { currentSlide, slideCount } = useCarousel();\r\n\r\n const pct = slideCount > 1\r\n ? Math.round((currentSlide / (slideCount - 1)) * 100)\r\n : 100;\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"progressbar\"\r\n aria-valuenow={pct}\r\n aria-valuemin={0}\r\n aria-valuemax={100}\r\n aria-label=\"Carousel progress\"\r\n className={cn('w-full h-1 bg-border rounded-full overflow-hidden mt-3', className)}\r\n {...props}\r\n >\r\n <div\r\n className=\"h-full bg-primary rounded-full transition-all duration-300\"\r\n style={{ width: `${pct}%` }}\r\n />\r\n </div>\r\n );\r\n }\r\n);\r\nCarouselProgress.displayName = 'CarouselProgress';\r\n\r\n// ─── CarouselCounter ──────────────────────────────────────────────────────────\r\n\r\nexport interface CarouselCounterProps extends React.HTMLAttributes<HTMLSpanElement> {\r\n className?: string;\r\n}\r\n\r\nconst CarouselCounter = React.forwardRef<HTMLSpanElement, CarouselCounterProps>(\r\n ({ className, ...props }, ref) => {\r\n const { currentSlide, slideCount } = useCarousel();\r\n\r\n return (\r\n <span\r\n ref={ref}\r\n aria-live=\"polite\"\r\n aria-label=\"Slide counter\"\r\n className={cn('text-xs text-muted-foreground tabular-nums', className)}\r\n {...props}\r\n >\r\n {currentSlide + 1} / {slideCount}\r\n </span>\r\n );\r\n }\r\n);\r\nCarouselCounter.displayName = 'CarouselCounter';\r\n\r\n// ─── Exports ──────────────────────────────────────────────────────────────────\r\n\r\nexport {\r\n Carousel,\r\n CarouselSlide,\r\n CarouselPrev,\r\n CarouselNext,\r\n CarouselDots,\r\n CarouselProgress,\r\n CarouselCounter,\r\n useCarousel,\r\n};\r\n"
198
+ "content": "import * as React from 'react';\nimport { useKeenSlider, type KeenSliderOptions, type KeenSliderPlugin, type KeenSliderInstance } from 'keen-slider/react';\nimport { tv } from 'tailwind-variants';\nimport { cn } from '@/lib/utils/cn';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport 'keen-slider/keen-slider.min.css';\n\n// ─── Variants ─────────────────────────────────────────────────────────────────\n\nconst carouselVariants = tv({\n slots: {\n root: 'relative w-full select-none',\n viewport: 'keen-slider overflow-hidden rounded-xl',\n slide: 'keen-slider__slide',\n arrow: [\n 'absolute top-1/2 -translate-y-1/2 z-10',\n 'flex items-center justify-center',\n 'h-9 w-9 rounded-full',\n 'bg-background/80 backdrop-blur-sm border border-border shadow-md',\n 'text-foreground transition-all duration-150',\n 'hover:bg-background hover:scale-105',\n 'disabled:opacity-30 disabled:cursor-not-allowed disabled:hover:scale-100',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',\n ],\n dotsWrapper: 'flex justify-center gap-1.5 mt-3',\n dot: [\n 'h-1.5 rounded-full bg-border transition-all duration-300',\n 'hover:bg-muted-foreground cursor-pointer',\n ],\n },\n});\n\nconst { root, viewport, slide, arrow, dotsWrapper, dot } = carouselVariants();\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\ninterface CarouselContextValue {\n instanceRef: React.MutableRefObject<KeenSliderInstance | null>;\n currentSlide: number;\n slideCount: number;\n loop: boolean;\n}\n\nconst CarouselContext = React.createContext<CarouselContextValue | null>(null);\n\nfunction useCarousel() {\n const ctx = React.useContext(CarouselContext);\n if (!ctx) throw new Error('useCarousel must be used within <Carousel>');\n return ctx;\n}\n\n// ─── AutoPlay plugin ──────────────────────────────────────────────────────────\n\nexport function AutoPlayPlugin(interval = 3000): KeenSliderPlugin {\n return (slider) => {\n let timeout: ReturnType<typeof setTimeout>;\n let mouseOver = false;\n\n const clearNext = () => clearTimeout(timeout);\n const next = () => {\n clearNext();\n timeout = setTimeout(() => {\n if (!mouseOver) slider.next();\n }, interval);\n };\n\n slider.on('created', () => {\n slider.container.addEventListener('mouseover', () => { mouseOver = true; clearNext(); });\n slider.container.addEventListener('mouseout', () => { mouseOver = false; next(); });\n next();\n });\n slider.on('dragStarted', clearNext);\n slider.on('animationEnded', next);\n slider.on('updated', next);\n slider.on('destroyed', clearNext);\n };\n}\n\n// ─── WheelControls plugin ────────────────────────────────────────────────────\n\nexport const WheelControlsPlugin: KeenSliderPlugin = (slider) => {\n let touchTimeout: ReturnType<typeof setTimeout>;\n let position = { x: 0, y: 0 };\n let wheelActive = false;\n\n const dispatch = (e: WheelEvent, name: string) => {\n position.x -= e.deltaX;\n position.y -= e.deltaY;\n slider.container.dispatchEvent(new CustomEvent(name, { detail: { x: position.x, y: position.y } }));\n };\n\n const wheelStart = (e: WheelEvent) => {\n position = { x: e.pageX, y: e.pageY };\n dispatch(e, 'ksDragStart');\n };\n\n const wheel = (e: WheelEvent) => {\n dispatch(e, 'ksDrag');\n };\n\n const wheelEnd = (e: WheelEvent) => {\n dispatch(e, 'ksDragEnd');\n };\n\n const eventWheel = (e: WheelEvent) => {\n if (!wheelActive) {\n wheelStart(e);\n wheelActive = true;\n }\n wheel(e);\n clearTimeout(touchTimeout);\n touchTimeout = setTimeout(() => {\n wheelActive = false;\n wheelEnd(e);\n }, 50);\n };\n\n slider.on('created', () => {\n slider.container.addEventListener('wheel', eventWheel, { passive: true });\n });\n slider.on('destroyed', () => {\n slider.container.removeEventListener('wheel', eventWheel);\n });\n};\n\n// ─── MutationPlugin ───────────────────────────────────────────────────────────\n\nexport const MutationPlugin: KeenSliderPlugin = (slider) => {\n const observer = new MutationObserver((mutations) => {\n mutations.forEach(() => slider.update());\n });\n slider.on('created', () => {\n observer.observe(slider.container, { childList: true });\n });\n slider.on('destroyed', () => observer.disconnect());\n};\n\n// ─── Carousel3DPlugin ─────────────────────────────────────────────────────────\n\n/**\n * 3-D rotating carousel plugin.\n * Wrap the `keen-slider` div inside a perspective scene:\n * <div style={{ perspective: '1000px' }}>\n * <div ref={sliderRef} style={{ transformStyle: 'preserve-3d' }} …>\n * {slides}\n * </div>\n * </div>\n *\n * @param depth translateZ radius in px (default 280). For N slides of width W:\n * depth ≈ (W / 2) / Math.tan(Math.PI / N)\n */\nexport function Carousel3DPlugin(depth = 280): KeenSliderPlugin {\n return (slider) => {\n function applyRotation() {\n const deg = 360 * slider.track.details.progress;\n slider.container.style.transform = `translateZ(-${depth}px) rotateY(${-deg}deg)`;\n }\n\n slider.on('created', () => {\n const perSlide = 360 / slider.slides.length;\n slider.slides.forEach((el, i) => {\n el.style.transform = `rotateY(${perSlide * i}deg) translateZ(${depth}px)`;\n });\n applyRotation();\n });\n\n slider.on('detailsChanged', applyRotation);\n };\n}\n\n// ─── ResizePlugin ─────────────────────────────────────────────────────────────\n\nexport const ResizePlugin: KeenSliderPlugin = (slider) => {\n const observer = new ResizeObserver(() => slider.update());\n slider.on('created', () => observer.observe(slider.container));\n slider.on('destroyed', () => observer.disconnect());\n};\n\n// ─── Root ─────────────────────────────────────────────────────────────────────\n\nexport interface CarouselProps {\n children: React.ReactNode;\n /** Loop back to start after last slide */\n loop?: boolean;\n /** Auto-advance interval in ms; `false` to disable */\n autoPlay?: number | false;\n /** Initial slide index */\n initial?: number;\n /** Slides to show per view */\n slidesPerView?: number;\n /** Gap between slides in px */\n spacing?: number;\n /** Drag / swipe enabled */\n drag?: boolean;\n /** Vertical orientation — must also provide `height` */\n vertical?: boolean;\n /** Explicit height for the viewport (required for vertical mode) e.g. \"320px\" */\n height?: string;\n /** Scroll mode */\n mode?: 'snap' | 'free' | 'free-snap';\n /** Enable mouse-wheel navigation */\n wheelControls?: boolean;\n /** Watch DOM mutations and auto-update */\n mutationObserver?: boolean;\n /** Breakpoint overrides — keyed by media query string */\n breakpoints?: KeenSliderOptions['breakpoints'];\n /** Called when the active slide changes */\n onSlideChange?: (index: number) => void;\n /** Called on every position change — provides progress 0-1 */\n onDetailsChanged?: (progress: number, rel: number) => void;\n /** Called once the slider is ready */\n onCreated?: (instance: CarouselContextValue['instanceRef']['current']) => void;\n /** Extra className applied to the inner viewport div */\n viewportClassName?: string;\n className?: string;\n}\n\nconst Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(\n (\n {\n children,\n loop = false,\n autoPlay = false,\n initial = 0,\n slidesPerView = 1,\n spacing = 16,\n drag = true,\n vertical = false,\n height,\n mode = 'snap',\n wheelControls = false,\n mutationObserver = false,\n breakpoints,\n onSlideChange,\n onDetailsChanged,\n onCreated,\n viewportClassName,\n className,\n },\n ref\n ) => {\n const [currentSlide, setCurrentSlide] = React.useState(initial);\n const [slideCount, setSlideCount] = React.useState(0);\n\n const plugins: KeenSliderPlugin[] = [];\n if (autoPlay !== false) plugins.push(AutoPlayPlugin(autoPlay));\n if (wheelControls) plugins.push(WheelControlsPlugin);\n if (mutationObserver) plugins.push(MutationPlugin);\n plugins.push(ResizePlugin);\n\n const [sliderRef, instanceRef] = useKeenSlider<HTMLDivElement>(\n {\n loop,\n initial,\n drag,\n vertical,\n mode,\n breakpoints,\n slides: { perView: slidesPerView, spacing },\n slideChanged(s) {\n setCurrentSlide(s.track.details.rel);\n onSlideChange?.(s.track.details.rel);\n },\n detailsChanged(s) {\n onDetailsChanged?.(s.track.details.progress, s.track.details.rel);\n },\n created(s) {\n setSlideCount(s.track.details.slides.length);\n onCreated?.(s);\n },\n updated(s) {\n setSlideCount(s.track.details.slides.length);\n },\n },\n plugins\n );\n\n // Separate CarouselSlide children from navigation children\n const slides: React.ReactNode[] = [];\n const navigation: React.ReactNode[] = [];\n React.Children.forEach(children, (child) => {\n if (React.isValidElement(child) && (child.type as { displayName?: string }).displayName === 'CarouselSlide') {\n slides.push(child);\n } else {\n navigation.push(child);\n }\n });\n\n return (\n <CarouselContext.Provider value={{ instanceRef, currentSlide, slideCount, loop }}>\n <div ref={ref} className={root({ className })} data-testid=\"carousel\">\n <div\n ref={sliderRef}\n className={cn(viewport(), viewportClassName)}\n style={height ? { height } : undefined}\n >\n {slides}\n </div>\n {navigation}\n </div>\n </CarouselContext.Provider>\n );\n }\n);\nCarousel.displayName = 'Carousel';\n\n// ─── CarouselSlide ────────────────────────────────────────────────────────────\n\nexport interface CarouselSlideProps extends React.HTMLAttributes<HTMLDivElement> {\n className?: string;\n children?: React.ReactNode;\n}\n\nconst CarouselSlide = React.forwardRef<HTMLDivElement, CarouselSlideProps>(\n ({ className, children, ...props }, ref) => (\n <div ref={ref} className={slide({ className })} {...props}>\n {children}\n </div>\n )\n);\nCarouselSlide.displayName = 'CarouselSlide';\n\n// ─── CarouselPrev / CarouselNext ──────────────────────────────────────────────\n\nexport interface CarouselArrowProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n className?: string;\n}\n\nconst CarouselPrev = React.forwardRef<HTMLButtonElement, CarouselArrowProps>(\n ({ className, ...props }, ref) => {\n const { instanceRef, currentSlide, loop } = useCarousel();\n const disabled = !loop && currentSlide === 0;\n\n return (\n <button\n ref={ref}\n aria-label=\"Previous slide\"\n disabled={disabled}\n onClick={() => instanceRef.current?.prev()}\n className={arrow({ className: cn('left-2', className) })}\n {...props}\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </button>\n );\n }\n);\nCarouselPrev.displayName = 'CarouselPrev';\n\nconst CarouselNext = React.forwardRef<HTMLButtonElement, CarouselArrowProps>(\n ({ className, ...props }, ref) => {\n const { instanceRef, currentSlide, slideCount, loop } = useCarousel();\n const disabled = !loop && currentSlide === slideCount - 1;\n\n return (\n <button\n ref={ref}\n aria-label=\"Next slide\"\n disabled={disabled}\n onClick={() => instanceRef.current?.next()}\n className={arrow({ className: cn('right-2', className) })}\n {...props}\n >\n <ChevronRight className=\"h-4 w-4\" />\n </button>\n );\n }\n);\nCarouselNext.displayName = 'CarouselNext';\n\n// ─── CarouselDots ─────────────────────────────────────────────────────────────\n\nexport interface CarouselDotsProps extends React.HTMLAttributes<HTMLDivElement> {\n className?: string;\n}\n\nconst CarouselDots = React.forwardRef<HTMLDivElement, CarouselDotsProps>(\n ({ className, ...props }, ref) => {\n const { instanceRef, currentSlide, slideCount } = useCarousel();\n\n if (slideCount === 0) return null;\n\n return (\n <div ref={ref} className={dotsWrapper({ className })} role=\"tablist\" aria-label=\"Carousel navigation\" {...props}>\n {Array.from({ length: slideCount }).map((_, i) => (\n <button\n key={i}\n role=\"tab\"\n aria-selected={i === currentSlide}\n aria-label={`Go to slide ${i + 1}`}\n onClick={() => instanceRef.current?.moveToIdx(i)}\n className={dot({\n className: cn(\n i === currentSlide\n ? 'w-6 bg-primary'\n : 'w-1.5 bg-border hover:bg-muted-foreground'\n ),\n })}\n />\n ))}\n </div>\n );\n }\n);\nCarouselDots.displayName = 'CarouselDots';\n\n// ─── CarouselProgress ─────────────────────────────────────────────────────────\n\nexport interface CarouselProgressProps extends React.HTMLAttributes<HTMLDivElement> {\n className?: string;\n}\n\nconst CarouselProgress = React.forwardRef<HTMLDivElement, CarouselProgressProps>(\n ({ className, ...props }, ref) => {\n const { currentSlide, slideCount } = useCarousel();\n\n const pct = slideCount > 1\n ? Math.round((currentSlide / (slideCount - 1)) * 100)\n : 100;\n\n return (\n <div\n ref={ref}\n role=\"progressbar\"\n aria-valuenow={pct}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label=\"Carousel progress\"\n className={cn('w-full h-1 bg-border rounded-full overflow-hidden mt-3', className)}\n {...props}\n >\n <div\n className=\"h-full bg-primary rounded-full transition-all duration-300\"\n style={{ width: `${pct}%` }}\n />\n </div>\n );\n }\n);\nCarouselProgress.displayName = 'CarouselProgress';\n\n// ─── CarouselCounter ──────────────────────────────────────────────────────────\n\nexport interface CarouselCounterProps extends React.HTMLAttributes<HTMLSpanElement> {\n className?: string;\n}\n\nconst CarouselCounter = React.forwardRef<HTMLSpanElement, CarouselCounterProps>(\n ({ className, ...props }, ref) => {\n const { currentSlide, slideCount } = useCarousel();\n\n return (\n <span\n ref={ref}\n aria-live=\"polite\"\n aria-label=\"Slide counter\"\n className={cn('text-xs text-muted-foreground tabular-nums', className)}\n {...props}\n >\n {currentSlide + 1} / {slideCount}\n </span>\n );\n }\n);\nCarouselCounter.displayName = 'CarouselCounter';\n\n// ─── Exports ──────────────────────────────────────────────────────────────────\n\nexport {\n Carousel,\n CarouselSlide,\n CarouselPrev,\n CarouselNext,\n CarouselDots,\n CarouselProgress,\n CarouselCounter,\n useCarousel,\n};\n"
199
199
  }
200
200
  ]
201
201
  },
@@ -286,7 +286,7 @@
286
286
  "files": [
287
287
  {
288
288
  "path": "src/components/ui/context-menu/ContextMenu.tsx",
289
- "content": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport * as ReactDOM from 'react-dom';\r\nimport { tv } from 'tailwind-variants';\r\nimport { Check, Circle } from 'lucide-react';\r\n\r\nconst contextMenuVariants = tv({\r\n slots: {\r\n content:\r\n 'fixed z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background p-1 text-foreground shadow-md animate-in fade-in-0 zoom-in-95',\r\n item: 'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\r\n checkboxItem:\r\n 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground',\r\n radioItem:\r\n 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground',\r\n label: 'px-2 py-1.5 text-sm font-semibold',\r\n separator: '-mx-1 my-1 h-px bg-border',\r\n shortcut: 'ml-auto text-xs tracking-widest opacity-60',\r\n indicatorWrapper: 'absolute left-2 flex h-3.5 w-3.5 items-center justify-center',\r\n },\r\n});\r\n\r\nconst styles = contextMenuVariants();\r\n\r\n// ─── Context ─────────────────────────────────────────────────────────────────\r\n\r\ninterface ContextMenuState {\r\n open: boolean;\r\n position: { x: number; y: number };\r\n}\r\n\r\nconst ContextMenuContext = React.createContext<{\r\n state: ContextMenuState;\r\n close: () => void;\r\n}>({ state: { open: false, position: { x: 0, y: 0 } }, close: () => {} });\r\n\r\n// ─── Root ─────────────────────────────────────────────────────────────────────\r\n\r\nexport interface ContextMenuProps {\r\n children: React.ReactNode;\r\n}\r\n\r\nconst ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {\r\n const [state, setState] = React.useState<ContextMenuState>({\r\n open: false,\r\n position: { x: 0, y: 0 },\r\n });\r\n\r\n const close = React.useCallback(() => setState(s => ({ ...s, open: false })), []);\r\n\r\n // Close on outside click / second right-click / scroll\r\n React.useEffect(() => {\r\n if (!state.open) return;\r\n const handleClose = () => close();\r\n document.addEventListener('click', handleClose, { capture: true });\r\n document.addEventListener('contextmenu', handleClose, { capture: true });\r\n document.addEventListener('scroll', handleClose, { capture: true, passive: true });\r\n return () => {\r\n document.removeEventListener('click', handleClose, { capture: true });\r\n document.removeEventListener('contextmenu', handleClose, { capture: true });\r\n document.removeEventListener('scroll', handleClose, { capture: true });\r\n };\r\n }, [state.open, close]);\r\n\r\n return (\r\n <ContextMenuContext.Provider value={{ state, close }}>\r\n {React.Children.map(children, child => {\r\n if (React.isValidElement(child) && child.type === ContextMenuTrigger) {\r\n return React.cloneElement(\r\n child as React.ReactElement<{ onContextMenu?: (e: React.MouseEvent) => void }>,\r\n {\r\n onContextMenu: (e: React.MouseEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n setState({ open: true, position: { x: e.clientX, y: e.clientY } });\r\n },\r\n }\r\n );\r\n }\r\n return child;\r\n })}\r\n </ContextMenuContext.Provider>\r\n );\r\n};\r\nContextMenu.displayName = 'ContextMenu';\r\n\r\n// ─── Trigger ──────────────────────────────────────────────────────────────────\r\n\r\nexport interface ContextMenuTriggerProps extends React.HTMLAttributes<HTMLDivElement> {}\r\n\r\nconst ContextMenuTrigger = React.forwardRef<HTMLDivElement, ContextMenuTriggerProps>(\r\n ({ children, ...props }, ref) => (\r\n <div ref={ref} {...props}>{children}</div>\r\n )\r\n);\r\nContextMenuTrigger.displayName = 'ContextMenuTrigger';\r\n\r\n// ─── Content ──────────────────────────────────────────────────────────────────\r\n\r\nexport interface ContextMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {}\r\n\r\nconst ContextMenuContent = React.forwardRef<HTMLDivElement, ContextMenuContentProps>(\r\n ({ className, children, ...props }, ref) => {\r\n const { state, close } = React.useContext(ContextMenuContext);\r\n const contentRef = React.useRef<HTMLDivElement>(null);\r\n\r\n // Merge refs\r\n const mergedRef = React.useCallback(\r\n (node: HTMLDivElement | null) => {\r\n (contentRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\r\n if (typeof ref === 'function') ref(node);\r\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\r\n },\r\n [ref]\r\n );\r\n\r\n // Focus first item on open\r\n React.useEffect(() => {\r\n if (state.open) {\r\n const first = contentRef.current?.querySelector<HTMLElement>('[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"]');\r\n first?.focus();\r\n }\r\n }, [state.open]);\r\n\r\n // Keyboard navigation\r\n const handleKeyDown = React.useCallback(\r\n (e: React.KeyboardEvent<HTMLDivElement>) => {\r\n const items = Array.from(\r\n contentRef.current?.querySelectorAll<HTMLElement>(\r\n '[role=\"menuitem\"]:not([disabled]),[role=\"menuitemcheckbox\"]:not([disabled]),[role=\"menuitemradio\"]:not([disabled])'\r\n ) ?? []\r\n );\r\n const current = document.activeElement as HTMLElement;\r\n const idx = items.indexOf(current);\r\n\r\n if (e.key === 'ArrowDown') {\r\n e.preventDefault();\r\n items[(idx + 1) % items.length]?.focus();\r\n } else if (e.key === 'ArrowUp') {\r\n e.preventDefault();\r\n items[(idx - 1 + items.length) % items.length]?.focus();\r\n } else if (e.key === 'Escape') {\r\n e.preventDefault();\r\n close();\r\n } else if (e.key === 'Tab') {\r\n e.preventDefault();\r\n close();\r\n } else if (e.key === 'Home') {\r\n e.preventDefault();\r\n items[0]?.focus();\r\n } else if (e.key === 'End') {\r\n e.preventDefault();\r\n items[items.length - 1]?.focus();\r\n }\r\n },\r\n [close]\r\n );\r\n\r\n if (!state.open) return null;\r\n\r\n // Clamp position to viewport\r\n const vw = typeof window !== 'undefined' ? window.innerWidth : 0;\r\n const vh = typeof window !== 'undefined' ? window.innerHeight : 0;\r\n const MENU_W = 192; // ~min-w-[8rem] generous estimate\r\n const MENU_H = 300;\r\n const x = Math.min(state.position.x, vw - MENU_W - 8);\r\n const y = Math.min(state.position.y, vh - MENU_H - 8);\r\n\r\n return ReactDOM.createPortal(\r\n <div\r\n ref={mergedRef}\r\n className={styles.content({ className })}\r\n style={{ top: y, left: x }}\r\n role=\"menu\"\r\n aria-orientation=\"vertical\"\r\n onKeyDown={handleKeyDown}\r\n {...props}\r\n >\r\n {children}\r\n </div>,\r\n document.body\r\n );\r\n }\r\n);\r\nContextMenuContent.displayName = 'ContextMenuContent';\r\n\r\n// ─── Item ─────────────────────────────────────────────────────────────────────\r\n\r\nexport interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {\r\n inset?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nconst ContextMenuItem = React.forwardRef<HTMLDivElement, ContextMenuItemProps>(\r\n ({ className, inset, disabled, onClick, ...props }, ref) => {\r\n const { close } = React.useContext(ContextMenuContext);\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"menuitem\"\r\n tabIndex={disabled ? undefined : -1}\r\n aria-disabled={disabled || undefined}\r\n className={styles.item({\r\n className: `${inset ? 'pl-8' : ''} ${disabled ? 'opacity-50 pointer-events-none' : ''} ${className ?? ''}`,\r\n })}\r\n onClick={(e) => {\r\n if (disabled) return;\r\n onClick?.(e);\r\n close();\r\n }}\r\n onKeyDown={(e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n if (!disabled) {\r\n onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);\r\n close();\r\n }\r\n }\r\n }}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nContextMenuItem.displayName = 'ContextMenuItem';\r\n\r\n// ─── CheckboxItem ─────────────────────────────────────────────────────────────\r\n\r\nexport interface ContextMenuCheckboxItemProps extends React.HTMLAttributes<HTMLDivElement> {\r\n checked?: boolean;\r\n onCheckedChange?: (checked: boolean) => void;\r\n}\r\n\r\nconst ContextMenuCheckboxItem = React.forwardRef<HTMLDivElement, ContextMenuCheckboxItemProps>(\r\n ({ className, children, checked, onCheckedChange, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n role=\"menuitemcheckbox\"\r\n tabIndex={-1}\r\n aria-checked={checked}\r\n className={styles.checkboxItem({ className })}\r\n onClick={() => onCheckedChange?.(!checked)}\r\n onKeyDown={(e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n onCheckedChange?.(!checked);\r\n }\r\n }}\r\n {...props}\r\n >\r\n <span className={styles.indicatorWrapper()}>\r\n {checked && <Check className=\"h-4 w-4\" />}\r\n </span>\r\n {children}\r\n </div>\r\n )\r\n);\r\nContextMenuCheckboxItem.displayName = 'ContextMenuCheckboxItem';\r\n\r\n// ─── RadioGroup + RadioItem ───────────────────────────────────────────────────\r\n\r\nconst ContextMenuRadioContext = React.createContext<{\r\n value?: string;\r\n onValueChange?: (value: string) => void;\r\n}>({});\r\n\r\nexport interface ContextMenuRadioGroupProps extends React.HTMLAttributes<HTMLDivElement> {\r\n value?: string;\r\n onValueChange?: (value: string) => void;\r\n}\r\n\r\nconst ContextMenuRadioGroup = React.forwardRef<HTMLDivElement, ContextMenuRadioGroupProps>(\r\n ({ value, onValueChange, ...props }, ref) => (\r\n <ContextMenuRadioContext.Provider value={{ value, onValueChange }}>\r\n <div ref={ref} role=\"group\" {...props} />\r\n </ContextMenuRadioContext.Provider>\r\n )\r\n);\r\nContextMenuRadioGroup.displayName = 'ContextMenuRadioGroup';\r\n\r\nexport interface ContextMenuRadioItemProps extends React.HTMLAttributes<HTMLDivElement> {\r\n value: string;\r\n}\r\n\r\nconst ContextMenuRadioItem = React.forwardRef<HTMLDivElement, ContextMenuRadioItemProps>(\r\n ({ className, children, value, ...props }, ref) => {\r\n const ctx = React.useContext(ContextMenuRadioContext);\r\n const isChecked = ctx.value === value;\r\n return (\r\n <div\r\n ref={ref}\r\n role=\"menuitemradio\"\r\n tabIndex={-1}\r\n aria-checked={isChecked}\r\n className={styles.radioItem({ className })}\r\n onClick={() => ctx.onValueChange?.(value)}\r\n onKeyDown={(e) => {\r\n if (e.key === 'Enter' || e.key === ' ') {\r\n e.preventDefault();\r\n ctx.onValueChange?.(value);\r\n }\r\n }}\r\n {...props}\r\n >\r\n <span className={styles.indicatorWrapper()}>\r\n {isChecked && <Circle className=\"h-2 w-2 fill-current\" />}\r\n </span>\r\n {children}\r\n </div>\r\n );\r\n }\r\n);\r\nContextMenuRadioItem.displayName = 'ContextMenuRadioItem';\r\n\r\n// ─── Label ────────────────────────────────────────────────────────────────────\r\n\r\nconst ContextMenuLabel = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(\r\n ({ className, ...props }, ref) => (\r\n <div ref={ref} className={styles.label({ className })} {...props} />\r\n )\r\n);\r\nContextMenuLabel.displayName = 'ContextMenuLabel';\r\n\r\n// ─── Separator ────────────────────────────────────────────────────────────────\r\n\r\nconst ContextMenuSeparator = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(\r\n ({ className, ...props }, ref) => (\r\n <div ref={ref} role=\"separator\" className={styles.separator({ className })} {...props} />\r\n )\r\n);\r\nContextMenuSeparator.displayName = 'ContextMenuSeparator';\r\n\r\n// ─── Shortcut ─────────────────────────────────────────────────────────────────\r\n\r\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (\r\n <span aria-hidden=\"true\" className={styles.shortcut({ className })} {...props} />\r\n);\r\nContextMenuShortcut.displayName = 'ContextMenuShortcut';\r\n\r\nexport {\r\n ContextMenu,\r\n ContextMenuTrigger,\r\n ContextMenuContent,\r\n ContextMenuItem,\r\n ContextMenuCheckboxItem,\r\n ContextMenuRadioGroup,\r\n ContextMenuRadioItem,\r\n ContextMenuLabel,\r\n ContextMenuSeparator,\r\n ContextMenuShortcut,\r\n contextMenuVariants,\r\n};\r\n"
289
+ "content": "'use client';\n\nimport * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { tv } from 'tailwind-variants';\nimport { Check, Circle } from 'lucide-react';\n\nconst contextMenuVariants = tv({\n slots: {\n content:\n 'fixed z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background p-1 text-foreground shadow-md animate-in fade-in-0 zoom-in-95',\n item: 'relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',\n checkboxItem:\n 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground',\n radioItem:\n 'relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground',\n label: 'px-2 py-1.5 text-sm font-semibold',\n separator: '-mx-1 my-1 h-px bg-border',\n shortcut: 'ml-auto text-xs tracking-widest opacity-60',\n indicatorWrapper: 'absolute left-2 flex h-3.5 w-3.5 items-center justify-center',\n },\n});\n\nconst styles = contextMenuVariants();\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\ninterface ContextMenuState {\n open: boolean;\n position: { x: number; y: number };\n}\n\nconst ContextMenuContext = React.createContext<{\n state: ContextMenuState;\n close: () => void;\n}>({ state: { open: false, position: { x: 0, y: 0 } }, close: () => {} });\n\n// ─── Root ─────────────────────────────────────────────────────────────────────\n\nexport interface ContextMenuProps {\n children: React.ReactNode;\n}\n\nconst ContextMenu: React.FC<ContextMenuProps> = ({ children }) => {\n const [state, setState] = React.useState<ContextMenuState>({\n open: false,\n position: { x: 0, y: 0 },\n });\n\n const close = React.useCallback(() => setState(s => ({ ...s, open: false })), []);\n\n // Close on outside click / second right-click / scroll\n React.useEffect(() => {\n if (!state.open) return;\n const handleClose = () => close();\n document.addEventListener('click', handleClose, { capture: true });\n document.addEventListener('contextmenu', handleClose, { capture: true });\n document.addEventListener('scroll', handleClose, { capture: true, passive: true });\n return () => {\n document.removeEventListener('click', handleClose, { capture: true });\n document.removeEventListener('contextmenu', handleClose, { capture: true });\n document.removeEventListener('scroll', handleClose, { capture: true });\n };\n }, [state.open, close]);\n\n return (\n <ContextMenuContext.Provider value={{ state, close }}>\n {React.Children.map(children, child => {\n if (React.isValidElement(child) && child.type === ContextMenuTrigger) {\n return React.cloneElement(\n child as React.ReactElement<{ onContextMenu?: (e: React.MouseEvent) => void }>,\n {\n onContextMenu: (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setState({ open: true, position: { x: e.clientX, y: e.clientY } });\n },\n }\n );\n }\n return child;\n })}\n </ContextMenuContext.Provider>\n );\n};\nContextMenu.displayName = 'ContextMenu';\n\n// ─── Trigger ──────────────────────────────────────────────────────────────────\n\nexport interface ContextMenuTriggerProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst ContextMenuTrigger = React.forwardRef<HTMLDivElement, ContextMenuTriggerProps>(\n ({ children, ...props }, ref) => (\n <div ref={ref} {...props}>{children}</div>\n )\n);\nContextMenuTrigger.displayName = 'ContextMenuTrigger';\n\n// ─── Content ──────────────────────────────────────────────────────────────────\n\nexport interface ContextMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst ContextMenuContent = React.forwardRef<HTMLDivElement, ContextMenuContentProps>(\n ({ className, children, ...props }, ref) => {\n const { state, close } = React.useContext(ContextMenuContext);\n const contentRef = React.useRef<HTMLDivElement>(null);\n\n // Merge refs\n const mergedRef = React.useCallback(\n (node: HTMLDivElement | null) => {\n (contentRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\n },\n [ref]\n );\n\n // Focus first item on open\n React.useEffect(() => {\n if (state.open) {\n const first = contentRef.current?.querySelector<HTMLElement>('[role=\"menuitem\"],[role=\"menuitemcheckbox\"],[role=\"menuitemradio\"]');\n first?.focus();\n }\n }, [state.open]);\n\n // Keyboard navigation\n const handleKeyDown = React.useCallback(\n (e: React.KeyboardEvent<HTMLDivElement>) => {\n const items = Array.from(\n contentRef.current?.querySelectorAll<HTMLElement>(\n '[role=\"menuitem\"]:not([disabled]),[role=\"menuitemcheckbox\"]:not([disabled]),[role=\"menuitemradio\"]:not([disabled])'\n ) ?? []\n );\n const current = document.activeElement as HTMLElement;\n const idx = items.indexOf(current);\n\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n items[(idx + 1) % items.length]?.focus();\n } else if (e.key === 'ArrowUp') {\n e.preventDefault();\n items[(idx - 1 + items.length) % items.length]?.focus();\n } else if (e.key === 'Escape') {\n e.preventDefault();\n close();\n } else if (e.key === 'Tab') {\n e.preventDefault();\n close();\n } else if (e.key === 'Home') {\n e.preventDefault();\n items[0]?.focus();\n } else if (e.key === 'End') {\n e.preventDefault();\n items[items.length - 1]?.focus();\n }\n },\n [close]\n );\n\n if (!state.open) return null;\n\n // Clamp position to viewport\n const vw = typeof window !== 'undefined' ? window.innerWidth : 0;\n const vh = typeof window !== 'undefined' ? window.innerHeight : 0;\n const MENU_W = 192; // ~min-w-[8rem] generous estimate\n const MENU_H = 300;\n const x = Math.min(state.position.x, vw - MENU_W - 8);\n const y = Math.min(state.position.y, vh - MENU_H - 8);\n\n return ReactDOM.createPortal(\n <div\n ref={mergedRef}\n className={styles.content({ className })}\n style={{ top: y, left: x }}\n role=\"menu\"\n aria-orientation=\"vertical\"\n onKeyDown={handleKeyDown}\n {...props}\n >\n {children}\n </div>,\n document.body\n );\n }\n);\nContextMenuContent.displayName = 'ContextMenuContent';\n\n// ─── Item ─────────────────────────────────────────────────────────────────────\n\nexport interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {\n inset?: boolean;\n disabled?: boolean;\n}\n\nconst ContextMenuItem = React.forwardRef<HTMLDivElement, ContextMenuItemProps>(\n ({ className, inset, disabled, onClick, ...props }, ref) => {\n const { close } = React.useContext(ContextMenuContext);\n return (\n <div\n ref={ref}\n role=\"menuitem\"\n tabIndex={disabled ? undefined : -1}\n aria-disabled={disabled || undefined}\n className={styles.item({\n className: `${inset ? 'pl-8' : ''} ${disabled ? 'opacity-50 pointer-events-none' : ''} ${className ?? ''}`,\n })}\n onClick={(e) => {\n if (disabled) return;\n onClick?.(e);\n close();\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n if (!disabled) {\n onClick?.(e as unknown as React.MouseEvent<HTMLDivElement>);\n close();\n }\n }\n }}\n {...props}\n />\n );\n }\n);\nContextMenuItem.displayName = 'ContextMenuItem';\n\n// ─── CheckboxItem ─────────────────────────────────────────────────────────────\n\nexport interface ContextMenuCheckboxItemProps extends React.HTMLAttributes<HTMLDivElement> {\n checked?: boolean;\n onCheckedChange?: (checked: boolean) => void;\n}\n\nconst ContextMenuCheckboxItem = React.forwardRef<HTMLDivElement, ContextMenuCheckboxItemProps>(\n ({ className, children, checked, onCheckedChange, ...props }, ref) => (\n <div\n ref={ref}\n role=\"menuitemcheckbox\"\n tabIndex={-1}\n aria-checked={checked}\n className={styles.checkboxItem({ className })}\n onClick={() => onCheckedChange?.(!checked)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onCheckedChange?.(!checked);\n }\n }}\n {...props}\n >\n <span className={styles.indicatorWrapper()}>\n {checked && <Check className=\"h-4 w-4\" />}\n </span>\n {children}\n </div>\n )\n);\nContextMenuCheckboxItem.displayName = 'ContextMenuCheckboxItem';\n\n// ─── RadioGroup + RadioItem ───────────────────────────────────────────────────\n\nconst ContextMenuRadioContext = React.createContext<{\n value?: string;\n onValueChange?: (value: string) => void;\n}>({});\n\nexport interface ContextMenuRadioGroupProps extends React.HTMLAttributes<HTMLDivElement> {\n value?: string;\n onValueChange?: (value: string) => void;\n}\n\nconst ContextMenuRadioGroup = React.forwardRef<HTMLDivElement, ContextMenuRadioGroupProps>(\n ({ value, onValueChange, ...props }, ref) => (\n <ContextMenuRadioContext.Provider value={{ value, onValueChange }}>\n <div ref={ref} role=\"group\" {...props} />\n </ContextMenuRadioContext.Provider>\n )\n);\nContextMenuRadioGroup.displayName = 'ContextMenuRadioGroup';\n\nexport interface ContextMenuRadioItemProps extends React.HTMLAttributes<HTMLDivElement> {\n value: string;\n}\n\nconst ContextMenuRadioItem = React.forwardRef<HTMLDivElement, ContextMenuRadioItemProps>(\n ({ className, children, value, ...props }, ref) => {\n const ctx = React.useContext(ContextMenuRadioContext);\n const isChecked = ctx.value === value;\n return (\n <div\n ref={ref}\n role=\"menuitemradio\"\n tabIndex={-1}\n aria-checked={isChecked}\n className={styles.radioItem({ className })}\n onClick={() => ctx.onValueChange?.(value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n ctx.onValueChange?.(value);\n }\n }}\n {...props}\n >\n <span className={styles.indicatorWrapper()}>\n {isChecked && <Circle className=\"h-2 w-2 fill-current\" />}\n </span>\n {children}\n </div>\n );\n }\n);\nContextMenuRadioItem.displayName = 'ContextMenuRadioItem';\n\n// ─── Label ────────────────────────────────────────────────────────────────────\n\nconst ContextMenuLabel = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(\n ({ className, ...props }, ref) => (\n <div ref={ref} className={styles.label({ className })} {...props} />\n )\n);\nContextMenuLabel.displayName = 'ContextMenuLabel';\n\n// ─── Separator ────────────────────────────────────────────────────────────────\n\nconst ContextMenuSeparator = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(\n ({ className, ...props }, ref) => (\n <div ref={ref} role=\"separator\" className={styles.separator({ className })} {...props} />\n )\n);\nContextMenuSeparator.displayName = 'ContextMenuSeparator';\n\n// ─── Shortcut ─────────────────────────────────────────────────────────────────\n\nconst ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (\n <span aria-hidden=\"true\" className={styles.shortcut({ className })} {...props} />\n);\nContextMenuShortcut.displayName = 'ContextMenuShortcut';\n\nexport {\n ContextMenu,\n ContextMenuTrigger,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuCheckboxItem,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n ContextMenuShortcut,\n contextMenuVariants,\n};\n"
290
290
  }
291
291
  ]
292
292
  },
@@ -309,7 +309,7 @@
309
309
  },
310
310
  {
311
311
  "path": "src/components/ui/datepicker/index.ts",
312
- "content": "export { DatePicker } from './DatePicker'\r\nexport type { DatePickerProps, DatePickerMode, TimeFormat, TimePickerStyle } from './DatePicker'\r\n"
312
+ "content": "export { DatePicker } from './DatePicker'\nexport type { DatePickerProps, DatePickerMode, TimeFormat, TimePickerStyle } from './DatePicker'\n"
313
313
  }
314
314
  ]
315
315
  },
@@ -339,7 +339,7 @@
339
339
  "files": [
340
340
  {
341
341
  "path": "src/components/ui/drawer/Drawer.tsx",
342
- "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\n\r\nconst drawerVariants = tv({\r\n slots: {\r\n overlay:\r\n 'fixed inset-0 z-50 bg-black/40 data-starting:animate-in data-ending:animate-out data-ending:fade-out-0 data-starting:fade-in-0',\r\n panel: [\r\n 'fixed z-50 bg-background shadow-2xl flex flex-col',\r\n 'data-starting:animate-in data-ending:animate-out duration-300',\r\n 'outline-none overflow-hidden m-0 p-0 max-w-full max-h-full border-none',\r\n ],\r\n header:\r\n 'flex items-center justify-between px-6 py-4 border-b border-border/50 shrink-0',\r\n title: 'text-base font-semibold text-foreground',\r\n description: 'text-sm text-muted-foreground mt-0.5',\r\n body: 'flex-1 overflow-y-auto px-6 py-4',\r\n footer: 'px-6 py-4 border-t border-border/50 shrink-0',\r\n close:\r\n 'rounded-sm opacity-70 hover:opacity-100 transition-opacity ring-offset-background focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',\r\n },\r\n variants: {\r\n direction: {\r\n left: {\r\n panel:\r\n 'inset-y-0 left-0 h-full data-[starting-style]:-translate-x-full data-[ending-style]:-translate-x-full transition-transform',\r\n },\r\n right: {\r\n panel:\r\n 'inset-y-0 right-0 h-full data-[starting-style]:translate-x-full data-[ending-style]:translate-x-full transition-transform',\r\n },\r\n top: {\r\n panel:\r\n 'inset-x-0 top-0 w-full data-[starting-style]:-translate-y-full data-[ending-style]:-translate-y-full transition-transform',\r\n },\r\n bottom: {\r\n panel:\r\n 'inset-x-0 bottom-0 w-full data-[starting-style]:translate-y-full data-[ending-style]:translate-y-full transition-transform',\r\n },\r\n },\r\n size: {\r\n sm: {},\r\n md: {},\r\n lg: {},\r\n full: {},\r\n },\r\n backdropBlur: {\r\n true: { overlay: 'backdrop-blur-sm' },\r\n false: { overlay: '' },\r\n },\r\n },\r\n compoundVariants: [\r\n { direction: 'left', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'left', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'left', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'left', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'right', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'right', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'right', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'right', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'top', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'top', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'top', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'top', size: 'full', class: { panel: 'h-full' } },\r\n { direction: 'bottom', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'bottom', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'bottom', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'bottom', size: 'full', class: { panel: 'h-full' } },\r\n ],\r\n defaultVariants: {\r\n direction: 'right',\r\n size: 'md',\r\n backdropBlur: true,\r\n },\r\n});\r\n\r\n/* ─── Root ─── */\r\nconst Drawer = BaseDialog.Root;\r\n\r\n/* ─── Trigger ─── */\r\nconst DrawerTrigger = BaseDialog.Trigger;\r\n\r\n/* ─── Close (re-export for custom close buttons) ─── */\r\nconst DrawerClose = BaseDialog.Close;\r\n\r\n/* ─── Content (Portal + Backdrop + Popup) ─── */\r\ninterface DrawerContentProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>, 'className'>,\r\n VariantProps<typeof drawerVariants> {\r\n className?: string;\r\n}\r\n\r\nconst DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(\r\n ({ className, children, direction, size, backdropBlur, ...props }, ref) => {\r\n const slots = drawerVariants({ direction, size, backdropBlur });\r\n return (\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup ref={ref} className={slots.panel({ className })} {...props}>\r\n {children}\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n );\r\n },\r\n);\r\nDrawerContent.displayName = 'DrawerContent';\r\n\r\n/* ─── Header (includes close button by default) ─── */\r\ninterface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\r\n hideClose?: boolean;\r\n}\r\n\r\nconst DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\r\n ({ className, children, hideClose, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return (\r\n <div ref={ref} className={slots.header({ className })} {...props}>\r\n <div>{children}</div>\r\n {!hideClose && (\r\n <BaseDialog.Close className={slots.close()} aria-label=\"Close\">\r\n <X className=\"h-4 w-4\" />\r\n </BaseDialog.Close>\r\n )}\r\n </div>\r\n );\r\n },\r\n);\r\nDrawerHeader.displayName = 'DrawerHeader';\r\n\r\n/* ─── Title ─── */\r\nconst DrawerTitle = React.forwardRef<\r\n HTMLHeadingElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>, 'className'> & {\r\n className?: string;\r\n }\r\n>(({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <BaseDialog.Title ref={ref} className={slots.title({ className })} {...props} />;\r\n});\r\nDrawerTitle.displayName = 'DrawerTitle';\r\n\r\n/* ─── Description ─── */\r\nconst DrawerDescription = React.forwardRef<\r\n HTMLParagraphElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>, 'className'> & {\r\n className?: string;\r\n }\r\n>(({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return (\r\n <BaseDialog.Description ref={ref} className={slots.description({ className })} {...props} />\r\n );\r\n});\r\nDrawerDescription.displayName = 'DrawerDescription';\r\n\r\n/* ─── Body (scrollable content area) ─── */\r\nconst DrawerBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <div ref={ref} className={slots.body({ className })} {...props} />;\r\n },\r\n);\r\nDrawerBody.displayName = 'DrawerBody';\r\n\r\n/* ─── Footer ─── */\r\nconst DrawerFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <div ref={ref} className={slots.footer({ className })} {...props} />;\r\n },\r\n);\r\nDrawerFooter.displayName = 'DrawerFooter';\r\n\r\nexport {\r\n Drawer,\r\n DrawerTrigger,\r\n DrawerContent,\r\n DrawerHeader,\r\n DrawerTitle,\r\n DrawerDescription,\r\n DrawerBody,\r\n DrawerFooter,\r\n DrawerClose,\r\n};\r\nexport type { DrawerContentProps, DrawerHeaderProps };\r\n"
342
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\n\r\nconst drawerVariants = tv({\r\n slots: {\r\n overlay: [\r\n 'fixed inset-0 z-50 bg-black/40',\r\n 'transition-opacity duration-200 ease-out',\r\n 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',\r\n ],\r\n panel: [\r\n 'fixed z-50 bg-background shadow-2xl flex flex-col',\r\n 'outline-none overflow-hidden m-0 p-0 max-w-full max-h-full border-none',\r\n 'transition duration-300 ease-out',\r\n 'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',\r\n ],\r\n header:\r\n 'flex items-center justify-between px-6 py-4 border-b border-border/50 shrink-0',\r\n title: 'text-base font-semibold text-foreground',\r\n description: 'text-sm text-muted-foreground mt-0.5',\r\n body: 'flex-1 overflow-y-auto px-6 py-4',\r\n footer: 'px-6 py-4 border-t border-border/50 shrink-0',\r\n close:\r\n 'rounded-sm opacity-70 hover:opacity-100 transition-opacity ring-offset-background focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',\r\n },\r\n variants: {\r\n direction: {\r\n left: {\r\n panel: [\r\n 'inset-y-0 left-0 h-full',\r\n 'data-[starting-style]:-translate-x-full data-[ending-style]:-translate-x-full',\r\n ],\r\n },\r\n right: {\r\n panel: [\r\n 'inset-y-0 right-0 h-full',\r\n 'data-[starting-style]:translate-x-full data-[ending-style]:translate-x-full',\r\n ],\r\n },\r\n top: {\r\n panel: [\r\n 'inset-x-0 top-0 w-full',\r\n 'data-[starting-style]:-translate-y-full data-[ending-style]:-translate-y-full',\r\n ],\r\n },\r\n bottom: {\r\n panel: [\r\n 'inset-x-0 bottom-0 w-full',\r\n 'data-[starting-style]:translate-y-full data-[ending-style]:translate-y-full',\r\n ],\r\n },\r\n },\r\n size: {\r\n sm: {},\r\n md: {},\r\n lg: {},\r\n full: {},\r\n },\r\n backdropBlur: {\r\n true: { overlay: 'backdrop-blur-sm' },\r\n false: { overlay: '' },\r\n },\r\n },\r\n compoundVariants: [\r\n { direction: 'left', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'left', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'left', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'left', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'right', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'right', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'right', size: 'lg', class: { panel: 'w-[500px]' } },\r\n { direction: 'right', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'top', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'top', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'top', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'top', size: 'full', class: { panel: 'h-full' } },\r\n { direction: 'bottom', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'bottom', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'bottom', size: 'lg', class: { panel: 'h-[500px]' } },\r\n { direction: 'bottom', size: 'full', class: { panel: 'h-full' } },\r\n ],\r\n defaultVariants: {\r\n direction: 'right',\r\n size: 'md',\r\n backdropBlur: true,\r\n },\r\n});\r\n\r\n/* ─── Root ─── */\r\nconst Drawer = BaseDialog.Root;\r\n\r\n/* ─── Trigger ─── */\r\nconst DrawerTrigger = BaseDialog.Trigger;\r\n\r\n/* ─── Close (re-export for custom close buttons) ─── */\r\nconst DrawerClose = BaseDialog.Close;\r\n\r\n/* ─── Content (Portal + Backdrop + Popup) ─── */\r\ninterface DrawerContentProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>, 'className'>,\r\n VariantProps<typeof drawerVariants> {\r\n className?: string;\r\n}\r\n\r\nconst DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(\r\n ({ className, children, direction, size, backdropBlur, ...props }, ref) => {\r\n const slots = drawerVariants({ direction, size, backdropBlur });\r\n return (\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup ref={ref} className={slots.panel({ className })} {...props}>\r\n {children}\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n );\r\n },\r\n);\r\nDrawerContent.displayName = 'DrawerContent';\r\n\r\n/* ─── Header (includes close button by default) ─── */\r\ninterface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\r\n hideClose?: boolean;\r\n}\r\n\r\nconst DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\r\n ({ className, children, hideClose, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return (\r\n <div ref={ref} className={slots.header({ className })} {...props}>\r\n <div>{children}</div>\r\n {!hideClose && (\r\n <BaseDialog.Close className={slots.close()} aria-label=\"Close\">\r\n <X className=\"h-4 w-4\" />\r\n </BaseDialog.Close>\r\n )}\r\n </div>\r\n );\r\n },\r\n);\r\nDrawerHeader.displayName = 'DrawerHeader';\r\n\r\n/* ─── Title ─── */\r\nconst DrawerTitle = React.forwardRef<\r\n HTMLHeadingElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>, 'className'> & {\r\n className?: string;\r\n }\r\n>(({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <BaseDialog.Title ref={ref} className={slots.title({ className })} {...props} />;\r\n});\r\nDrawerTitle.displayName = 'DrawerTitle';\r\n\r\n/* ─── Description ─── */\r\nconst DrawerDescription = React.forwardRef<\r\n HTMLParagraphElement,\r\n Omit<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>, 'className'> & {\r\n className?: string;\r\n }\r\n>(({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return (\r\n <BaseDialog.Description ref={ref} className={slots.description({ className })} {...props} />\r\n );\r\n});\r\nDrawerDescription.displayName = 'DrawerDescription';\r\n\r\n/* ─── Body (scrollable content area) ─── */\r\nconst DrawerBody = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <div ref={ref} className={slots.body({ className })} {...props} />;\r\n },\r\n);\r\nDrawerBody.displayName = 'DrawerBody';\r\n\r\n/* ─── Footer ─── */\r\nconst DrawerFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const slots = drawerVariants();\r\n return <div ref={ref} className={slots.footer({ className })} {...props} />;\r\n },\r\n);\r\nDrawerFooter.displayName = 'DrawerFooter';\r\n\r\nexport {\r\n Drawer,\r\n DrawerTrigger,\r\n DrawerContent,\r\n DrawerHeader,\r\n DrawerTitle,\r\n DrawerDescription,\r\n DrawerBody,\r\n DrawerFooter,\r\n DrawerClose,\r\n};\r\nexport type { DrawerContentProps, DrawerHeaderProps };\r\n"
343
343
  }
344
344
  ]
345
345
  },
@@ -449,7 +449,7 @@
449
449
  "files": [
450
450
  {
451
451
  "path": "src/components/ui/layout-demo1/constants/route.constants.tsx",
452
- "content": "import React from \"react\";\nimport { BookOpen, Info, Settings } from \"lucide-react\";\n\ninterface SidebarItem {\n title: string;\n href: string;\n icon?: React.ReactNode;\n badge?: string;\n}\n\ninterface SidebarSection {\n title: string;\n icon?: React.ReactNode;\n collapsible: boolean;\n defaultOpen?: boolean;\n items: SidebarItem[];\n}\n\nexport const routesConfig: SidebarSection[] = [\n {\n title: \"Tổng quan\",\n icon: <BookOpen className=\"w-4 h-4\" />,\n collapsible: false,\n items: [\n {\n title: \"Giới thiệu\",\n href: \"/blocks\",\n icon: <Info className=\"w-4 h-4\" />,\n },\n {\n title: \"Guideline & Cài đặt CLI\",\n href: \"/#\",\n icon: <Settings className=\"w-4 h-4\" />,\n badge: \"CLI\",\n },\n ],\n },\n];\n"
452
+ "content": "import React from \"react\";\r\nimport { BookOpen, Info, Settings } from \"lucide-react\";\r\n\r\ninterface SidebarItem {\r\n title: string;\r\n href: string;\r\n icon?: React.ReactNode;\r\n badge?: string;\r\n}\r\n\r\ninterface SidebarSection {\r\n title: string;\r\n icon?: React.ReactNode;\r\n collapsible: boolean;\r\n defaultOpen?: boolean;\r\n items: SidebarItem[];\r\n}\r\n\r\nexport const routesConfig: SidebarSection[] = [\r\n {\r\n title: \"Tổng quan\",\r\n icon: <BookOpen className=\"w-4 h-4\" />,\r\n collapsible: false,\r\n items: [\r\n {\r\n title: \"Giới thiệu\",\r\n href: \"/blocks\",\r\n icon: <Info className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Guideline & Cài đặt CLI\",\r\n href: \"/#\",\r\n icon: <Settings className=\"w-4 h-4\" />,\r\n badge: \"CLI\",\r\n },\r\n ],\r\n },\r\n];\r\n"
453
453
  },
454
454
  {
455
455
  "path": "src/components/ui/layout-demo1/layout.tsx",
@@ -469,7 +469,7 @@
469
469
  },
470
470
  {
471
471
  "path": "src/components/ui/layout-demo1/_layout/SideBar.tsx",
472
- "content": "import React from \"react\";\nimport { Link, useLocation } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils/cn\";\nimport {\n Sidebar,\n SidebarHeader,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarGroupContent,\n SidebarMenu,\n SidebarMenuItem,\n SidebarMenuButton,\n SidebarMenuCollapsible,\n SidebarMenuSubItem,\n useSidebar,\n} from \"@/components/ui/sidebar/Sidebar\";\nimport { routesConfig } from \"../constants/route.constants\";\n\nconst SideBar = () => {\n const { pathname } = useLocation();\n const { state } = useSidebar();\n const isCollapsed = state === \"collapsed\";\n return (\n <Sidebar collapsible=\"icon\" variant=\"sidebar\" side=\"left\">\n <SidebarHeader>\n <Link to=\"/blocks\">\n <div\n className={cn(\n \"flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200\",\n isCollapsed && \"justify-center\",\n )}\n >\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm flex-none\">\n <span className=\"text-primary-foreground font-bold text-sm select-none\">\n UI\n </span>\n </div>\n {!isCollapsed && (\n <div className=\"overflow-hidden min-w-0\">\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">\n UI Library\n </p>\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">\n Component Showcase\n </p>\n </div>\n )}\n </div>\n </Link>\n </SidebarHeader>\n\n <SidebarContent>\n {routesConfig.map((route) => {\n const isChildActive = route.items.some(\n (item) => pathname === item.href,\n );\n const shouldDefaultOpen = isChildActive || route.defaultOpen === true;\n\n return (\n <SidebarGroup key={route.title}>\n {route.collapsible ? (\n <>\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuCollapsible\n id={route.title}\n icon={\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {route.icon}\n </span>\n }\n label={route.title}\n defaultOpen={shouldDefaultOpen}\n isChildActive={isChildActive}\n >\n {route.items.map((item) => {\n const isActive = pathname === item.href;\n return (\n <SidebarMenuSubItem key={item.href}>\n <Link to={item.href}>\n <SidebarMenuButton\n isActive={isActive}\n tooltip={item.title}\n size=\"sm\"\n className={cn(\n isActive &&\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\n )}\n >\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {item.icon}\n </span>\n <span className=\"truncate\">{item.title}</span>\n </SidebarMenuButton>\n </Link>\n </SidebarMenuSubItem>\n );\n })}\n </SidebarMenuCollapsible>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarGroupContent>\n </>\n ) : (\n <>\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {route.items.map((item) => {\n const isActive = pathname === item.href;\n return (\n <SidebarMenuItem key={item.href}>\n <Link to={item.href}>\n <SidebarMenuButton\n isActive={isActive}\n tooltip={item.title}\n size=\"sm\"\n className={cn(\n isActive &&\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\n )}\n >\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {item.icon}\n </span>\n <span className=\"truncate\">{item.title}</span>\n </SidebarMenuButton>\n </Link>\n </SidebarMenuItem>\n );\n })}\n </SidebarMenu>\n </SidebarGroupContent>\n </>\n )}\n </SidebarGroup>\n );\n })}\n </SidebarContent>\n </Sidebar>\n );\n};\n\nexport default SideBar;\n"
472
+ "content": "import React from \"react\";\r\nimport { Link, useLocation } from \"react-router-dom\";\r\nimport { cn } from \"@/lib/utils/cn\";\r\nimport {\r\n Sidebar,\r\n SidebarHeader,\r\n SidebarContent,\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarMenuButton,\r\n SidebarMenuCollapsible,\r\n SidebarMenuSubItem,\r\n useSidebar,\r\n} from \"@/components/ui/sidebar/Sidebar\";\r\nimport { routesConfig } from \"../constants/route.constants\";\r\n\r\nconst SideBar = () => {\r\n const { pathname } = useLocation();\r\n const { state } = useSidebar();\r\n const isCollapsed = state === \"collapsed\";\r\n return (\r\n <Sidebar collapsible=\"icon\" variant=\"sidebar\" side=\"left\">\r\n <SidebarHeader>\r\n <Link to=\"/blocks\">\r\n <div\r\n className={cn(\r\n \"flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200\",\r\n isCollapsed && \"justify-center\",\r\n )}\r\n >\r\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm flex-none\">\r\n <span className=\"text-primary-foreground font-bold text-sm select-none\">\r\n UI\r\n </span>\r\n </div>\r\n {!isCollapsed && (\r\n <div className=\"overflow-hidden min-w-0\">\r\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">\r\n UI Library\r\n </p>\r\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">\r\n Component Showcase\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n </Link>\r\n </SidebarHeader>\r\n\r\n <SidebarContent>\r\n {routesConfig.map((route) => {\r\n const isChildActive = route.items.some(\r\n (item) => pathname === item.href,\r\n );\r\n const shouldDefaultOpen = isChildActive || route.defaultOpen === true;\r\n\r\n return (\r\n <SidebarGroup key={route.title}>\r\n {route.collapsible ? (\r\n <>\r\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuCollapsible\r\n id={route.title}\r\n icon={\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {route.icon}\r\n </span>\r\n }\r\n label={route.title}\r\n defaultOpen={shouldDefaultOpen}\r\n isChildActive={isChildActive}\r\n >\r\n {route.items.map((item) => {\r\n const isActive = pathname === item.href;\r\n return (\r\n <SidebarMenuSubItem key={item.href}>\r\n <Link to={item.href}>\r\n <SidebarMenuButton\r\n isActive={isActive}\r\n tooltip={item.title}\r\n size=\"sm\"\r\n className={cn(\r\n isActive &&\r\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\r\n )}\r\n >\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {item.icon}\r\n </span>\r\n <span className=\"truncate\">{item.title}</span>\r\n </SidebarMenuButton>\r\n </Link>\r\n </SidebarMenuSubItem>\r\n );\r\n })}\r\n </SidebarMenuCollapsible>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </>\r\n ) : (\r\n <>\r\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n {route.items.map((item) => {\r\n const isActive = pathname === item.href;\r\n return (\r\n <SidebarMenuItem key={item.href}>\r\n <Link to={item.href}>\r\n <SidebarMenuButton\r\n isActive={isActive}\r\n tooltip={item.title}\r\n size=\"sm\"\r\n className={cn(\r\n isActive &&\r\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\r\n )}\r\n >\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {item.icon}\r\n </span>\r\n <span className=\"truncate\">{item.title}</span>\r\n </SidebarMenuButton>\r\n </Link>\r\n </SidebarMenuItem>\r\n );\r\n })}\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </>\r\n )}\r\n </SidebarGroup>\r\n );\r\n })}\r\n </SidebarContent>\r\n </Sidebar>\r\n );\r\n};\r\n\r\nexport default SideBar;\r\n"
473
473
  }
474
474
  ]
475
475
  },
@@ -491,7 +491,7 @@
491
491
  "files": [
492
492
  {
493
493
  "path": "src/components/ui/layout-demo2/constants/route.constants.tsx",
494
- "content": "import React from \"react\";\nimport {\n BookOpen,\n ChevronsUpDown,\n FormInput,\n Info,\n LayoutGrid,\n ListChecks,\n Minus,\n ScrollText,\n Settings,\n Square,\n Tag,\n TextCursorInput,\n UserCircle,\n} from \"lucide-react\";\n\ninterface SidebarItem {\n title: string;\n href: string;\n icon?: React.ReactNode;\n badge?: string;\n}\n\ninterface SidebarSection {\n title: string;\n icon?: React.ReactNode;\n collapsible: boolean;\n defaultOpen?: boolean;\n items: SidebarItem[];\n}\n\nexport const routesConfig: SidebarSection[] = [\n {\n title: \"Tổng quan\",\n icon: <BookOpen className=\"w-4 h-4\" />,\n collapsible: false,\n items: [\n {\n title: \"Giới thiệu\",\n href: \"/blocks\",\n icon: <Info className=\"w-4 h-4\" />,\n },\n {\n title: \"Guideline & Cài đặt CLI\",\n href: \"/#\",\n icon: <Settings className=\"w-4 h-4\" />,\n badge: \"CLI\",\n },\n ],\n },\n {\n title: \"General\",\n icon: <LayoutGrid className=\"w-4 h-4\" />,\n collapsible: true,\n defaultOpen: true,\n items: [\n {\n title: \"Button\",\n href: \"/#\",\n icon: <Square className=\"w-4 h-4\" />,\n },\n {\n title: \"Badge\",\n href: \"/#\",\n icon: <Tag className=\"w-4 h-4\" />,\n },\n {\n title: \"Avatar\",\n href: \"/#\",\n icon: <UserCircle className=\"w-4 h-4\" />,\n },\n {\n title: \"Separator\",\n href: \"/#\",\n icon: <Minus className=\"w-4 h-4\" />,\n },\n ],\n },\n {\n title: \"Forms\",\n icon: <FormInput className=\"w-4 h-4\" />,\n collapsible: true,\n defaultOpen: true,\n items: [\n {\n title: \"Input\",\n href: \"/components-page/input\",\n icon: <TextCursorInput className=\"w-4 h-4\" />,\n },\n {\n title: \"Textarea\",\n href: \"/components-page/textarea\",\n icon: <ScrollText className=\"w-4 h-4\" />,\n },\n {\n title: \"Select\",\n href: \"/components-page/select\",\n icon: <ChevronsUpDown className=\"w-4 h-4\" />,\n },\n {\n title: \"Checkbox\",\n href: \"/components-page/checkbox\",\n icon: <ListChecks className=\"w-4 h-4\" />,\n },\n ],\n },\n];\n"
494
+ "content": "import React from \"react\";\r\nimport {\r\n BookOpen,\r\n ChevronsUpDown,\r\n FormInput,\r\n Info,\r\n LayoutGrid,\r\n ListChecks,\r\n Minus,\r\n ScrollText,\r\n Settings,\r\n Square,\r\n Tag,\r\n TextCursorInput,\r\n UserCircle,\r\n} from \"lucide-react\";\r\n\r\ninterface SidebarItem {\r\n title: string;\r\n href: string;\r\n icon?: React.ReactNode;\r\n badge?: string;\r\n}\r\n\r\ninterface SidebarSection {\r\n title: string;\r\n icon?: React.ReactNode;\r\n collapsible: boolean;\r\n defaultOpen?: boolean;\r\n items: SidebarItem[];\r\n}\r\n\r\nexport const routesConfig: SidebarSection[] = [\r\n {\r\n title: \"Tổng quan\",\r\n icon: <BookOpen className=\"w-4 h-4\" />,\r\n collapsible: false,\r\n items: [\r\n {\r\n title: \"Giới thiệu\",\r\n href: \"/blocks\",\r\n icon: <Info className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Guideline & Cài đặt CLI\",\r\n href: \"/#\",\r\n icon: <Settings className=\"w-4 h-4\" />,\r\n badge: \"CLI\",\r\n },\r\n ],\r\n },\r\n {\r\n title: \"General\",\r\n icon: <LayoutGrid className=\"w-4 h-4\" />,\r\n collapsible: true,\r\n defaultOpen: true,\r\n items: [\r\n {\r\n title: \"Button\",\r\n href: \"/#\",\r\n icon: <Square className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Badge\",\r\n href: \"/#\",\r\n icon: <Tag className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Avatar\",\r\n href: \"/#\",\r\n icon: <UserCircle className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Separator\",\r\n href: \"/#\",\r\n icon: <Minus className=\"w-4 h-4\" />,\r\n },\r\n ],\r\n },\r\n {\r\n title: \"Forms\",\r\n icon: <FormInput className=\"w-4 h-4\" />,\r\n collapsible: true,\r\n defaultOpen: true,\r\n items: [\r\n {\r\n title: \"Input\",\r\n href: \"/components-page/input\",\r\n icon: <TextCursorInput className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Textarea\",\r\n href: \"/components-page/textarea\",\r\n icon: <ScrollText className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Select\",\r\n href: \"/components-page/select\",\r\n icon: <ChevronsUpDown className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Checkbox\",\r\n href: \"/components-page/checkbox\",\r\n icon: <ListChecks className=\"w-4 h-4\" />,\r\n },\r\n ],\r\n },\r\n];\r\n"
495
495
  },
496
496
  {
497
497
  "path": "src/components/ui/layout-demo2/layout.tsx",
@@ -511,7 +511,7 @@
511
511
  },
512
512
  {
513
513
  "path": "src/components/ui/layout-demo2/_layout/SideBar.tsx",
514
- "content": "import React from \"react\";\nimport { Link, useLocation } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils/cn\";\nimport {\n Sidebar,\n SidebarHeader,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarGroupContent,\n SidebarMenu,\n SidebarMenuItem,\n SidebarMenuButton,\n SidebarMenuCollapsible,\n SidebarMenuSubItem,\n useSidebar,\n} from \"@/components/ui/sidebar/Sidebar\";\nimport { routesConfig } from \"../constants/route.constants\";\n\nconst SideBar = () => {\n const { pathname } = useLocation();\n const { state } = useSidebar();\n const isCollapsed = state === \"collapsed\";\n return (\n <Sidebar collapsible=\"icon\" variant=\"sidebar\" side=\"left\" data-lenis-prevent>\n <SidebarHeader>\n <Link to=\"/blocks\">\n <div\n className={cn(\n \"flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200\",\n isCollapsed && \"justify-center\",\n )}\n >\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm flex-none\">\n <span className=\"text-primary-foreground font-bold text-sm select-none\">\n UI\n </span>\n </div>\n {!isCollapsed && (\n <div className=\"overflow-hidden min-w-0\">\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">\n UI Library\n </p>\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">\n Component Showcase\n </p>\n </div>\n )}\n </div>\n </Link>\n </SidebarHeader>\n\n <SidebarContent>\n {routesConfig.map((route, idx) => {\n const isChildActive = route.items.some(\n (item) => pathname === item.href,\n );\n const shouldDefaultOpen = isChildActive || route.defaultOpen === true;\n\n return (\n <SidebarGroup key={idx}>\n {route.collapsible ? (\n <>\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuCollapsible\n id={route.title}\n icon={\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {route.icon}\n </span>\n }\n label={route.title}\n defaultOpen={shouldDefaultOpen}\n isChildActive={isChildActive}\n >\n {route.items.map((item, i) => {\n const isActive = pathname === item.href;\n return (\n <SidebarMenuSubItem key={i}>\n <Link to={item.href}>\n <SidebarMenuButton\n isActive={isActive}\n tooltip={item.title}\n size=\"sm\"\n className={cn(\n isActive &&\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\n )}\n >\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {item.icon}\n </span>\n <span className=\"truncate\">{item.title}</span>\n </SidebarMenuButton>\n </Link>\n </SidebarMenuSubItem>\n );\n })}\n </SidebarMenuCollapsible>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarGroupContent>\n </>\n ) : (\n <>\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {route.items.map((item, i) => {\n const isActive = pathname === item.href;\n return (\n <SidebarMenuItem key={i}>\n <Link to={item.href}>\n <SidebarMenuButton\n isActive={isActive}\n tooltip={item.title}\n size=\"sm\"\n className={cn(\n isActive &&\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\n )}\n >\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {item.icon}\n </span>\n <span className=\"truncate\">{item.title}</span>\n </SidebarMenuButton>\n </Link>\n </SidebarMenuItem>\n );\n })}\n </SidebarMenu>\n </SidebarGroupContent>\n </>\n )}\n </SidebarGroup>\n );\n })}\n </SidebarContent>\n </Sidebar>\n );\n};\n\nexport default SideBar;\n"
514
+ "content": "import React from \"react\";\r\nimport { Link, useLocation } from \"react-router-dom\";\r\nimport { cn } from \"@/lib/utils/cn\";\r\nimport {\r\n Sidebar,\r\n SidebarHeader,\r\n SidebarContent,\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarMenuButton,\r\n SidebarMenuCollapsible,\r\n SidebarMenuSubItem,\r\n useSidebar,\r\n} from \"@/components/ui/sidebar/Sidebar\";\r\nimport { routesConfig } from \"../constants/route.constants\";\r\n\r\nconst SideBar = () => {\r\n const { pathname } = useLocation();\r\n const { state } = useSidebar();\r\n const isCollapsed = state === \"collapsed\";\r\n return (\r\n <Sidebar collapsible=\"icon\" variant=\"sidebar\" side=\"left\" data-lenis-prevent>\r\n <SidebarHeader>\r\n <Link to=\"/blocks\">\r\n <div\r\n className={cn(\r\n \"flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200\",\r\n isCollapsed && \"justify-center\",\r\n )}\r\n >\r\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm flex-none\">\r\n <span className=\"text-primary-foreground font-bold text-sm select-none\">\r\n UI\r\n </span>\r\n </div>\r\n {!isCollapsed && (\r\n <div className=\"overflow-hidden min-w-0\">\r\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">\r\n UI Library\r\n </p>\r\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">\r\n Component Showcase\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n </Link>\r\n </SidebarHeader>\r\n\r\n <SidebarContent>\r\n {routesConfig.map((route, idx) => {\r\n const isChildActive = route.items.some(\r\n (item) => pathname === item.href,\r\n );\r\n const shouldDefaultOpen = isChildActive || route.defaultOpen === true;\r\n\r\n return (\r\n <SidebarGroup key={idx}>\r\n {route.collapsible ? (\r\n <>\r\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuCollapsible\r\n id={route.title}\r\n icon={\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {route.icon}\r\n </span>\r\n }\r\n label={route.title}\r\n defaultOpen={shouldDefaultOpen}\r\n isChildActive={isChildActive}\r\n >\r\n {route.items.map((item, i) => {\r\n const isActive = pathname === item.href;\r\n return (\r\n <SidebarMenuSubItem key={i}>\r\n <Link to={item.href}>\r\n <SidebarMenuButton\r\n isActive={isActive}\r\n tooltip={item.title}\r\n size=\"sm\"\r\n className={cn(\r\n isActive &&\r\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\r\n )}\r\n >\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {item.icon}\r\n </span>\r\n <span className=\"truncate\">{item.title}</span>\r\n </SidebarMenuButton>\r\n </Link>\r\n </SidebarMenuSubItem>\r\n );\r\n })}\r\n </SidebarMenuCollapsible>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </>\r\n ) : (\r\n <>\r\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n {route.items.map((item, i) => {\r\n const isActive = pathname === item.href;\r\n return (\r\n <SidebarMenuItem key={i}>\r\n <Link to={item.href}>\r\n <SidebarMenuButton\r\n isActive={isActive}\r\n tooltip={item.title}\r\n size=\"sm\"\r\n className={cn(\r\n isActive &&\r\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\r\n )}\r\n >\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {item.icon}\r\n </span>\r\n <span className=\"truncate\">{item.title}</span>\r\n </SidebarMenuButton>\r\n </Link>\r\n </SidebarMenuItem>\r\n );\r\n })}\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </>\r\n )}\r\n </SidebarGroup>\r\n );\r\n })}\r\n </SidebarContent>\r\n </Sidebar>\r\n );\r\n};\r\n\r\nexport default SideBar;\r\n"
515
515
  }
516
516
  ]
517
517
  },
@@ -533,7 +533,7 @@
533
533
  "files": [
534
534
  {
535
535
  "path": "src/components/ui/layout-demo3/constants/route.constants.tsx",
536
- "content": "import React from \"react\";\nimport {\n BookOpen,\n ChevronsUpDown,\n FormInput,\n Info,\n LayoutGrid,\n ListChecks,\n Minus,\n ScrollText,\n Settings,\n Square,\n Tag,\n TextCursorInput,\n UserCircle,\n} from \"lucide-react\";\n\ninterface SidebarItem {\n title: string;\n href: string;\n icon?: React.ReactNode;\n badge?: string;\n}\n\ninterface SidebarSection {\n title: string;\n icon?: React.ReactNode;\n collapsible: boolean;\n defaultOpen?: boolean;\n items: SidebarItem[];\n}\n\nexport const routesConfig: SidebarSection[] = [\n {\n title: \"Tổng quan\",\n icon: <BookOpen className=\"w-4 h-4\" />,\n collapsible: false,\n items: [\n {\n title: \"Giới thiệu\",\n href: \"/blocks\",\n icon: <Info className=\"w-4 h-4\" />,\n },\n {\n title: \"Guideline & Cài đặt CLI\",\n href: \"/#\",\n icon: <Settings className=\"w-4 h-4\" />,\n badge: \"CLI\",\n },\n ],\n },\n {\n title: \"General\",\n icon: <LayoutGrid className=\"w-4 h-4\" />,\n collapsible: true,\n defaultOpen: true,\n items: [\n {\n title: \"Button\",\n href: \"/#\",\n icon: <Square className=\"w-4 h-4\" />,\n },\n {\n title: \"Badge\",\n href: \"/#\",\n icon: <Tag className=\"w-4 h-4\" />,\n },\n {\n title: \"Avatar\",\n href: \"/#\",\n icon: <UserCircle className=\"w-4 h-4\" />,\n },\n {\n title: \"Separator\",\n href: \"/#\",\n icon: <Minus className=\"w-4 h-4\" />,\n },\n ],\n },\n {\n title: \"Forms\",\n icon: <FormInput className=\"w-4 h-4\" />,\n collapsible: true,\n defaultOpen: true,\n items: [\n {\n title: \"Input\",\n href: \"/components-page/input\",\n icon: <TextCursorInput className=\"w-4 h-4\" />,\n },\n {\n title: \"Textarea\",\n href: \"/components-page/textarea\",\n icon: <ScrollText className=\"w-4 h-4\" />,\n },\n {\n title: \"Select\",\n href: \"/components-page/select\",\n icon: <ChevronsUpDown className=\"w-4 h-4\" />,\n },\n {\n title: \"Checkbox\",\n href: \"/components-page/checkbox\",\n icon: <ListChecks className=\"w-4 h-4\" />,\n },\n ],\n },\n];\n"
536
+ "content": "import React from \"react\";\r\nimport {\r\n BookOpen,\r\n ChevronsUpDown,\r\n FormInput,\r\n Info,\r\n LayoutGrid,\r\n ListChecks,\r\n Minus,\r\n ScrollText,\r\n Settings,\r\n Square,\r\n Tag,\r\n TextCursorInput,\r\n UserCircle,\r\n} from \"lucide-react\";\r\n\r\ninterface SidebarItem {\r\n title: string;\r\n href: string;\r\n icon?: React.ReactNode;\r\n badge?: string;\r\n}\r\n\r\ninterface SidebarSection {\r\n title: string;\r\n icon?: React.ReactNode;\r\n collapsible: boolean;\r\n defaultOpen?: boolean;\r\n items: SidebarItem[];\r\n}\r\n\r\nexport const routesConfig: SidebarSection[] = [\r\n {\r\n title: \"Tổng quan\",\r\n icon: <BookOpen className=\"w-4 h-4\" />,\r\n collapsible: false,\r\n items: [\r\n {\r\n title: \"Giới thiệu\",\r\n href: \"/blocks\",\r\n icon: <Info className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Guideline & Cài đặt CLI\",\r\n href: \"/#\",\r\n icon: <Settings className=\"w-4 h-4\" />,\r\n badge: \"CLI\",\r\n },\r\n ],\r\n },\r\n {\r\n title: \"General\",\r\n icon: <LayoutGrid className=\"w-4 h-4\" />,\r\n collapsible: true,\r\n defaultOpen: true,\r\n items: [\r\n {\r\n title: \"Button\",\r\n href: \"/#\",\r\n icon: <Square className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Badge\",\r\n href: \"/#\",\r\n icon: <Tag className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Avatar\",\r\n href: \"/#\",\r\n icon: <UserCircle className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Separator\",\r\n href: \"/#\",\r\n icon: <Minus className=\"w-4 h-4\" />,\r\n },\r\n ],\r\n },\r\n {\r\n title: \"Forms\",\r\n icon: <FormInput className=\"w-4 h-4\" />,\r\n collapsible: true,\r\n defaultOpen: true,\r\n items: [\r\n {\r\n title: \"Input\",\r\n href: \"/components-page/input\",\r\n icon: <TextCursorInput className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Textarea\",\r\n href: \"/components-page/textarea\",\r\n icon: <ScrollText className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Select\",\r\n href: \"/components-page/select\",\r\n icon: <ChevronsUpDown className=\"w-4 h-4\" />,\r\n },\r\n {\r\n title: \"Checkbox\",\r\n href: \"/components-page/checkbox\",\r\n icon: <ListChecks className=\"w-4 h-4\" />,\r\n },\r\n ],\r\n },\r\n];\r\n"
537
537
  },
538
538
  {
539
539
  "path": "src/components/ui/layout-demo3/layout.tsx",
@@ -549,11 +549,11 @@
549
549
  },
550
550
  {
551
551
  "path": "src/components/ui/layout-demo3/_layout/Header.tsx",
552
- "content": "import { Button, Code, Separator, SidebarTrigger } from '@/components/ui'\nimport { Autocomplete } from '@/components/ui/autocomplete/Autocomplete'\nimport { Bell, Home, Search, Settings } from 'lucide-react'\nimport React from 'react'\nimport { Link } from 'react-router-dom'\n\nconst Header = () => {\n return (\n <header className=\"h-[60px] sticky top-0 flex items-center gap-3 border-b border-border bg-background/80 backdrop-blur-md px-4 py-2 shrink-0\">\n <SidebarTrigger />\n <Separator orientation=\"vertical\" className=\"h-4\" />\n\n <nav className=\"flex items-center w-full gap-1.5 text-sm min-w-0\">\n <Link\n to=\"/\"\n className=\"text-muted-foreground hover:text-foreground transition-colors flex items-center gap-2 shrink-0\"\n >\n <Home className=\"h-3.5 w-3.5\" /><span className=\"hidden sm:inline\">Home</span>\n </Link>\n\n <Autocomplete\n options={[]}\n placeholder=\"Tìm kiếm component...\"\n leftIcon={<Search className=\"h-4 w-4\" />}\n clearOnSelect\n emptyText=\"Không tìm thấy component\"\n className=\"ml-auto hidden md:flex md:w-[200px] lg:w-[240px]\"\n />\n\n <div className=\"flex items-center gap-1.5 sm:gap-2 ml-auto md:ml-3 shrink-0\">\n <Link to=\"https://github.com\" target=\"_blank\" rel=\"noopener noreferrer\" className=\"hidden sm:block\">\n <Button variant=\"outline\" size=\"icon-sm\" className=\"rounded-lg\">\n <Code className=\"h-4 w-4\" />\n </Button>\n </Link>\n <Button variant=\"outline\" size=\"icon-sm\" className=\"rounded-lg\">\n <Bell className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"icon-sm\" className=\"rounded-lg\">\n <Settings className=\"h-4 w-4\" />\n </Button>\n </div>\n </nav>\n </header>\n )\n}\n\nexport default Header\n"
552
+ "content": "import { Button, Code, Separator, SidebarTrigger } from '@/components/ui'\r\nimport { Autocomplete } from '@/components/ui/autocomplete/Autocomplete'\r\nimport { Bell, Home, Search, Settings } from 'lucide-react'\r\nimport React from 'react'\r\nimport { Link } from 'react-router-dom'\r\n\r\nconst Header = () => {\r\n return (\r\n <header className=\"h-[60px] sticky top-0 flex items-center gap-3 border-b border-border bg-background/80 backdrop-blur-md px-4 py-2 shrink-0\">\r\n <SidebarTrigger />\r\n <Separator orientation=\"vertical\" className=\"h-4\" />\r\n\r\n <nav className=\"flex items-center w-full gap-1.5 text-sm min-w-0\">\r\n <Link\r\n to=\"/\"\r\n className=\"text-muted-foreground hover:text-foreground transition-colors flex items-center gap-2 shrink-0\"\r\n >\r\n <Home className=\"h-3.5 w-3.5\" /><span className=\"hidden sm:inline\">Home</span>\r\n </Link>\r\n\r\n <Autocomplete\r\n options={[]}\r\n placeholder=\"Tìm kiếm component...\"\r\n leftIcon={<Search className=\"h-4 w-4\" />}\r\n clearOnSelect\r\n emptyText=\"Không tìm thấy component\"\r\n className=\"ml-auto hidden md:flex md:w-[200px] lg:w-[240px]\"\r\n />\r\n\r\n <div className=\"flex items-center gap-1.5 sm:gap-2 ml-auto md:ml-3 shrink-0\">\r\n <Link to=\"https://github.com\" target=\"_blank\" rel=\"noopener noreferrer\" className=\"hidden sm:block\">\r\n <Button variant=\"outline\" size=\"icon-sm\" className=\"rounded-lg\">\r\n <Code className=\"h-4 w-4\" />\r\n </Button>\r\n </Link>\r\n <Button variant=\"outline\" size=\"icon-sm\" className=\"rounded-lg\">\r\n <Bell className=\"h-4 w-4\" />\r\n </Button>\r\n <Button variant=\"outline\" size=\"icon-sm\" className=\"rounded-lg\">\r\n <Settings className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n </nav>\r\n </header>\r\n )\r\n}\r\n\r\nexport default Header\r\n"
553
553
  },
554
554
  {
555
555
  "path": "src/components/ui/layout-demo3/_layout/SideBar.tsx",
556
- "content": "import React from \"react\";\nimport { Link, useLocation } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils/cn\";\nimport {\n Sidebar,\n SidebarHeader,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarGroupContent,\n SidebarMenu,\n SidebarMenuItem,\n SidebarMenuButton,\n SidebarMenuCollapsible,\n SidebarMenuSubItem,\n useSidebar,\n} from \"@/components/ui/sidebar/Sidebar\";\nimport { routesConfig } from \"../constants/route.constants\";\n\nconst SideBar = () => {\n const { pathname } = useLocation();\n const { state } = useSidebar();\n const isCollapsed = state === \"collapsed\";\n return (\n <Sidebar collapsible=\"icon\" variant=\"sidebar\" side=\"left\" data-lenis-prevent>\n <SidebarHeader>\n <Link to=\"/blocks\">\n <div\n className={cn(\n \"flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200\",\n isCollapsed && \"justify-center\",\n )}\n >\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm flex-none\">\n <span className=\"text-primary-foreground font-bold text-sm select-none\">\n UI\n </span>\n </div>\n {!isCollapsed && (\n <div className=\"overflow-hidden min-w-0\">\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">\n UI Library\n </p>\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">\n Component Showcase\n </p>\n </div>\n )}\n </div>\n </Link>\n </SidebarHeader>\n\n <SidebarContent>\n {routesConfig.map((route, idx) => {\n const isChildActive = route.items.some(\n (item) => pathname === item.href,\n );\n const shouldDefaultOpen = isChildActive || route.defaultOpen === true;\n\n return (\n <SidebarGroup key={idx}>\n {route.collapsible ? (\n <>\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuCollapsible\n id={route.title}\n icon={\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {route.icon}\n </span>\n }\n label={route.title}\n defaultOpen={shouldDefaultOpen}\n isChildActive={isChildActive}\n >\n {route.items.map((item, i) => {\n const isActive = pathname === item.href;\n return (\n <SidebarMenuSubItem key={i}>\n <Link to={item.href}>\n <SidebarMenuButton\n isActive={isActive}\n tooltip={item.title}\n size=\"sm\"\n className={cn(\n isActive &&\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\n )}\n >\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {item.icon}\n </span>\n <span className=\"truncate\">{item.title}</span>\n </SidebarMenuButton>\n </Link>\n </SidebarMenuSubItem>\n );\n })}\n </SidebarMenuCollapsible>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarGroupContent>\n </>\n ) : (\n <>\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\n <SidebarGroupContent>\n <SidebarMenu>\n {route.items.map((item, i) => {\n const isActive = pathname === item.href;\n return (\n <SidebarMenuItem key={i}>\n <Link to={item.href}>\n <SidebarMenuButton\n isActive={isActive}\n tooltip={item.title}\n size=\"sm\"\n className={cn(\n isActive &&\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\n )}\n >\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\n {item.icon}\n </span>\n <span className=\"truncate\">{item.title}</span>\n </SidebarMenuButton>\n </Link>\n </SidebarMenuItem>\n );\n })}\n </SidebarMenu>\n </SidebarGroupContent>\n </>\n )}\n </SidebarGroup>\n );\n })}\n </SidebarContent>\n </Sidebar>\n );\n};\n\nexport default SideBar;\n"
556
+ "content": "import React from \"react\";\r\nimport { Link, useLocation } from \"react-router-dom\";\r\nimport { cn } from \"@/lib/utils/cn\";\r\nimport {\r\n Sidebar,\r\n SidebarHeader,\r\n SidebarContent,\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarMenuButton,\r\n SidebarMenuCollapsible,\r\n SidebarMenuSubItem,\r\n useSidebar,\r\n} from \"@/components/ui/sidebar/Sidebar\";\r\nimport { routesConfig } from \"../constants/route.constants\";\r\n\r\nconst SideBar = () => {\r\n const { pathname } = useLocation();\r\n const { state } = useSidebar();\r\n const isCollapsed = state === \"collapsed\";\r\n return (\r\n <Sidebar collapsible=\"icon\" variant=\"sidebar\" side=\"left\" data-lenis-prevent>\r\n <SidebarHeader>\r\n <Link to=\"/blocks\">\r\n <div\r\n className={cn(\r\n \"flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200\",\r\n isCollapsed && \"justify-center\",\r\n )}\r\n >\r\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm flex-none\">\r\n <span className=\"text-primary-foreground font-bold text-sm select-none\">\r\n UI\r\n </span>\r\n </div>\r\n {!isCollapsed && (\r\n <div className=\"overflow-hidden min-w-0\">\r\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">\r\n UI Library\r\n </p>\r\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">\r\n Component Showcase\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n </Link>\r\n </SidebarHeader>\r\n\r\n <SidebarContent>\r\n {routesConfig.map((route, idx) => {\r\n const isChildActive = route.items.some(\r\n (item) => pathname === item.href,\r\n );\r\n const shouldDefaultOpen = isChildActive || route.defaultOpen === true;\r\n\r\n return (\r\n <SidebarGroup key={idx}>\r\n {route.collapsible ? (\r\n <>\r\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <SidebarMenuCollapsible\r\n id={route.title}\r\n icon={\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {route.icon}\r\n </span>\r\n }\r\n label={route.title}\r\n defaultOpen={shouldDefaultOpen}\r\n isChildActive={isChildActive}\r\n >\r\n {route.items.map((item, i) => {\r\n const isActive = pathname === item.href;\r\n return (\r\n <SidebarMenuSubItem key={i}>\r\n <Link to={item.href}>\r\n <SidebarMenuButton\r\n isActive={isActive}\r\n tooltip={item.title}\r\n size=\"sm\"\r\n className={cn(\r\n isActive &&\r\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\r\n )}\r\n >\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {item.icon}\r\n </span>\r\n <span className=\"truncate\">{item.title}</span>\r\n </SidebarMenuButton>\r\n </Link>\r\n </SidebarMenuSubItem>\r\n );\r\n })}\r\n </SidebarMenuCollapsible>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </>\r\n ) : (\r\n <>\r\n <SidebarGroupLabel>{route.title}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n {route.items.map((item, i) => {\r\n const isActive = pathname === item.href;\r\n return (\r\n <SidebarMenuItem key={i}>\r\n <Link to={item.href}>\r\n <SidebarMenuButton\r\n isActive={isActive}\r\n tooltip={item.title}\r\n size=\"sm\"\r\n className={cn(\r\n isActive &&\r\n \"bg-sidebar-accent text-sidebar-accent-foreground font-semibold\",\r\n )}\r\n >\r\n <span className=\"shrink-0 w-4 h-4 flex items-center justify-center\">\r\n {item.icon}\r\n </span>\r\n <span className=\"truncate\">{item.title}</span>\r\n </SidebarMenuButton>\r\n </Link>\r\n </SidebarMenuItem>\r\n );\r\n })}\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </>\r\n )}\r\n </SidebarGroup>\r\n );\r\n })}\r\n </SidebarContent>\r\n </Sidebar>\r\n );\r\n};\r\n\r\nexport default SideBar;\r\n"
557
557
  }
558
558
  ]
559
559
  },
@@ -722,7 +722,7 @@
722
722
  "files": [
723
723
  {
724
724
  "path": "src/components/ui/popover/Popover.tsx",
725
- "content": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst popoverVariants = tv({\r\n slots: {\r\n popup: 'z-50 w-72 rounded-md border border-border bg-background p-4 text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-ending:animate-out data-ending:fade-out-0 data-ending:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n arrow: 'fill-popover stroke-border stroke-[1px]',\r\n },\r\n});\r\n\r\nconst { popup, arrow } = popoverVariants();\r\n\r\n// ─── Compound Components ─────────────────────────────────────────────────────\r\n\r\nconst Popover = BasePopover.Root;\r\n\r\nconst PopoverTrigger = React.forwardRef<\r\n HTMLButtonElement,\r\n React.ComponentPropsWithoutRef<typeof BasePopover.Trigger>\r\n>(({ children, render: renderProp, ...props }, ref) => {\r\n // Wrap pattern: <PopoverTrigger><Button /></PopoverTrigger>\r\n // → forward the element as `render` so Base UI merges trigger props into it (no nested <button>)\r\n const isElement = React.isValidElement(children);\r\n return (\r\n <BasePopover.Trigger\r\n ref={ref}\r\n render={renderProp ?? (isElement ? (children as React.ReactElement) : undefined)}\r\n {...props}\r\n >\r\n {isElement ? undefined : children}\r\n </BasePopover.Trigger>\r\n );\r\n});\r\nPopoverTrigger.displayName = 'PopoverTrigger';\r\n\r\nexport interface PopoverContentProps\r\n extends React.ComponentPropsWithoutRef<typeof BasePopover.Popup> {\r\n /** Side offset from the trigger (default: 4) */\r\n sideOffset?: number;\r\n /** Side to display the popover (default: 'bottom') */\r\n side?: 'top' | 'right' | 'bottom' | 'left';\r\n /** Alignment relative to the trigger (default: 'center') */\r\n align?: 'start' | 'center' | 'end';\r\n /** Show the arrow indicator */\r\n showArrow?: boolean;\r\n}\r\n\r\nconst PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(\r\n ({ className, sideOffset = 4, side = 'bottom', align = 'center', showArrow = true, children, ...props }, ref) => (\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner sideOffset={sideOffset} side={side} align={align}>\r\n <BasePopover.Popup ref={ref} className={cn(popup(), className)} {...props}>\r\n {showArrow && <BasePopover.Arrow className={arrow()} />}\r\n {children}\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n )\r\n);\r\nPopoverContent.displayName = 'PopoverContent';\r\n\r\nconst PopoverClose = BasePopover.Close;\r\n\r\nexport { Popover, PopoverTrigger, PopoverContent, PopoverClose };\r\n"
725
+ "content": "'use client';\n\nimport * as React from 'react';\nimport { Popover as BasePopover } from '@base-ui/react';\nimport { tv } from 'tailwind-variants';\nimport { cn } from '@/lib/utils/cn';\n\nconst popoverVariants = tv({\n slots: {\n popup: 'z-50 w-72 rounded-md border border-border bg-background p-4 text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-ending:animate-out data-ending:fade-out-0 data-ending:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\n arrow: 'fill-popover stroke-border stroke-[1px]',\n },\n});\n\nconst { popup, arrow } = popoverVariants();\n\n// ─── Compound Components ─────────────────────────────────────────────────────\n\nconst Popover = BasePopover.Root;\n\nconst PopoverTrigger = React.forwardRef<\n HTMLButtonElement,\n React.ComponentPropsWithoutRef<typeof BasePopover.Trigger>\n>(({ children, render: renderProp, ...props }, ref) => {\n // Wrap pattern: <PopoverTrigger><Button /></PopoverTrigger>\n // → forward the element as `render` so Base UI merges trigger props into it (no nested <button>)\n const isElement = React.isValidElement(children);\n return (\n <BasePopover.Trigger\n ref={ref}\n render={renderProp ?? (isElement ? (children as React.ReactElement) : undefined)}\n {...props}\n >\n {isElement ? undefined : children}\n </BasePopover.Trigger>\n );\n});\nPopoverTrigger.displayName = 'PopoverTrigger';\n\nexport interface PopoverContentProps\n extends React.ComponentPropsWithoutRef<typeof BasePopover.Popup> {\n /** Side offset from the trigger (default: 4) */\n sideOffset?: number;\n /** Side to display the popover (default: 'bottom') */\n side?: 'top' | 'right' | 'bottom' | 'left';\n /** Alignment relative to the trigger (default: 'center') */\n align?: 'start' | 'center' | 'end';\n /** Show the arrow indicator */\n showArrow?: boolean;\n}\n\nconst PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(\n ({ className, sideOffset = 4, side = 'bottom', align = 'center', showArrow = true, children, ...props }, ref) => (\n <BasePopover.Portal>\n <BasePopover.Positioner sideOffset={sideOffset} side={side} align={align}>\n <BasePopover.Popup ref={ref} className={cn(popup(), className)} {...props}>\n {showArrow && <BasePopover.Arrow className={arrow()} />}\n {children}\n </BasePopover.Popup>\n </BasePopover.Positioner>\n </BasePopover.Portal>\n )\n);\nPopoverContent.displayName = 'PopoverContent';\n\nconst PopoverClose = BasePopover.Close;\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverClose };\n"
726
726
  }
727
727
  ]
728
728
  },
@@ -739,7 +739,7 @@
739
739
  "files": [
740
740
  {
741
741
  "path": "src/components/ui/pretty-code/PrettyCode.tsx",
742
- "content": "import React, { useState, useEffect, useCallback } from 'react';\r\nimport { createHighlighter, type Highlighter } from 'shiki';\r\nimport { unified } from 'unified';\r\nimport rehypeParse from 'rehype-parse';\r\nimport rehypeReact from 'rehype-react';\r\nimport * as prod from 'react/jsx-runtime';\r\nimport { Copy, Check } from 'lucide-react';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Singleton highlighter ────────────────────────────────────────────────────\r\nlet globalHighlighter: Highlighter | null = null;\r\n\r\nconst getHighlighter = async () => {\r\n if (globalHighlighter) return globalHighlighter;\r\n globalHighlighter = await createHighlighter({\r\n themes: ['nord'],\r\n langs: ['tsx', 'typescript', 'javascript', 'bash', 'json'],\r\n });\r\n return globalHighlighter;\r\n};\r\n\r\n// ─── Helpers ──────────────────────────────────────────────────────────────────\r\nconst LANG_LABELS: Record<string, string> = {\r\n tsx: 'TSX',\r\n typescript: 'TypeScript',\r\n javascript: 'JavaScript',\r\n bash: 'Bash',\r\n json: 'JSON',\r\n};\r\n\r\n// Fixed widths for loading skeleton rows\r\nconst SKELETON_WIDTHS = ['68%', '82%', '54%', '76%', '45%', '60%'];\r\n\r\n// ─── Types ────────────────────────────────────────────────────────────────────\r\ninterface PrettyCodeProps {\r\n code: string;\r\n lang?: string;\r\n /** Optional filename shown in the header bar */\r\n filename?: string;\r\n className?: string;\r\n}\r\n\r\n// ─── Component ────────────────────────────────────────────────────────────────\r\nexport const PrettyCode: React.FC<PrettyCodeProps> = ({\r\n code,\r\n lang = 'tsx',\r\n filename,\r\n className,\r\n}) => {\r\n const [nodes, setNodes] = useState<React.ReactNode>(null);\r\n const [loading, setLoading] = useState(true);\r\n const [copied, setCopied] = useState(false);\r\n\r\n useEffect(() => {\r\n let isMounted = true;\r\n setLoading(true);\r\n setNodes(null);\r\n\r\n const highlight = async () => {\r\n try {\r\n const highlighter = await getHighlighter();\r\n const html = highlighter.codeToHtml(code, { lang, theme: 'nord' });\r\n\r\n const file = await unified()\r\n .use(rehypeParse, { fragment: true })\r\n .use(rehypeReact, { ...prod })\r\n .process(html);\r\n\r\n if (isMounted) {\r\n setNodes(file.result as React.ReactNode);\r\n setLoading(false);\r\n }\r\n } catch (err) {\r\n console.error('Failed to highlight code:', err);\r\n if (isMounted) setLoading(false);\r\n }\r\n };\r\n\r\n highlight();\r\n return () => { isMounted = false; };\r\n }, [code, lang]);\r\n\r\n const handleCopy = useCallback(async () => {\r\n try {\r\n await navigator.clipboard.writeText(code);\r\n setCopied(true);\r\n setTimeout(() => setCopied(false), 2000);\r\n } catch {\r\n // clipboard not available\r\n }\r\n }, [code]);\r\n\r\n const langLabel = LANG_LABELS[lang] ?? lang.toUpperCase();\r\n\r\n return (\r\n <div className={cn('rounded-xl overflow-hidden border border-white/[0.07] bg-[#2e3440] shadow-2xl', className)}>\r\n\r\n {/* ── Header bar ── */}\r\n <div className=\"flex items-center justify-between px-4 py-2.5 bg-[#252b37] border-b border-white/[0.07] select-none\">\r\n\r\n {/* Left: window dots + filename */}\r\n <div className=\"flex items-center gap-3 min-w-0\">\r\n <div className=\"flex items-center gap-1.5 shrink-0\">\r\n <span className=\"w-3 h-3 rounded-full bg-[#ff5f57]\" />\r\n <span className=\"w-3 h-3 rounded-full bg-[#febc2e]\" />\r\n <span className=\"w-3 h-3 rounded-full bg-[#28c840]\" />\r\n </div>\r\n {filename && (\r\n <span className=\"text-[11px] text-zinc-400 font-mono truncate leading-none\">\r\n {filename}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {/* Right: language badge + copy button */}\r\n <div className=\"flex items-center gap-2 shrink-0 ml-4\">\r\n <span className=\"hidden sm:block text-[10px] font-bold tracking-widest text-zinc-600 uppercase\">\r\n {langLabel}\r\n </span>\r\n\r\n <button\r\n onClick={handleCopy}\r\n aria-label=\"Copy code\"\r\n className={cn(\r\n 'flex items-center gap-1.5 px-2 py-1 rounded-md',\r\n 'text-xs font-medium transition-all duration-150 active:scale-95',\r\n copied\r\n ? 'text-emerald-400 bg-emerald-400/10'\r\n : 'text-zinc-400 hover:text-zinc-100 hover:bg-white/10',\r\n )}\r\n >\r\n {copied\r\n ? <Check className=\"w-3.5 h-3.5 shrink-0\" />\r\n : <Copy className=\"w-3.5 h-3.5 shrink-0\" />\r\n }\r\n <span className=\"hidden sm:inline w-[42px]\">\r\n {copied ? 'Copied!' : 'Copy'}\r\n </span>\r\n </button>\r\n </div>\r\n </div>\r\n\r\n {/* ── Code area ── */}\r\n {loading ? (\r\n <div className=\"p-5 space-y-3 animate-pulse\" aria-hidden>\r\n {SKELETON_WIDTHS.map((w, i) => (\r\n <div\r\n key={i}\r\n className=\"h-3.5 rounded-full bg-white/[0.08]\"\r\n style={{ width: w }}\r\n />\r\n ))}\r\n </div>\r\n ) : (\r\n <div\r\n className=\"overflow-x-auto\r\n [&_pre]:!bg-transparent [&_pre]:m-0\r\n [&_pre]:px-5 [&_pre]:py-4\r\n [&_pre]:text-[13px] [&_pre]:leading-[1.7]\r\n [&_code]:font-mono [&_code]:text-[13px]\"\r\n >\r\n {nodes}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n};\r\n"
742
+ "content": "import React, { useState, useEffect, useCallback } from 'react';\nimport { createHighlighter, type Highlighter } from 'shiki';\nimport { unified } from 'unified';\nimport rehypeParse from 'rehype-parse';\nimport rehypeReact from 'rehype-react';\nimport * as prod from 'react/jsx-runtime';\nimport { Copy, Check } from 'lucide-react';\nimport { cn } from '@/lib/utils/cn';\n\n// ─── Singleton highlighter ────────────────────────────────────────────────────\nlet globalHighlighter: Highlighter | null = null;\n\nconst getHighlighter = async () => {\n if (globalHighlighter) return globalHighlighter;\n globalHighlighter = await createHighlighter({\n themes: ['nord'],\n langs: ['tsx', 'typescript', 'javascript', 'bash', 'json'],\n });\n return globalHighlighter;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\nconst LANG_LABELS: Record<string, string> = {\n tsx: 'TSX',\n typescript: 'TypeScript',\n javascript: 'JavaScript',\n bash: 'Bash',\n json: 'JSON',\n};\n\n// Fixed widths for loading skeleton rows\nconst SKELETON_WIDTHS = ['68%', '82%', '54%', '76%', '45%', '60%'];\n\n// ─── Types ────────────────────────────────────────────────────────────────────\ninterface PrettyCodeProps {\n code: string;\n lang?: string;\n /** Optional filename shown in the header bar */\n filename?: string;\n className?: string;\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\nexport const PrettyCode: React.FC<PrettyCodeProps> = ({\n code,\n lang = 'tsx',\n filename,\n className,\n}) => {\n const [nodes, setNodes] = useState<React.ReactNode>(null);\n const [loading, setLoading] = useState(true);\n const [copied, setCopied] = useState(false);\n\n useEffect(() => {\n let isMounted = true;\n setLoading(true);\n setNodes(null);\n\n const highlight = async () => {\n try {\n const highlighter = await getHighlighter();\n const html = highlighter.codeToHtml(code, { lang, theme: 'nord' });\n\n const file = await unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeReact, { ...prod })\n .process(html);\n\n if (isMounted) {\n setNodes(file.result as React.ReactNode);\n setLoading(false);\n }\n } catch (err) {\n console.error('Failed to highlight code:', err);\n if (isMounted) setLoading(false);\n }\n };\n\n highlight();\n return () => { isMounted = false; };\n }, [code, lang]);\n\n const handleCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch {\n // clipboard not available\n }\n }, [code]);\n\n const langLabel = LANG_LABELS[lang] ?? lang.toUpperCase();\n\n return (\n <div className={cn('rounded-xl overflow-hidden border border-white/[0.07] bg-[#2e3440] shadow-2xl', className)}>\n\n {/* ── Header bar ── */}\n <div className=\"flex items-center justify-between px-4 py-2.5 bg-[#252b37] border-b border-white/[0.07] select-none\">\n\n {/* Left: window dots + filename */}\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"flex items-center gap-1.5 shrink-0\">\n <span className=\"w-3 h-3 rounded-full bg-[#ff5f57]\" />\n <span className=\"w-3 h-3 rounded-full bg-[#febc2e]\" />\n <span className=\"w-3 h-3 rounded-full bg-[#28c840]\" />\n </div>\n {filename && (\n <span className=\"text-[11px] text-zinc-400 font-mono truncate leading-none\">\n {filename}\n </span>\n )}\n </div>\n\n {/* Right: language badge + copy button */}\n <div className=\"flex items-center gap-2 shrink-0 ml-4\">\n <span className=\"hidden sm:block text-[10px] font-bold tracking-widest text-zinc-600 uppercase\">\n {langLabel}\n </span>\n\n <button\n onClick={handleCopy}\n aria-label=\"Copy code\"\n className={cn(\n 'flex items-center gap-1.5 px-2 py-1 rounded-md',\n 'text-xs font-medium transition-all duration-150 active:scale-95',\n copied\n ? 'text-emerald-400 bg-emerald-400/10'\n : 'text-zinc-400 hover:text-zinc-100 hover:bg-white/10',\n )}\n >\n {copied\n ? <Check className=\"w-3.5 h-3.5 shrink-0\" />\n : <Copy className=\"w-3.5 h-3.5 shrink-0\" />\n }\n <span className=\"hidden sm:inline w-[42px]\">\n {copied ? 'Copied!' : 'Copy'}\n </span>\n </button>\n </div>\n </div>\n\n {/* ── Code area ── */}\n {loading ? (\n <div className=\"p-5 space-y-3 animate-pulse\" aria-hidden>\n {SKELETON_WIDTHS.map((w, i) => (\n <div\n key={i}\n className=\"h-3.5 rounded-full bg-white/[0.08]\"\n style={{ width: w }}\n />\n ))}\n </div>\n ) : (\n <div\n className=\"overflow-x-auto\n [&_pre]:!bg-transparent [&_pre]:m-0\n [&_pre]:px-5 [&_pre]:py-4\n [&_pre]:text-[13px] [&_pre]:leading-[1.7]\n [&_code]:font-mono [&_code]:text-[13px]\"\n >\n {nodes}\n </div>\n )}\n </div>\n );\n};\n"
743
743
  }
744
744
  ]
745
745
  },
@@ -783,7 +783,7 @@
783
783
  "files": [
784
784
  {
785
785
  "path": "src/components/ui/radio/Radio.tsx",
786
- "content": "import * as React from 'react';\r\nimport { Radio as BaseRadio } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst radioVariants = tv({\r\n slots: {\r\n root: 'group flex shrink-0 items-center justify-center rounded-full border border-border bg-background transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:border-primary data-checked:border-primary',\r\n indicator: 'flex items-center justify-center',\r\n dot: 'rounded-full bg-primary',\r\n card: 'group/card relative flex flex-row items-start gap-4 cursor-pointer rounded-xl border border-border bg-card p-4 w-full shadow-sm outline-none transition-all hover:bg-accent/50 hover:text-accent-foreground data-[checked]:border-primary data-[checked]:bg-primary/5 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 overflow-hidden',\r\n cardCircle: 'flex shrink-0 items-center justify-center rounded-full border border-border bg-background transition-all group-data-[checked]/card:border-primary group-data-[checked]/card:text-primary mt-0.5',\r\n },\r\n variants: {\r\n size: {\r\n sm: { root: 'h-4 w-4', cardCircle: 'h-4 w-4', dot: 'h-1.5 w-1.5' },\r\n md: { root: 'h-5 w-5', cardCircle: 'h-5 w-5', dot: 'h-2 w-2' },\r\n lg: { root: 'h-6 w-6', cardCircle: 'h-6 w-6', dot: 'h-2.5 w-2.5' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface RadioProps\r\n extends Omit<BaseRadio.Root.Props, 'className'>,\r\n VariantProps<typeof radioVariants> {\r\n variant?: 'default' | 'card';\r\n label?: string;\r\n className?: string;\r\n /** Hiện/ẩn indicator (circle). Card variant mặc định ẩn. */\r\n showIndicator?: boolean;\r\n}\r\n\r\nconst Radio = React.forwardRef<React.ElementRef<typeof BaseRadio.Root>, RadioProps>(\r\n ({ variant = 'default', className, size, label, id, children, showIndicator, ...props }, ref) => {\r\n const defaultId = React.useId();\r\n const radioId = id || defaultId;\r\n\r\n const { root, indicator, dot, card, cardCircle } = radioVariants({ size });\r\n\r\n if (variant === 'card') {\r\n return (\r\n <BaseRadio.Root\r\n ref={ref}\r\n id={radioId}\r\n className={card({ className })}\r\n {...props}\r\n >\r\n {showIndicator && (\r\n <div className={cardCircle()}>\r\n <BaseRadio.Indicator className={indicator()}>\r\n <div className={dot()} />\r\n </BaseRadio.Indicator>\r\n </div>\r\n )}\r\n {children}\r\n </BaseRadio.Root>\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"flex items-center gap-2 w-fit\">\r\n <BaseRadio.Root\r\n ref={ref}\r\n id={radioId}\r\n className={root({ className })}\r\n {...props}\r\n >\r\n <BaseRadio.Indicator className={indicator()}>\r\n <div className={dot()} />\r\n </BaseRadio.Indicator>\r\n </BaseRadio.Root>\r\n {children}\r\n {label && (\r\n <label\r\n htmlFor={radioId}\r\n className=\"text-sm font-medium leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\r\n >\r\n {label}\r\n </label>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nRadio.displayName = 'Radio';\r\n\r\nexport { Radio };\r\n"
786
+ "content": "import * as React from 'react';\r\nimport { Radio as BaseRadio } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst radioVariants = tv({\r\n slots: {\r\n root: 'group flex shrink-0 items-center justify-center rounded-full border border-border bg-background transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:border-primary data-checked:border-primary',\r\n indicator: 'flex items-center justify-center',\r\n dot: 'rounded-full bg-primary flex items-center justify-center',\r\n card: 'group/card relative flex flex-row items-start gap-4 cursor-pointer rounded-xl border border-border bg-card p-4 w-full shadow-sm outline-none transition-all hover:bg-accent/50 hover:text-accent-foreground data-[checked]:border-primary data-[checked]:bg-primary/5 focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 overflow-hidden',\r\n cardCircle: 'flex shrink-0 items-center justify-center rounded-full border border-border bg-background transition-all group-data-[checked]/card:border-primary group-data-[checked]/card:text-primary mt-0.5',\r\n },\r\n variants: {\r\n size: {\r\n sm: { root: 'h-4 w-4', cardCircle: 'h-4 w-4', dot: 'h-2.5 w-2.5' },\r\n md: { root: 'h-5 w-5', cardCircle: 'h-5 w-5', dot: 'h-3 w-3' },\r\n lg: { root: 'h-6 w-6', cardCircle: 'h-6 w-6', dot: 'h-3.5 w-3.5' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface RadioProps\r\n extends Omit<BaseRadio.Root.Props, 'className'>,\r\n VariantProps<typeof radioVariants> {\r\n variant?: 'default' | 'card';\r\n label?: string;\r\n className?: string;\r\n /** Hiện/ẩn indicator (circle). Card variant mặc định ẩn. */\r\n showIndicator?: boolean;\r\n}\r\n\r\nconst Radio = React.forwardRef<React.ElementRef<typeof BaseRadio.Root>, RadioProps>(\r\n ({ variant = 'default', className, size, label, id, children, showIndicator, ...props }, ref) => {\r\n const defaultId = React.useId();\r\n const radioId = id || defaultId;\r\n\r\n const { root, indicator, dot, card, cardCircle } = radioVariants({ size });\r\n\r\n if (variant === 'card') {\r\n return (\r\n <BaseRadio.Root\r\n ref={ref}\r\n id={radioId}\r\n className={card({ className })}\r\n {...props}\r\n >\r\n {showIndicator && (\r\n <div className={cardCircle()}>\r\n <BaseRadio.Indicator className={indicator()}>\r\n <div className={dot()} />\r\n </BaseRadio.Indicator>\r\n </div>\r\n )}\r\n {children}\r\n </BaseRadio.Root>\r\n );\r\n }\r\n\r\n return (\r\n <div className=\"flex items-center gap-2 w-fit\">\r\n <BaseRadio.Root\r\n ref={ref}\r\n id={radioId}\r\n className={root({ className })}\r\n {...props}\r\n >\r\n <BaseRadio.Indicator className={indicator()}>\r\n <div className={dot()} />\r\n </BaseRadio.Indicator>\r\n </BaseRadio.Root>\r\n {children}\r\n {label && (\r\n <label\r\n htmlFor={radioId}\r\n className=\"text-sm font-medium leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\r\n >\r\n {label}\r\n </label>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nRadio.displayName = 'Radio';\r\n\r\nexport { Radio };\r\n"
787
787
  }
788
788
  ]
789
789
  },
@@ -797,7 +797,7 @@
797
797
  "files": [
798
798
  {
799
799
  "path": "src/components/ui/radio-group/RadioGroup.tsx",
800
- "content": "import * as React from 'react';\r\nimport { RadioGroup as BaseRadioGroup } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst radioGroupVariants = tv({\r\n base: 'grid gap-2',\r\n variants: {\r\n orientation: {\r\n vertical: 'grid-flow-row',\r\n horizontal: 'grid-flow-col auto-cols-auto',\r\n },\r\n },\r\n defaultVariants: {\r\n orientation: 'vertical',\r\n },\r\n});\r\n\r\n/** Props for the RadioGroup component */\r\nexport interface RadioGroupProps\r\n extends React.ComponentPropsWithoutRef<typeof BaseRadioGroup>,\r\n VariantProps<typeof radioGroupVariants> {\r\n className?: string;\r\n}\r\n\r\nconst RadioGroup = React.forwardRef<React.ElementRef<typeof BaseRadioGroup>, RadioGroupProps>(\r\n ({ className, orientation, ...props }, ref) => {\r\n return (\r\n <BaseRadioGroup\r\n ref={ref}\r\n className={radioGroupVariants({ orientation, className })}\r\n aria-orientation={orientation ?? 'vertical'}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\n\r\nRadioGroup.displayName = 'RadioGroup';\r\n\r\nexport { RadioGroup, radioGroupVariants };\r\n"
800
+ "content": "import * as React from 'react';\r\nimport { RadioGroup as BaseRadioGroup } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst radioGroupVariants = tv({\r\n base: 'grid gap-2',\r\n variants: {\r\n orientation: {\r\n vertical: 'grid-flow-row',\r\n horizontal: 'grid-flow-col auto-cols-auto',\r\n },\r\n },\r\n defaultVariants: {\r\n orientation: 'vertical',\r\n },\r\n});\r\n\r\n/** Props for the RadioGroup component */\r\nexport interface RadioGroupProps\r\n extends React.ComponentPropsWithoutRef<typeof BaseRadioGroup>,\r\n VariantProps<typeof radioGroupVariants> {\r\n className?: string;\r\n}\r\n\r\nconst RadioGroup = React.forwardRef<React.ElementRef<typeof BaseRadioGroup>, RadioGroupProps>(\r\n ({ className, orientation, ...props }, ref) => {\r\n return (\r\n <BaseRadioGroup\r\n ref={ref}\r\n className={radioGroupVariants({ orientation, className })}\r\n aria-orientation={orientation ?? 'vertical'}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\n\r\nRadioGroup.displayName = 'RadioGroup';\r\n\r\nexport { RadioGroup };\r\n"
801
801
  }
802
802
  ]
803
803
  },
@@ -811,7 +811,7 @@
811
811
  "files": [
812
812
  {
813
813
  "path": "src/components/ui/rate/Rate.tsx",
814
- "content": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Star } from 'lucide-react';\r\n\r\nconst rateVariants = tv({\r\n slots: {\r\n root: 'inline-flex items-center gap-0.5',\r\n star: 'relative cursor-pointer transition-transform duration-100 hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-sm',\r\n starIcon: 'transition-colors duration-150',\r\n },\r\n variants: {\r\n size: {\r\n sm: { star: 'w-4 h-4', starIcon: 'w-4 h-4' },\r\n md: { star: 'w-6 h-6', starIcon: 'w-6 h-6' },\r\n lg: { star: 'w-8 h-8', starIcon: 'w-8 h-8' },\r\n xl: { star: 'w-10 h-10', starIcon: 'w-10 h-10' },\r\n },\r\n readonly: {\r\n true: { star: 'cursor-default hover:scale-100' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\n/** Props for the Rate component */\r\nexport interface RateProps extends VariantProps<typeof rateVariants> {\r\n /** Controlled rating value */\r\n value?: number;\r\n /** Default rating value (uncontrolled) */\r\n defaultValue?: number;\r\n /** Callback fired when the rating changes */\r\n onChange?: (value: number) => void;\r\n /** Total number of stars */\r\n count?: number;\r\n /** Allow half-star precision */\r\n allowHalf?: boolean;\r\n /** Allow clicking the current value to reset to 0 */\r\n allowClear?: boolean;\r\n /** Display as read-only (no interaction) */\r\n readonly?: boolean;\r\n /** Disable the rating component */\r\n disabled?: boolean;\r\n /** Custom element to render instead of the default star icon */\r\n character?: React.ReactNode;\r\n /** Tailwind text color class for filled stars */\r\n activeColor?: string;\r\n /** Tailwind text color class for empty stars */\r\n inactiveColor?: string;\r\n className?: string;\r\n /** Accessible label for the rating group */\r\n 'aria-label'?: string;\r\n /**\r\n * Custom label for individual stars.\r\n * Receives the star index (1-based) and returns a string.\r\n * Defaults to \"1 star\", \"2 stars\", etc.\r\n */\r\n getStarLabel?: (index: number) => string;\r\n}\r\n\r\nconst Rate = React.forwardRef<HTMLDivElement, RateProps>(({\r\n value: controlledValue,\r\n defaultValue = 0,\r\n onChange,\r\n count = 5,\r\n allowHalf = false,\r\n allowClear = true,\r\n readonly = false,\r\n disabled = false,\r\n character,\r\n activeColor = 'text-amber-400',\r\n inactiveColor = 'text-muted-foreground/30',\r\n size,\r\n className,\r\n 'aria-label': ariaLabel,\r\n getStarLabel,\r\n}, ref) => {\r\n const isControlled = controlledValue !== undefined;\r\n const [internalValue, setInternalValue] = React.useState(defaultValue);\r\n const [hoverValue, setHoverValue] = React.useState<number | null>(null);\r\n\r\n const value = isControlled ? controlledValue! : internalValue;\r\n\r\n const handleChange = (newVal: number) => {\r\n if (readonly || disabled) return;\r\n const next = allowClear && newVal === value ? 0 : newVal;\r\n if (!isControlled) setInternalValue(next);\r\n onChange?.(next);\r\n };\r\n\r\n const defaultGetStarLabel = (index: number) =>\r\n index === 1 ? '1 star' : `${index} stars`;\r\n\r\n const resolveLabel = getStarLabel ?? defaultGetStarLabel;\r\n\r\n const getStarFraction = (starIndex: number, displayValue: number): number => {\r\n const full = starIndex + 1;\r\n const half = starIndex + 0.5;\r\n if (displayValue >= full) return 1;\r\n if (allowHalf && displayValue >= half) return 0.5;\r\n return 0;\r\n };\r\n\r\n const slots = rateVariants({ size, readonly: readonly || disabled });\r\n const display = hoverValue ?? value;\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n className={slots.root({ className })}\r\n role=\"radiogroup\"\r\n aria-label={ariaLabel || 'Rating'}\r\n >\r\n {Array.from({ length: count }, (_, i) => {\r\n const fraction = getStarFraction(i, display);\r\n const full = i + 1;\r\n const half = i + 0.5;\r\n\r\n const renderStar = (frac: number) => {\r\n if (character) {\r\n if (frac === 0.5) {\r\n return (\r\n <span className=\"relative inline-flex items-center justify-center w-full h-full\">\r\n <span className={`absolute inset-0 overflow-hidden ${inactiveColor}`}>{character}</span>\r\n <span className=\"absolute inset-0 overflow-hidden w-1/2\" style={{ color: 'inherit' }}>\r\n <span className={activeColor}>{character}</span>\r\n </span>\r\n </span>\r\n );\r\n }\r\n return <span className={frac === 1 ? activeColor : inactiveColor}>{character}</span>;\r\n }\r\n\r\n if (frac === 0.5) {\r\n return (\r\n <span className=\"relative inline-flex items-center justify-center w-full h-full\">\r\n <Star className={`${slots.starIcon()} ${inactiveColor}`} fill=\"currentColor\" />\r\n <span\r\n className=\"absolute inset-0 overflow-hidden\"\r\n style={{ width: '50%' }}\r\n aria-hidden=\"true\"\r\n >\r\n <Star className={`${slots.starIcon()} ${activeColor}`} fill=\"currentColor\" />\r\n </span>\r\n </span>\r\n );\r\n }\r\n\r\n return (\r\n <Star\r\n className={`${slots.starIcon()} ${frac === 1 ? activeColor : inactiveColor}`}\r\n fill=\"currentColor\"\r\n />\r\n );\r\n };\r\n\r\n return (\r\n <button\r\n key={i}\r\n type=\"button\"\r\n role=\"radio\"\r\n aria-checked={full <= value}\r\n aria-label={resolveLabel(full)}\r\n disabled={disabled}\r\n className={slots.star()}\r\n onMouseMove={(e) => {\r\n if (readonly || disabled) return;\r\n if (allowHalf) {\r\n const rect = e.currentTarget.getBoundingClientRect();\r\n const isLeft = e.clientX - rect.left < rect.width / 2;\r\n setHoverValue(isLeft ? half : full);\r\n } else {\r\n setHoverValue(full);\r\n }\r\n }}\r\n onMouseLeave={() => setHoverValue(null)}\r\n onClick={(e) => {\r\n if (readonly || disabled) return;\r\n if (allowHalf) {\r\n const rect = e.currentTarget.getBoundingClientRect();\r\n const isLeft = e.clientX - rect.left < rect.width / 2;\r\n handleChange(isLeft ? half : full);\r\n } else {\r\n handleChange(full);\r\n }\r\n }}\r\n onKeyDown={(e) => {\r\n if (readonly || disabled) return;\r\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\r\n e.preventDefault();\r\n handleChange(Math.min(count, value + (allowHalf ? 0.5 : 1)));\r\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\r\n e.preventDefault();\r\n handleChange(Math.max(0, value - (allowHalf ? 0.5 : 1)));\r\n }\r\n }}\r\n >\r\n {renderStar(fraction)}\r\n </button>\r\n );\r\n })}\r\n </div>\r\n );\r\n});\r\n\r\nRate.displayName = 'Rate';\r\n\r\nexport { Rate };\r\n"
814
+ "content": "'use client';\n\nimport * as React from 'react';\nimport { tv, type VariantProps } from 'tailwind-variants';\nimport { Star } from 'lucide-react';\n\nconst rateVariants = tv({\n slots: {\n root: 'inline-flex items-center gap-0.5',\n star: 'relative cursor-pointer transition-transform duration-100 hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-sm',\n starIcon: 'transition-colors duration-150',\n },\n variants: {\n size: {\n sm: { star: 'w-4 h-4', starIcon: 'w-4 h-4' },\n md: { star: 'w-6 h-6', starIcon: 'w-6 h-6' },\n lg: { star: 'w-8 h-8', starIcon: 'w-8 h-8' },\n xl: { star: 'w-10 h-10', starIcon: 'w-10 h-10' },\n },\n readonly: {\n true: { star: 'cursor-default hover:scale-100' },\n },\n },\n defaultVariants: {\n size: 'md',\n },\n});\n\n/** Props for the Rate component */\nexport interface RateProps extends VariantProps<typeof rateVariants> {\n /** Controlled rating value */\n value?: number;\n /** Default rating value (uncontrolled) */\n defaultValue?: number;\n /** Callback fired when the rating changes */\n onChange?: (value: number) => void;\n /** Total number of stars */\n count?: number;\n /** Allow half-star precision */\n allowHalf?: boolean;\n /** Allow clicking the current value to reset to 0 */\n allowClear?: boolean;\n /** Display as read-only (no interaction) */\n readonly?: boolean;\n /** Disable the rating component */\n disabled?: boolean;\n /** Custom element to render instead of the default star icon */\n character?: React.ReactNode;\n /** Tailwind text color class for filled stars */\n activeColor?: string;\n /** Tailwind text color class for empty stars */\n inactiveColor?: string;\n className?: string;\n /** Accessible label for the rating group */\n 'aria-label'?: string;\n /**\n * Custom label for individual stars.\n * Receives the star index (1-based) and returns a string.\n * Defaults to \"1 star\", \"2 stars\", etc.\n */\n getStarLabel?: (index: number) => string;\n}\n\nconst Rate = React.forwardRef<HTMLDivElement, RateProps>(({\n value: controlledValue,\n defaultValue = 0,\n onChange,\n count = 5,\n allowHalf = false,\n allowClear = true,\n readonly = false,\n disabled = false,\n character,\n activeColor = 'text-amber-400',\n inactiveColor = 'text-muted-foreground/30',\n size,\n className,\n 'aria-label': ariaLabel,\n getStarLabel,\n}, ref) => {\n const isControlled = controlledValue !== undefined;\n const [internalValue, setInternalValue] = React.useState(defaultValue);\n const [hoverValue, setHoverValue] = React.useState<number | null>(null);\n\n const value = isControlled ? controlledValue! : internalValue;\n\n const handleChange = (newVal: number) => {\n if (readonly || disabled) return;\n const next = allowClear && newVal === value ? 0 : newVal;\n if (!isControlled) setInternalValue(next);\n onChange?.(next);\n };\n\n const defaultGetStarLabel = (index: number) =>\n index === 1 ? '1 star' : `${index} stars`;\n\n const resolveLabel = getStarLabel ?? defaultGetStarLabel;\n\n const getStarFraction = (starIndex: number, displayValue: number): number => {\n const full = starIndex + 1;\n const half = starIndex + 0.5;\n if (displayValue >= full) return 1;\n if (allowHalf && displayValue >= half) return 0.5;\n return 0;\n };\n\n const slots = rateVariants({ size, readonly: readonly || disabled });\n const display = hoverValue ?? value;\n\n return (\n <div\n ref={ref}\n className={slots.root({ className })}\n role=\"radiogroup\"\n aria-label={ariaLabel || 'Rating'}\n >\n {Array.from({ length: count }, (_, i) => {\n const fraction = getStarFraction(i, display);\n const full = i + 1;\n const half = i + 0.5;\n\n const renderStar = (frac: number) => {\n if (character) {\n if (frac === 0.5) {\n return (\n <span className=\"relative inline-flex items-center justify-center w-full h-full\">\n <span className={`absolute inset-0 overflow-hidden ${inactiveColor}`}>{character}</span>\n <span className=\"absolute inset-0 overflow-hidden w-1/2\" style={{ color: 'inherit' }}>\n <span className={activeColor}>{character}</span>\n </span>\n </span>\n );\n }\n return <span className={frac === 1 ? activeColor : inactiveColor}>{character}</span>;\n }\n\n if (frac === 0.5) {\n return (\n <span className=\"relative inline-flex items-center justify-center w-full h-full\">\n <Star className={`${slots.starIcon()} ${inactiveColor}`} fill=\"currentColor\" />\n <span\n className=\"absolute inset-0 overflow-hidden\"\n style={{ width: '50%' }}\n aria-hidden=\"true\"\n >\n <Star className={`${slots.starIcon()} ${activeColor}`} fill=\"currentColor\" />\n </span>\n </span>\n );\n }\n\n return (\n <Star\n className={`${slots.starIcon()} ${frac === 1 ? activeColor : inactiveColor}`}\n fill=\"currentColor\"\n />\n );\n };\n\n return (\n <button\n key={i}\n type=\"button\"\n role=\"radio\"\n aria-checked={full <= value}\n aria-label={resolveLabel(full)}\n disabled={disabled}\n className={slots.star()}\n onMouseMove={(e) => {\n if (readonly || disabled) return;\n if (allowHalf) {\n const rect = e.currentTarget.getBoundingClientRect();\n const isLeft = e.clientX - rect.left < rect.width / 2;\n setHoverValue(isLeft ? half : full);\n } else {\n setHoverValue(full);\n }\n }}\n onMouseLeave={() => setHoverValue(null)}\n onClick={(e) => {\n if (readonly || disabled) return;\n if (allowHalf) {\n const rect = e.currentTarget.getBoundingClientRect();\n const isLeft = e.clientX - rect.left < rect.width / 2;\n handleChange(isLeft ? half : full);\n } else {\n handleChange(full);\n }\n }}\n onKeyDown={(e) => {\n if (readonly || disabled) return;\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\n e.preventDefault();\n handleChange(Math.min(count, value + (allowHalf ? 0.5 : 1)));\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\n e.preventDefault();\n handleChange(Math.max(0, value - (allowHalf ? 0.5 : 1)));\n }\n }}\n >\n {renderStar(fraction)}\n </button>\n );\n })}\n </div>\n );\n});\n\nRate.displayName = 'Rate';\n\nexport { Rate };\n"
815
815
  }
816
816
  ]
817
817
  },
@@ -838,7 +838,7 @@
838
838
  "files": [
839
839
  {
840
840
  "path": "src/components/ui/scroll-area/ScrollArea.tsx",
841
- "content": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst scrollAreaVariants = tv({\r\n slots: {\r\n root: 'relative overflow-hidden',\r\n viewport: 'h-full w-full rounded-[inherit] [&>div]:!block',\r\n scrollbar: 'flex touch-none select-none transition-colors',\r\n thumb: 'relative rounded-full bg-border hover:bg-muted-foreground/30 transition-colors',\r\n },\r\n variants: {\r\n size: {\r\n sm: { scrollbar: '', thumb: '' },\r\n md: {},\r\n lg: {},\r\n },\r\n orientation: {\r\n vertical: {\r\n scrollbar: 'h-full w-2.5 border-l border-l-transparent p-[1px]',\r\n thumb: 'flex-1',\r\n },\r\n horizontal: {\r\n scrollbar: 'h-2.5 flex-col border-t border-t-transparent p-[1px]',\r\n thumb: '',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n orientation: 'vertical',\r\n },\r\n});\r\n\r\n/** Props for the ScrollArea component */\r\nexport interface ScrollAreaProps\r\n extends React.HTMLAttributes<HTMLDivElement>,\r\n Omit<VariantProps<typeof scrollAreaVariants>, 'orientation'> {\r\n /** Scroll direction: vertical, horizontal, or both */\r\n orientation?: 'vertical' | 'horizontal' | 'both';\r\n /** Accessible label for the scrollable region */\r\n 'aria-label'?: string;\r\n}\r\n\r\nconst ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(\r\n ({ className, children, orientation = 'vertical', size, 'aria-label': ariaLabel, ...props }, ref) => {\r\n const { root, viewport } = scrollAreaVariants({ size });\r\n\r\n const overflowClass =\r\n orientation === 'both'\r\n ? 'overflow-auto'\r\n : orientation === 'horizontal'\r\n ? 'overflow-x-auto overflow-y-hidden'\r\n : 'overflow-y-auto overflow-x-hidden';\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n className={root({ className })}\r\n role=\"region\"\r\n aria-label={ariaLabel}\r\n {...props}\r\n >\r\n <div\r\n className={viewport({\r\n className: `${overflowClass} scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent`,\r\n })}\r\n >\r\n {children}\r\n </div>\r\n </div>\r\n );\r\n }\r\n);\r\nScrollArea.displayName = 'ScrollArea';\r\n\r\n/** Props for the ScrollBar component */\r\nexport interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {\r\n /** Scrollbar axis direction */\r\n orientation?: 'vertical' | 'horizontal';\r\n}\r\n\r\nconst ScrollBar = React.forwardRef<HTMLDivElement, ScrollBarProps>(\r\n ({ className, orientation = 'vertical', ...props }, ref) => {\r\n const { scrollbar, thumb } = scrollAreaVariants({ orientation });\r\n return (\r\n <div ref={ref} className={scrollbar({ className })} {...props}>\r\n <div className={thumb()} />\r\n </div>\r\n );\r\n }\r\n);\r\nScrollBar.displayName = 'ScrollBar';\r\n\r\nexport { ScrollArea, ScrollBar, scrollAreaVariants };\r\n"
841
+ "content": "'use client';\n\nimport * as React from 'react';\nimport { tv, type VariantProps } from 'tailwind-variants';\n\nconst scrollAreaVariants = tv({\n slots: {\n root: 'relative overflow-hidden',\n viewport: 'h-full w-full rounded-[inherit] [&>div]:!block',\n scrollbar: 'flex touch-none select-none transition-colors',\n thumb: 'relative rounded-full bg-border hover:bg-muted-foreground/30 transition-colors',\n },\n variants: {\n size: {\n sm: { scrollbar: '', thumb: '' },\n md: {},\n lg: {},\n },\n orientation: {\n vertical: {\n scrollbar: 'h-full w-2.5 border-l border-l-transparent p-[1px]',\n thumb: 'flex-1',\n },\n horizontal: {\n scrollbar: 'h-2.5 flex-col border-t border-t-transparent p-[1px]',\n thumb: '',\n },\n },\n },\n defaultVariants: {\n size: 'md',\n orientation: 'vertical',\n },\n});\n\n/** Props for the ScrollArea component */\nexport interface ScrollAreaProps\n extends React.HTMLAttributes<HTMLDivElement>,\n Omit<VariantProps<typeof scrollAreaVariants>, 'orientation'> {\n /** Scroll direction: vertical, horizontal, or both */\n orientation?: 'vertical' | 'horizontal' | 'both';\n /** Accessible label for the scrollable region */\n 'aria-label'?: string;\n}\n\nconst ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(\n ({ className, children, orientation = 'vertical', size, 'aria-label': ariaLabel, ...props }, ref) => {\n const { root, viewport } = scrollAreaVariants({ size });\n\n const overflowClass =\n orientation === 'both'\n ? 'overflow-auto'\n : orientation === 'horizontal'\n ? 'overflow-x-auto overflow-y-hidden'\n : 'overflow-y-auto overflow-x-hidden';\n\n return (\n <div\n ref={ref}\n className={root({ className })}\n role=\"region\"\n aria-label={ariaLabel}\n {...props}\n >\n <div\n className={viewport({\n className: `${overflowClass} scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent`,\n })}\n >\n {children}\n </div>\n </div>\n );\n }\n);\nScrollArea.displayName = 'ScrollArea';\n\n/** Props for the ScrollBar component */\nexport interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Scrollbar axis direction */\n orientation?: 'vertical' | 'horizontal';\n}\n\nconst ScrollBar = React.forwardRef<HTMLDivElement, ScrollBarProps>(\n ({ className, orientation = 'vertical', ...props }, ref) => {\n const { scrollbar, thumb } = scrollAreaVariants({ orientation });\n return (\n <div ref={ref} className={scrollbar({ className })} {...props}>\n <div className={thumb()} />\n </div>\n );\n }\n);\nScrollBar.displayName = 'ScrollBar';\n\nexport { ScrollArea, ScrollBar, scrollAreaVariants };\n"
842
842
  }
843
843
  ]
844
844
  },
@@ -853,11 +853,11 @@
853
853
  "files": [
854
854
  {
855
855
  "path": "src/components/ui/select/MultiSelect.tsx",
856
- "content": "\"use client\"\r\nimport * as React from 'react';\r\nimport { Select as BaseSelect } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronDown, Check, X } from 'lucide-react';\r\n\r\nconst multiSelectVariants = tv({\r\n slots: {\r\n trigger: 'flex min-h-10 w-full items-start justify-between rounded-lg border border-border bg-background px-3 py-2 text-sm ring-offset-background focus:border-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow group cursor-pointer gap-2',\r\n content: 'relative z-50 max-h-[300px] min-w-[var(--anchor-width)] overflow-hidden rounded-lg border border-border bg-background text-foreground shadow-[rgba(0,0,0,0.08)_0px_4px_16px] data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n viewport: 'p-1',\r\n item: 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-muted data-highlighted:text-foreground',\r\n icon: 'h-4 w-4 opacity-50 transition-transform duration-200 group-data-open:rotate-180',\r\n }\r\n});\r\n\r\nconst { trigger, content, viewport, item, icon } = multiSelectVariants();\r\n\r\n/** Props for the MultiSelect component */\r\nexport interface MultiSelectProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Root>, 'value' | 'defaultValue'> {\r\n /** Label text displayed above the select trigger */\r\n label?: string;\r\n /** Helper text displayed below the select (hidden when error is present) */\r\n description?: string;\r\n /** Error message displayed below the select; also applies danger styling */\r\n error?: string;\r\n /** Placeholder shown when no option is selected */\r\n placeholder?: string;\r\n /** Array of selectable options */\r\n options: { label: string; value: string }[];\r\n id?: string;\r\n className?: string;\r\n /** Controlled selected values */\r\n value?: string[];\r\n /** Initial selected values for uncontrolled usage */\r\n defaultValue?: string[];\r\n /** Whether a clear button is shown when value is selected */\r\n clearable?: boolean;\r\n /** Callback fired when the selected values change */\r\n onChange?: (values: string[]) => void;\r\n /** Text shown when the options array is empty */\r\n emptyText?: string;\r\n /** Accessible label for the clear button */\r\n clearLabel?: string;\r\n /** Max tags displayed before showing \"+N more\" */\r\n maxTags?: number;\r\n}\r\n\r\nconst MultiSelect = React.forwardRef<React.ElementRef<typeof BaseSelect.Trigger>, MultiSelectProps>(\r\n ({ label, description, error, placeholder = 'Select...', options, id, className, clearable = true, onChange, value, defaultValue, emptyText = 'No results found.', clearLabel = 'Clear selection', maxTags = 2, ...props }, ref) => {\r\n const triggerRef = React.useRef<HTMLButtonElement>(null);\r\n\r\n const [selectedValues, setSelectedValues] = React.useState<string[]>(value ?? defaultValue ?? []);\r\n\r\n React.useEffect(() => {\r\n if (value !== undefined) setSelectedValues(value);\r\n }, [value]);\r\n\r\n const handleValueChange = (val: string) => {\r\n const newValues = selectedValues.includes(val)\r\n ? selectedValues.filter((v) => v !== val)\r\n : [...selectedValues, val];\r\n setSelectedValues(newValues);\r\n onChange?.(newValues);\r\n };\r\n\r\n const handleClear = (e: React.MouseEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n setSelectedValues([]);\r\n onChange?.([]);\r\n };\r\n\r\n const selectedLabels = selectedValues\r\n .map((v) => options.find((o) => o.value === v)?.label)\r\n .filter(Boolean) as string[];\r\n\r\n const displayLabels = selectedLabels.slice(0, maxTags);\r\n const moreCount = selectedLabels.length - maxTags;\r\n\r\n return (\r\n <div className=\"flex flex-col gap-1.5 w-full\">\r\n {label && (\r\n <label className=\"text-sm font-medium text-foreground leading-none\">\r\n {label}\r\n </label>\r\n )}\r\n\r\n <div className=\"relative w-full\">\r\n <BaseSelect.Root\r\n value={undefined}\r\n onValueChange={handleValueChange as (value: unknown) => void}\r\n {...props}\r\n >\r\n <BaseSelect.Trigger\r\n ref={(node) => {\r\n triggerRef.current = node;\r\n if (typeof ref === 'function') ref(node);\r\n else if (ref) (ref as React.RefObject <HTMLButtonElement | null>).current = node;\r\n }}\r\n className={trigger({ className: cn(className, error ? 'border-danger focus:border-danger' : '') })}\r\n id={id}\r\n >\r\n <div className=\"flex flex-wrap gap-1.5 items-center\">\r\n {selectedLabels.length === 0 ? (\r\n <span className=\"text-muted-foreground\">{placeholder}</span>\r\n ) : (\r\n <>\r\n {displayLabels.map((label) => (\r\n <span\r\n key={label}\r\n className=\"inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs font-medium text-foreground\"\r\n >\r\n {label}\r\n </span>\r\n ))}\r\n {moreCount > 0 && (\r\n <span className=\"text-xs text-muted-foreground\">\r\n +{moreCount} more\r\n </span>\r\n )}\r\n </>\r\n )}\r\n </div>\r\n <BaseSelect.Icon>\r\n { selectedValues.length <= 0 &&<ChevronDown className={icon()} />}\r\n </BaseSelect.Icon>\r\n </BaseSelect.Trigger>\r\n <BaseSelect.Portal>\r\n <BaseSelect.Positioner anchor={triggerRef} className=\"z-50\" sideOffset={4}>\r\n <BaseSelect.Popup className={content()}>\r\n <div className={viewport()}>\r\n {options.length === 0 ? (\r\n <div className=\"py-2 px-8 text-sm text-muted-foreground italic text-center\">\r\n {emptyText}\r\n </div>\r\n ) : (\r\n options.map((option) => (\r\n <button\r\n key={option.value}\r\n type=\"button\"\r\n onClick={(e) => {\r\n e.preventDefault();\r\n handleValueChange(option.value);\r\n }}\r\n className={cn(\r\n item(),\r\n 'text-left',\r\n selectedValues.includes(option.value) && 'bg-muted text-foreground'\r\n )}\r\n >\r\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\r\n {selectedValues.includes(option.value) && <Check className=\"h-4 w-4\" />}\r\n </span>\r\n <span>{option.label}</span>\r\n </button>\r\n ))\r\n )}\r\n </div>\r\n </BaseSelect.Popup>\r\n </BaseSelect.Positioner>\r\n </BaseSelect.Portal>\r\n </BaseSelect.Root>\r\n\r\n {clearable && selectedValues.length > 0 && (\r\n <button\r\n type=\"button\"\r\n aria-label={clearLabel}\r\n onMouseDown={handleClear}\r\n className=\"cursor-pointer absolute right-1 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-red-50 hover:text-red-500 transition-colors z-10\"\r\n >\r\n <X className=\"h-3 w-3\" />\r\n </button>\r\n )}\r\n </div>\r\n\r\n {description && !error && (\r\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nMultiSelect.displayName = 'MultiSelect';\r\n\r\nexport { MultiSelect };\r\n"
856
+ "content": "import * as React from 'react';\r\nimport { Select as BaseSelect } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronDown, Check, X } from 'lucide-react';\r\n\r\nconst multiSelectVariants = tv({\r\n slots: {\r\n trigger: 'flex min-h-10 w-full items-start justify-between rounded-lg border border-border bg-background px-3 py-2 text-sm ring-offset-background focus:border-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow group cursor-pointer gap-2',\r\n content: 'relative z-50 max-h-[300px] min-w-[var(--anchor-width)] overflow-hidden rounded-lg border border-border bg-background text-foreground shadow-[rgba(0,0,0,0.08)_0px_4px_16px] data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n viewport: 'p-1',\r\n item: 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-muted data-highlighted:text-foreground',\r\n icon: 'h-4 w-4 opacity-50 transition-transform duration-200 group-data-open:rotate-180',\r\n }\r\n});\r\n\r\nconst { trigger, content, viewport, item, icon } = multiSelectVariants();\r\n\r\n/** Props for the MultiSelect component */\r\nexport interface MultiSelectProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Root>, 'value' | 'defaultValue'> {\r\n /** Label text displayed above the select trigger */\r\n label?: string;\r\n /** Helper text displayed below the select (hidden when error is present) */\r\n description?: string;\r\n /** Error message displayed below the select; also applies danger styling */\r\n error?: string;\r\n /** Placeholder shown when no option is selected */\r\n placeholder?: string;\r\n /** Array of selectable options */\r\n options: { label: string; value: string }[];\r\n id?: string;\r\n className?: string;\r\n /** Controlled selected values */\r\n value?: string[];\r\n /** Initial selected values for uncontrolled usage */\r\n defaultValue?: string[];\r\n /** Whether a clear button is shown when value is selected */\r\n clearable?: boolean;\r\n /** Callback fired when the selected values change */\r\n onChange?: (values: string[]) => void;\r\n /** Text shown when the options array is empty */\r\n emptyText?: string;\r\n /** Accessible label for the clear button */\r\n clearLabel?: string;\r\n /** Max tags displayed before showing \"+N more\" */\r\n maxTags?: number;\r\n}\r\n\r\nconst MultiSelect = React.forwardRef<React.ElementRef<typeof BaseSelect.Trigger>, MultiSelectProps>(\r\n ({ label, description, error, placeholder = 'Select...', options, id, className, clearable = true, onChange, value, defaultValue, emptyText = 'No results found.', clearLabel = 'Clear selection', maxTags = 2, ...props }, ref) => {\r\n const triggerRef = React.useRef<HTMLButtonElement>(null);\r\n\r\n const [selectedValues, setSelectedValues] = React.useState<string[]>(value ?? defaultValue ?? []);\r\n\r\n React.useEffect(() => {\r\n if (value !== undefined) setSelectedValues(value);\r\n }, [value]);\r\n\r\n const handleValueChange = (val: string) => {\r\n const newValues = selectedValues.includes(val)\r\n ? selectedValues.filter((v) => v !== val)\r\n : [...selectedValues, val];\r\n setSelectedValues(newValues);\r\n onChange?.(newValues);\r\n };\r\n\r\n const handleClear = (e: React.MouseEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n setSelectedValues([]);\r\n onChange?.([]);\r\n };\r\n\r\n const selectedLabels = selectedValues\r\n .map((v) => options.find((o) => o.value === v)?.label)\r\n .filter(Boolean) as string[];\r\n\r\n const displayLabels = selectedLabels.slice(0, maxTags);\r\n const moreCount = selectedLabels.length - maxTags;\r\n\r\n return (\r\n <div className=\"flex flex-col gap-1.5 w-full\">\r\n {label && (\r\n <label className=\"text-sm font-medium text-foreground\">\r\n {label}\r\n </label>\r\n )}\r\n\r\n <div className=\"relative w-full\">\r\n <BaseSelect.Root\r\n value={undefined}\r\n onValueChange={handleValueChange as (value: unknown) => void}\r\n {...props}\r\n >\r\n <BaseSelect.Trigger\r\n ref={(node) => {\r\n triggerRef.current = node;\r\n if (typeof ref === 'function') ref(node);\r\n else if (ref) (ref as React.RefObject <HTMLButtonElement | null>).current = node;\r\n }}\r\n className={trigger({ className: cn(className, error ? 'border-danger focus:border-danger' : '') })}\r\n id={id}\r\n >\r\n <div className=\"flex flex-wrap gap-1.5 items-center\">\r\n {selectedLabels.length === 0 ? (\r\n <span className=\"text-muted-foreground\">{placeholder}</span>\r\n ) : (\r\n <>\r\n {displayLabels.map((label) => (\r\n <span\r\n key={label}\r\n className=\"inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs font-medium text-foreground\"\r\n >\r\n {label}\r\n </span>\r\n ))}\r\n {moreCount > 0 && (\r\n <span className=\"text-xs text-muted-foreground\">\r\n +{moreCount} more\r\n </span>\r\n )}\r\n </>\r\n )}\r\n </div>\r\n <BaseSelect.Icon>\r\n { selectedValues.length <= 0 &&<ChevronDown className={icon()} />}\r\n </BaseSelect.Icon>\r\n </BaseSelect.Trigger>\r\n <BaseSelect.Portal>\r\n <BaseSelect.Positioner anchor={triggerRef} className=\"z-50\" sideOffset={4}>\r\n <BaseSelect.Popup className={content()}>\r\n <div className={viewport()}>\r\n {options.length === 0 ? (\r\n <div className=\"py-2 px-8 text-sm text-muted-foreground italic text-center\">\r\n {emptyText}\r\n </div>\r\n ) : (\r\n options.map((option) => (\r\n <button\r\n key={option.value}\r\n type=\"button\"\r\n onClick={(e) => {\r\n e.preventDefault();\r\n handleValueChange(option.value);\r\n }}\r\n className={cn(\r\n item(),\r\n 'text-left',\r\n selectedValues.includes(option.value) && 'bg-muted text-foreground'\r\n )}\r\n >\r\n <span className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\r\n {selectedValues.includes(option.value) && <Check className=\"h-4 w-4\" />}\r\n </span>\r\n <span>{option.label}</span>\r\n </button>\r\n ))\r\n )}\r\n </div>\r\n </BaseSelect.Popup>\r\n </BaseSelect.Positioner>\r\n </BaseSelect.Portal>\r\n </BaseSelect.Root>\r\n\r\n {clearable && selectedValues.length > 0 && (\r\n <button\r\n type=\"button\"\r\n aria-label={clearLabel}\r\n onMouseDown={handleClear}\r\n className=\"cursor-pointer absolute right-1 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-red-50 hover:text-red-500 transition-colors z-10\"\r\n >\r\n <X className=\"h-3 w-3\" />\r\n </button>\r\n )}\r\n </div>\r\n\r\n {description && !error && (\r\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nMultiSelect.displayName = 'MultiSelect';\r\n\r\nexport { MultiSelect };\r\n"
857
857
  },
858
858
  {
859
859
  "path": "src/components/ui/select/Select.tsx",
860
- "content": "import * as React from 'react';\r\nimport { Select as BaseSelect } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronDown, Check, X } from 'lucide-react';\r\n\r\nconst selectVariants = tv({\r\n slots: {\r\n trigger: 'flex h-10 w-full items-center justify-between rounded-lg border border-border bg-background px-3 py-2 text-sm ring-offset-background focus:border-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow group cursor-pointer',\r\n content: 'relative z-50 max-h-[300px] min-w-[var(--anchor-width)] overflow-hidden rounded-lg border border-border bg-background text-foreground shadow-[rgba(0,0,0,0.08)_0px_4px_16px] data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n viewport: 'p-1',\r\n item: 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-muted data-highlighted:text-foreground',\r\n icon: 'h-4 w-4 opacity-50 transition-transform duration-200 group-data-open:rotate-180',\r\n }\r\n});\r\n\r\nconst { trigger, content, viewport, item, icon } = selectVariants();\r\n\r\n/** Props for the Select component */\r\nexport interface SelectProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Root>, 'value' | 'defaultValue'> {\r\n /** Label text displayed above the select trigger */\r\n label?: string;\r\n /** Helper text displayed below the select (hidden when error is present) */\r\n description?: string;\r\n /** Error message displayed below the select; also applies danger styling */\r\n error?: string;\r\n /** Placeholder shown when no option is selected */\r\n placeholder?: string;\r\n /** Array of selectable options */\r\n options: { label: string; value: string }[];\r\n id?: string;\r\n className?: string;\r\n /** Controlled selected value */\r\n value?: string;\r\n /** Initial selected value for uncontrolled usage */\r\n defaultValue?: string;\r\n /** Whether a clear button is shown when a value is selected */\r\n clearable?: boolean;\r\n /** Callback fired when the selected value changes */\r\n onChange?: (value: string) => void;\r\n /** Text shown when the options array is empty */\r\n emptyText?: string;\r\n /** Accessible label for the clear button */\r\n clearLabel?: string;\r\n}\r\n\r\nconst Select = React.forwardRef<React.ElementRef<typeof BaseSelect.Trigger>, SelectProps>(\r\n ({ label, description, error, placeholder = 'Select...', options, id, className, clearable = true, onChange, value, defaultValue, emptyText = 'No results found.', clearLabel = 'Clear selection', ...props }, ref) => {\r\n const triggerRef = React.useRef<HTMLButtonElement>(null);\r\n\r\n const [selectedValue, setSelectedValue] = React.useState<string>(value ?? defaultValue ?? '');\r\n\r\n React.useEffect(() => {\r\n if (value !== undefined) setSelectedValue(value);\r\n }, [value]);\r\n\r\n const handleValueChange = (val: string) => {\r\n setSelectedValue(val);\r\n onChange?.(val);\r\n };\r\n\r\n const handleClear = (e: React.SyntheticEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n setSelectedValue('');\r\n onChange?.('');\r\n };\r\n\r\n const selectedLabel = options.find((o) => o.value === selectedValue)?.label;\r\n\r\n return (\r\n <div className=\"flex flex-col gap-1.5\">\r\n {label && (\r\n <label className=\"text-sm font-medium text-foreground leading-none\">\r\n {label}\r\n </label>\r\n )}\r\n\r\n {/*\r\n * Wrapper relative: nút X nằm NGOÀI BaseSelect.Trigger (absolute)\r\n * → click X không bao giờ bubble lên Trigger → popup không mở\r\n */}\r\n <BaseSelect.Root\r\n value={value}\r\n defaultValue={defaultValue}\r\n onValueChange={handleValueChange as (value: unknown) => void}\r\n {...props}\r\n >\r\n <BaseSelect.Trigger\r\n ref={(node) => {\r\n triggerRef.current = node;\r\n if (typeof ref === 'function') ref(node);\r\n else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;\r\n }}\r\n className={trigger({ className: cn(className, error ? 'border-danger focus:border-danger' : '') })}\r\n id={id}\r\n >\r\n <span className={cn('truncate', selectedLabel ? 'text-foreground' : 'text-muted-foreground')}>\r\n {selectedLabel ?? placeholder}\r\n </span>\r\n \r\n <div className=\"flex items-center gap-1 shrink-0 text-muted-foreground\">\r\n {clearable && selectedValue ? (\r\n <span\r\n role=\"button\"\r\n aria-label={clearLabel}\r\n onPointerDown={(e) => {\r\n e.stopPropagation();\r\n handleClear(e);\r\n }}\r\n onClick={(e) => e.stopPropagation()}\r\n className=\"cursor-pointer flex h-5 w-5 items-center justify-center rounded-full hover:bg-red-50 hover:text-red-500 transition-colors z-10 pointer-events-auto\"\r\n >\r\n <X className=\"h-3.5 w-3.5\" />\r\n </span>\r\n ) : (\r\n <BaseSelect.Icon>\r\n <ChevronDown className={icon()} />\r\n </BaseSelect.Icon>\r\n )}\r\n </div>\r\n </BaseSelect.Trigger>\r\n <BaseSelect.Portal>\r\n <BaseSelect.Positioner anchor={triggerRef} className=\"z-50\" sideOffset={4}>\r\n <BaseSelect.Popup className={content()}>\r\n <div className={viewport()}>\r\n {options.length === 0 ? (\r\n <div className=\"py-2 px-8 text-sm text-muted-foreground italic text-center\">\r\n {emptyText}\r\n </div>\r\n ) : (\r\n options.map((option) => (\r\n <BaseSelect.Item key={option.value} value={option.value} className={item()}>\r\n <BaseSelect.ItemIndicator className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\r\n {option.value === selectedValue && <Check className=\"h-4 w-4\" />}\r\n </BaseSelect.ItemIndicator>\r\n <BaseSelect.ItemText>{option.label}</BaseSelect.ItemText>\r\n </BaseSelect.Item>\r\n ))\r\n )}\r\n </div>\r\n </BaseSelect.Popup>\r\n </BaseSelect.Positioner>\r\n </BaseSelect.Portal>\r\n </BaseSelect.Root>\r\n\r\n {description && !error && (\r\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nSelect.displayName = 'Select';\r\n\r\nexport { Select };\r\n"
860
+ "content": "import * as React from 'react';\r\nimport { Select as BaseSelect } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { ChevronDown, Check, X } from 'lucide-react';\r\n\r\nconst selectVariants = tv({\r\n slots: {\r\n trigger: 'flex h-10 w-full items-center justify-between rounded-lg border border-border bg-background px-3 py-2 text-sm ring-offset-background focus:border-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow group cursor-pointer',\r\n content: 'relative z-50 max-h-[300px] min-w-[var(--anchor-width)] overflow-hidden rounded-lg border border-border bg-background text-foreground shadow-[rgba(0,0,0,0.08)_0px_4px_16px] data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n viewport: 'p-1',\r\n item: 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-muted data-highlighted:text-foreground',\r\n icon: 'h-4 w-4 opacity-50 transition-transform duration-200 group-data-open:rotate-180',\r\n }\r\n});\r\n\r\nconst { trigger, content, viewport, item, icon } = selectVariants();\r\n\r\n/** Props for the Select component */\r\nexport interface SelectProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Root>, 'value' | 'defaultValue'> {\r\n /** Label text displayed above the select trigger */\r\n label?: string;\r\n /** Helper text displayed below the select (hidden when error is present) */\r\n description?: string;\r\n /** Error message displayed below the select; also applies danger styling */\r\n error?: string;\r\n /** Placeholder shown when no option is selected */\r\n placeholder?: string;\r\n /** Array of selectable options */\r\n options: { label: string; value: string }[];\r\n id?: string;\r\n className?: string;\r\n /** Controlled selected value */\r\n value?: string;\r\n /** Initial selected value for uncontrolled usage */\r\n defaultValue?: string;\r\n /** Whether a clear button is shown when a value is selected */\r\n clearable?: boolean;\r\n /** Callback fired when the selected value changes */\r\n onChange?: (value: string) => void;\r\n /** Text shown when the options array is empty */\r\n emptyText?: string;\r\n /** Accessible label for the clear button */\r\n clearLabel?: string;\r\n}\r\n\r\nconst Select = React.forwardRef<React.ElementRef<typeof BaseSelect.Trigger>, SelectProps>(\r\n ({ label, description, error, placeholder = 'Select...', options, id, className, clearable = true, onChange, value, defaultValue, emptyText = 'No results found.', clearLabel = 'Clear selection', ...props }, ref) => {\r\n const triggerRef = React.useRef<HTMLButtonElement>(null);\r\n\r\n const [selectedValue, setSelectedValue] = React.useState<string>(value ?? defaultValue ?? '');\r\n\r\n React.useEffect(() => {\r\n if (value !== undefined) setSelectedValue(value);\r\n }, [value]);\r\n\r\n const handleValueChange = (val: string) => {\r\n setSelectedValue(val);\r\n onChange?.(val);\r\n };\r\n\r\n const handleClear = (e: React.SyntheticEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n setSelectedValue('');\r\n onChange?.('');\r\n };\r\n\r\n const selectedLabel = options.find((o) => o.value === selectedValue)?.label;\r\n\r\n return (\r\n <div className=\"flex flex-col gap-1.5\">\r\n {label && (\r\n <label className=\"text-sm font-medium text-foreground\">\r\n {label}\r\n </label>\r\n )}\r\n\r\n {/*\r\n * Wrapper relative: nút X nằm NGOÀI BaseSelect.Trigger (absolute)\r\n * → click X không bao giờ bubble lên Trigger → popup không mở\r\n */}\r\n <BaseSelect.Root\r\n value={value}\r\n defaultValue={defaultValue}\r\n onValueChange={handleValueChange as (value: unknown) => void}\r\n {...props}\r\n >\r\n <BaseSelect.Trigger\r\n ref={(node) => {\r\n triggerRef.current = node;\r\n if (typeof ref === 'function') ref(node);\r\n else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;\r\n }}\r\n className={trigger({ className: cn(className, error ? 'border-danger focus:border-danger' : '') })}\r\n id={id}\r\n >\r\n <span className={cn('truncate', selectedLabel ? 'text-foreground' : 'text-muted-foreground')}>\r\n {selectedLabel ?? placeholder}\r\n </span>\r\n \r\n <div className=\"flex items-center gap-1 shrink-0 text-muted-foreground\">\r\n {clearable && selectedValue ? (\r\n <span\r\n role=\"button\"\r\n aria-label={clearLabel}\r\n onPointerDown={(e) => {\r\n e.stopPropagation();\r\n handleClear(e);\r\n }}\r\n onClick={(e) => e.stopPropagation()}\r\n className=\"cursor-pointer flex h-5 w-5 items-center justify-center rounded-full hover:bg-red-50 hover:text-red-500 transition-colors z-10 pointer-events-auto\"\r\n >\r\n <X className=\"h-3.5 w-3.5\" />\r\n </span>\r\n ) : (\r\n <BaseSelect.Icon>\r\n <ChevronDown className={icon()} />\r\n </BaseSelect.Icon>\r\n )}\r\n </div>\r\n </BaseSelect.Trigger>\r\n <BaseSelect.Portal>\r\n <BaseSelect.Positioner anchor={triggerRef} className=\"z-50\" sideOffset={4}>\r\n <BaseSelect.Popup className={content()}>\r\n <div className={viewport()}>\r\n {options.length === 0 ? (\r\n <div className=\"py-2 px-8 text-sm text-muted-foreground italic text-center\">\r\n {emptyText}\r\n </div>\r\n ) : (\r\n options.map((option) => (\r\n <BaseSelect.Item key={option.value} value={option.value} className={item()}>\r\n <BaseSelect.ItemIndicator className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\r\n {option.value === selectedValue && <Check className=\"h-4 w-4\" />}\r\n </BaseSelect.ItemIndicator>\r\n <BaseSelect.ItemText>{option.label}</BaseSelect.ItemText>\r\n </BaseSelect.Item>\r\n ))\r\n )}\r\n </div>\r\n </BaseSelect.Popup>\r\n </BaseSelect.Positioner>\r\n </BaseSelect.Portal>\r\n </BaseSelect.Root>\r\n\r\n {description && !error && (\r\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nSelect.displayName = 'Select';\r\n\r\nexport { Select };\r\n"
861
861
  }
862
862
  ]
863
863
  },
@@ -901,11 +901,11 @@
901
901
  "files": [
902
902
  {
903
903
  "path": "src/components/ui/sidebar/Sidebar.tsx",
904
- "content": "// Sidebar — barrel re-export\r\n// Split into: SidebarContext, SidebarLayout, SidebarMenu, SidebarUserMenu\r\n\r\nexport { useSidebar, SidebarProvider } from './SidebarContext';\r\nexport type { SidebarProviderProps } from './SidebarContext';\r\n\r\nexport {\r\n SidebarTrigger,\r\n Sidebar,\r\n SidebarRail,\r\n SidebarInset,\r\n SidebarHeader,\r\n SidebarFooter,\r\n SidebarContent,\r\n SidebarSeparator,\r\n} from './SidebarLayout';\r\nexport type { SidebarProps } from './SidebarLayout';\r\n\r\nexport {\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarMenuButton,\r\n SidebarNavLink,\r\n SidebarMenuCollapsible,\r\n SidebarMenuSub,\r\n SidebarMenuSubItem,\r\n SidebarMenuBadge,\r\n SidebarMenuSkeleton,\r\n} from './SidebarMenu';\r\nexport type { SidebarMenuButtonProps, SidebarNavLinkProps, SidebarMenuCollapsibleProps } from './SidebarMenu';\r\n\r\nexport { UserMenuPopover, UserMenuItem } from './SidebarUserMenu';\r\n"
904
+ "content": "// Sidebar — barrel re-export\n// Split into: SidebarContext, SidebarLayout, SidebarMenu, SidebarUserMenu\n\nexport { useSidebar, SidebarProvider } from './SidebarContext';\nexport type { SidebarProviderProps } from './SidebarContext';\n\nexport {\n SidebarTrigger,\n Sidebar,\n SidebarRail,\n SidebarInset,\n SidebarHeader,\n SidebarFooter,\n SidebarContent,\n SidebarSeparator,\n} from './SidebarLayout';\nexport type { SidebarProps } from './SidebarLayout';\n\nexport {\n SidebarGroup,\n SidebarGroupLabel,\n SidebarGroupContent,\n SidebarMenu,\n SidebarMenuItem,\n SidebarMenuButton,\n SidebarNavLink,\n SidebarMenuCollapsible,\n SidebarMenuSub,\n SidebarMenuSubItem,\n SidebarMenuBadge,\n SidebarMenuSkeleton,\n} from './SidebarMenu';\nexport type { SidebarMenuButtonProps, SidebarNavLinkProps, SidebarMenuCollapsibleProps } from './SidebarMenu';\n\nexport { UserMenuPopover, UserMenuItem } from './SidebarUserMenu';\n"
905
905
  },
906
906
  {
907
907
  "path": "src/components/ui/sidebar/SidebarContext.tsx",
908
- "content": "import * as React from 'react';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\n// ─── Constants ────────────────────────────────────────────────────────────────\r\n\r\nexport const SIDEBAR_WIDTH_DEFAULT = 256; // px\r\nexport const SIDEBAR_WIDTH_MIN = 160; // px\r\nexport const SIDEBAR_WIDTH_MAX = 480; // px\r\nexport const SIDEBAR_WIDTH_ICON = '4rem';\r\nexport const MOBILE_BREAKPOINT = 768;\r\n\r\n// ─── Context ──────────────────────────────────────────────────────────────────\r\n\r\nexport type SidebarState = 'expanded' | 'collapsed';\r\n\r\nexport interface SidebarContextValue {\r\n state: SidebarState;\r\n open: boolean;\r\n setOpen: (open: boolean) => void;\r\n toggleSidebar: () => void;\r\n isMobile: boolean;\r\n openMobile: boolean;\r\n setOpenMobile: (open: boolean) => void;\r\n sidebarWidth: number;\r\n setSidebarWidth: (w: number) => void;\r\n}\r\n\r\nexport const SidebarContext = React.createContext<SidebarContextValue | null>(null);\r\n\r\nexport function useSidebar() {\r\n const ctx = React.useContext(SidebarContext);\r\n if (!ctx) throw new Error('useSidebar must be used within SidebarProvider');\r\n return ctx;\r\n}\r\n\r\n// ─── Provider ─────────────────────────────────────────────────────────────────\r\n\r\n/** Props for the SidebarProvider that manages sidebar state (open/collapsed, mobile, width) */\r\nexport interface SidebarProviderProps {\r\n children: React.ReactNode;\r\n /** Initial open state for uncontrolled usage (default: true) */\r\n defaultOpen?: boolean;\r\n /** Controlled open state */\r\n open?: boolean;\r\n /** Callback fired when the sidebar opens or closes */\r\n onOpenChange?: (open: boolean) => void;\r\n className?: string;\r\n style?: React.CSSProperties;\r\n}\r\n\r\nexport const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\r\n ({ children, defaultOpen = true, open: controlledOpen, onOpenChange, className, style }, ref) => {\r\n const [isMobile, setIsMobile] = React.useState(false);\r\n const [openMobile, setOpenMobile] = React.useState(false);\r\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\r\n const [sidebarWidth, setSidebarWidth] = React.useState(SIDEBAR_WIDTH_DEFAULT);\r\n\r\n const isControlled = controlledOpen !== undefined;\r\n const open = isControlled ? controlledOpen! : internalOpen;\r\n\r\n React.useEffect(() => {\r\n const check = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\r\n check();\r\n window.addEventListener('resize', check);\r\n return () => window.removeEventListener('resize', check);\r\n }, []);\r\n\r\n const setOpen = React.useCallback(\r\n (val: boolean) => {\r\n if (!isControlled) setInternalOpen(val);\r\n onOpenChange?.(val);\r\n },\r\n [isControlled, onOpenChange]\r\n );\r\n\r\n const toggleSidebar = React.useCallback(() => {\r\n if (isMobile) setOpenMobile((v) => !v);\r\n else setOpen(!open);\r\n }, [isMobile, open, setOpen]);\r\n\r\n React.useEffect(() => {\r\n const onKey = (e: KeyboardEvent) => {\r\n if ((e.metaKey || e.ctrlKey) && e.key === 'b') {\r\n e.preventDefault();\r\n toggleSidebar();\r\n }\r\n };\r\n window.addEventListener('keydown', onKey);\r\n return () => window.removeEventListener('keydown', onKey);\r\n }, [toggleSidebar]);\r\n\r\n const state: SidebarState = open ? 'expanded' : 'collapsed';\r\n\r\n return (\r\n <SidebarContext.Provider\r\n value={{ state, open, setOpen, toggleSidebar, isMobile, openMobile, setOpenMobile, sidebarWidth, setSidebarWidth }}\r\n >\r\n <div\r\n ref={ref}\r\n data-sidebar-state={state}\r\n style={\r\n {\r\n '--sidebar-width': `${sidebarWidth}px`,\r\n '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,\r\n ...style,\r\n } as React.CSSProperties\r\n }\r\n className={cn('group/sidebar-wrapper flex min-h-screen w-full has-data-[variant=inset]:bg-muted/30', className)}\r\n >\r\n {children}\r\n </div>\r\n </SidebarContext.Provider>\r\n );\r\n }\r\n);\r\nSidebarProvider.displayName = 'SidebarProvider';\r\n"
908
+ "content": "import * as React from 'react';\nimport { cn } from '@/lib/utils/cn';\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nexport const SIDEBAR_WIDTH_DEFAULT = 256; // px\nexport const SIDEBAR_WIDTH_MIN = 160; // px\nexport const SIDEBAR_WIDTH_MAX = 480; // px\nexport const SIDEBAR_WIDTH_ICON = '4rem';\nexport const MOBILE_BREAKPOINT = 768;\n\n// ─── Context ──────────────────────────────────────────────────────────────────\n\nexport type SidebarState = 'expanded' | 'collapsed';\n\nexport interface SidebarContextValue {\n state: SidebarState;\n open: boolean;\n setOpen: (open: boolean) => void;\n toggleSidebar: () => void;\n isMobile: boolean;\n openMobile: boolean;\n setOpenMobile: (open: boolean) => void;\n sidebarWidth: number;\n setSidebarWidth: (w: number) => void;\n}\n\nexport const SidebarContext = React.createContext<SidebarContextValue | null>(null);\n\nexport function useSidebar() {\n const ctx = React.useContext(SidebarContext);\n if (!ctx) throw new Error('useSidebar must be used within SidebarProvider');\n return ctx;\n}\n\n// ─── Provider ─────────────────────────────────────────────────────────────────\n\n/** Props for the SidebarProvider that manages sidebar state (open/collapsed, mobile, width) */\nexport interface SidebarProviderProps {\n children: React.ReactNode;\n /** Initial open state for uncontrolled usage (default: true) */\n defaultOpen?: boolean;\n /** Controlled open state */\n open?: boolean;\n /** Callback fired when the sidebar opens or closes */\n onOpenChange?: (open: boolean) => void;\n className?: string;\n style?: React.CSSProperties;\n}\n\nexport const SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\n ({ children, defaultOpen = true, open: controlledOpen, onOpenChange, className, style }, ref) => {\n const [isMobile, setIsMobile] = React.useState(false);\n const [openMobile, setOpenMobile] = React.useState(false);\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n const [sidebarWidth, setSidebarWidth] = React.useState(SIDEBAR_WIDTH_DEFAULT);\n\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen! : internalOpen;\n\n React.useEffect(() => {\n const check = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n check();\n window.addEventListener('resize', check);\n return () => window.removeEventListener('resize', check);\n }, []);\n\n const setOpen = React.useCallback(\n (val: boolean) => {\n if (!isControlled) setInternalOpen(val);\n onOpenChange?.(val);\n },\n [isControlled, onOpenChange]\n );\n\n const toggleSidebar = React.useCallback(() => {\n if (isMobile) setOpenMobile((v) => !v);\n else setOpen(!open);\n }, [isMobile, open, setOpen]);\n\n React.useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key === 'b') {\n e.preventDefault();\n toggleSidebar();\n }\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [toggleSidebar]);\n\n const state: SidebarState = open ? 'expanded' : 'collapsed';\n\n return (\n <SidebarContext.Provider\n value={{ state, open, setOpen, toggleSidebar, isMobile, openMobile, setOpenMobile, sidebarWidth, setSidebarWidth }}\n >\n <div\n ref={ref}\n data-sidebar-state={state}\n style={\n {\n '--sidebar-width': `${sidebarWidth}px`,\n '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn('group/sidebar-wrapper flex min-h-screen w-full has-data-[variant=inset]:bg-muted/30', className)}\n >\n {children}\n </div>\n </SidebarContext.Provider>\n );\n }\n);\nSidebarProvider.displayName = 'SidebarProvider';\n"
909
909
  },
910
910
  {
911
911
  "path": "src/components/ui/sidebar/SidebarLayout.tsx",
@@ -913,11 +913,11 @@
913
913
  },
914
914
  {
915
915
  "path": "src/components/ui/sidebar/SidebarMenu.tsx",
916
- "content": "import * as React from 'react';\r\nimport { NavLink } from 'react-router-dom';\r\nimport { ChevronRight } from 'lucide-react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../tooltip/Tooltip';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { useSidebar } from './SidebarContext';\r\n\r\n// ─── Group ────────────────────────────────────────────────────────────────────\r\n\r\nexport const SidebarGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"group\"\r\n className={cn('relative flex flex-col w-full min-w-0 px-2 py-1', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarGroup.displayName = 'SidebarGroup';\r\n\r\nexport const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const { state } = useSidebar();\r\n return (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"group-label\"\r\n className={cn(\r\n 'flex h-8 shrink-0 items-center rounded-md px-2',\r\n 'text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wider',\r\n 'motion-safe:transition-all motion-safe:duration-200 overflow-hidden whitespace-nowrap select-none',\r\n state === 'collapsed' ? 'opacity-0 h-0 mb-0 hidden' : 'opacity-100',\r\n className\r\n )}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nSidebarGroupLabel.displayName = 'SidebarGroupLabel';\r\n\r\nexport const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"group-content\"\r\n className={cn('w-full', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarGroupContent.displayName = 'SidebarGroupContent';\r\n\r\n// ─── Menu ─────────────────────────────────────────────────────────────────────\r\n\r\nexport const SidebarMenu = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\r\n ({ className, ...props }, ref) => (\r\n <ul\r\n ref={ref}\r\n data-sidebar=\"menu\"\r\n className={cn('flex flex-col gap-0.5 list-none m-0 p-0 w-full', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarMenu.displayName = 'SidebarMenu';\r\n\r\nexport const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\r\n ({ className, ...props }, ref) => (\r\n <li\r\n ref={ref}\r\n data-sidebar=\"menu-item\"\r\n className={cn('group/menu-item relative', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarMenuItem.displayName = 'SidebarMenuItem';\r\n\r\n// ─── SidebarMenuButton ────────────────────────────────────────────────────────\r\n\r\nexport const menuButtonVariants = tv({\r\n base: [\r\n 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md',\r\n 'text-sm font-medium outline-none ring-sidebar-ring motion-safe:transition-all motion-safe:duration-150',\r\n 'hover:bg-accent hover:text-accent-foreground',\r\n 'focus-visible:ring-2 active:bg-accent/80',\r\n 'disabled:pointer-events-none disabled:opacity-50',\r\n 'group-has-data-[sidebar=menu-action]/menu-item:pr-8',\r\n // Data state active\r\n 'data-[active=true]:bg-accent data-[active=true]:text-accent-foreground data-[active=true]:font-semibold',\r\n ],\r\n variants: {\r\n size: {\r\n sm: 'h-7 text-xs px-2',\r\n md: 'h-9 px-2',\r\n lg: 'h-11 text-base px-3',\r\n },\r\n collapsed: {\r\n true: 'justify-center px-0',\r\n false: 'justify-start',\r\n },\r\n },\r\n defaultVariants: { size: 'md', collapsed: false },\r\n});\r\n\r\n/** Props for a sidebar menu button */\r\nexport interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n /** Render as child element instead of a button */\r\n asChild?: boolean;\r\n /** Marks the button as the currently active item */\r\n isActive?: boolean;\r\n /** Tooltip text shown when the sidebar is collapsed */\r\n tooltip?: string;\r\n /** Button size variant */\r\n size?: 'sm' | 'md' | 'lg';\r\n}\r\n\r\nexport const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\r\n ({ className, isActive = false, tooltip, size = 'md', children, ...props }, ref) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n\r\n const button = (\r\n <button\r\n ref={ref}\r\n type=\"button\"\r\n data-sidebar=\"menu-button\"\r\n data-active={isActive}\r\n data-size={size}\r\n className={menuButtonVariants({ size, collapsed: isCollapsed, className })}\r\n {...props}\r\n >\r\n {isCollapsed\r\n ? React.Children.toArray(children)[0]\r\n : children}\r\n </button>\r\n );\r\n\r\n if (isCollapsed && tooltip) {\r\n return (\r\n <Tooltip>\r\n <TooltipTrigger render={button} />\r\n <TooltipContent side=\"right\">{tooltip}</TooltipContent>\r\n </Tooltip>\r\n );\r\n }\r\n\r\n return button;\r\n }\r\n);\r\nSidebarMenuButton.displayName = 'SidebarMenuButton';\r\n\r\n// ─── SidebarNavLink — wraps React Router NavLink ─────────────────────────────\r\n\r\n/** Props for a sidebar navigation link (wraps React Router NavLink) */\r\nexport interface SidebarNavLinkProps {\r\n /** Route path for the link */\r\n to: string;\r\n /** Icon rendered before the label */\r\n icon?: React.ReactNode;\r\n /** Display text for the link; also used as tooltip when collapsed */\r\n label: string;\r\n /** Match route exactly (React Router `end` prop) */\r\n end?: boolean;\r\n /** Badge element rendered after the label */\r\n badge?: React.ReactNode;\r\n /** Link size variant */\r\n size?: 'sm' | 'md' | 'lg';\r\n className?: string;\r\n}\r\n\r\nexport const SidebarNavLink: React.FC<SidebarNavLinkProps> = ({\r\n to,\r\n icon,\r\n label,\r\n end = false,\r\n badge,\r\n size = 'md',\r\n className,\r\n}) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n\r\n const link = (\r\n <NavLink\r\n to={to}\r\n end={end}\r\n className={({ isActive }) => cn(\r\n menuButtonVariants({ size, collapsed: isCollapsed, className }),\r\n isActive ? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold' : 'text-sidebar-foreground/70'\r\n )}\r\n >\r\n {icon && (\r\n <span className=\"shrink-0 flex h-4 w-4 items-center justify-center\">\r\n {icon}\r\n </span>\r\n )}\r\n {!isCollapsed && (\r\n <>\r\n <span className=\"flex-1 truncate\">{label}</span>\r\n {badge && <span className=\"ml-auto shrink-0\">{badge}</span>}\r\n </>\r\n )}\r\n </NavLink>\r\n );\r\n\r\n if (isCollapsed && label) {\r\n return (\r\n <Tooltip>\r\n <TooltipTrigger render={link} />\r\n <TooltipContent side=\"right\">{label}</TooltipContent>\r\n </Tooltip>\r\n );\r\n }\r\n\r\n return link;\r\n};\r\nSidebarNavLink.displayName = 'SidebarNavLink';\r\n\r\n// ─── SidebarMenuCollapsible — nhóm có sub-items ───────────────────────────────\r\n\r\n/** Props for a collapsible sidebar menu group with sub-items */\r\nexport interface SidebarMenuCollapsibleProps {\r\n /** Unique identifier for the group */\r\n id: string;\r\n /** Icon displayed next to the group label */\r\n icon: React.ReactNode;\r\n /** Display text for the collapsible group header */\r\n label: string;\r\n children: React.ReactNode;\r\n /** Whether the group is initially expanded */\r\n defaultOpen?: boolean;\r\n /** When true, the group auto-expands and shows an active indicator */\r\n isChildActive?: boolean;\r\n}\r\n\r\nexport const SidebarMenuCollapsible: React.FC<SidebarMenuCollapsibleProps> = ({\r\n icon,\r\n label,\r\n children,\r\n defaultOpen = false,\r\n isChildActive = false,\r\n}) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n\r\n const [isOpen, setIsOpen] = React.useState(defaultOpen || isChildActive);\r\n const prevOpenRef = React.useRef(isOpen);\r\n\r\n // Khi sidebar collapse → đóng tất cả sub-menu, ghi nhớ state\r\n // Khi sidebar expand → khôi phục state cũ\r\n React.useEffect(() => {\r\n if (isCollapsed) {\r\n prevOpenRef.current = isOpen;\r\n setIsOpen(false);\r\n } else {\r\n setIsOpen(prevOpenRef.current);\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isCollapsed]);\r\n\r\n // Khi có child active, mở group\r\n React.useEffect(() => {\r\n if (isChildActive && !isCollapsed) {\r\n setIsOpen(true);\r\n prevOpenRef.current = true;\r\n }\r\n }, [isChildActive, isCollapsed]);\r\n\r\n const trigger = (\r\n <button\r\n type=\"button\"\r\n aria-expanded={isOpen}\r\n data-active={isChildActive && isCollapsed}\r\n onClick={() => {\r\n if (!isCollapsed) {\r\n const next = !isOpen;\r\n setIsOpen(next);\r\n prevOpenRef.current = next;\r\n }\r\n }}\r\n className={menuButtonVariants({\r\n collapsed: isCollapsed,\r\n className:\r\n isChildActive && isCollapsed\r\n ? 'text-sidebar-accent-foreground'\r\n : 'text-sidebar-foreground/70',\r\n })}\r\n >\r\n <span className=\"shrink-0 flex h-4 w-4 items-center justify-center\">{icon}</span>\r\n {!isCollapsed && (\r\n <>\r\n <span className=\"flex-1 truncate text-left\">{label}</span>\r\n <ChevronRight\r\n className={cn(\r\n 'ml-auto h-3.5 w-3.5 shrink-0 text-sidebar-foreground/40',\r\n 'motion-safe:transition-transform motion-safe:duration-200',\r\n isOpen && 'rotate-90'\r\n )}\r\n />\r\n </>\r\n )}\r\n </button>\r\n );\r\n\r\n return (\r\n <>\r\n {isCollapsed ? (\r\n <Tooltip>\r\n <TooltipTrigger render={trigger} />\r\n <TooltipContent side=\"right\">{label}</TooltipContent>\r\n </Tooltip>\r\n ) : (\r\n trigger\r\n )}\r\n\r\n {/* Sub-items với animation mượt - Sử dụng SidebarMenuSub (ul) để hợp lệ HTML */}\r\n <SidebarMenuSub\r\n className={cn(\r\n 'overflow-hidden motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-in-out',\r\n !isCollapsed && isOpen ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'\r\n )}\r\n >\r\n {children}\r\n </SidebarMenuSub>\r\n </>\r\n );\r\n};\r\nSidebarMenuCollapsible.displayName = 'SidebarMenuCollapsible';\r\n\r\n// ─── SidebarMenuSub ───────────────────────────────────────────────────────────\r\n\r\nexport const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\r\n ({ className, ...props }, ref) => {\r\n const { state } = useSidebar();\r\n if (state === 'collapsed') return null;\r\n return (\r\n <ul\r\n ref={ref}\r\n data-sidebar=\"menu-sub\"\r\n className={cn('mx-3.5 flex min-w-0 flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 list-none', className)}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nSidebarMenuSub.displayName = 'SidebarMenuSub';\r\n\r\nexport const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\r\n (props, ref) => <li ref={ref} {...props} />\r\n);\r\nSidebarMenuSubItem.displayName = 'SidebarMenuSubItem';\r\n\r\n// ─── Badge & Skeleton ─────────────────────────────────────────────────────────\r\n\r\nexport const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"menu-badge\"\r\n className={cn('ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-primary/10 px-1 text-xs font-medium text-primary', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarMenuBadge.displayName = 'SidebarMenuBadge';\r\n\r\nexport const SidebarMenuSkeleton: React.FC<{ showIcon?: boolean }> = ({ showIcon = true }) => (\r\n <div className=\"flex h-9 items-center gap-2 rounded-md px-2\">\r\n {showIcon && <div className=\"h-4 w-4 rounded bg-sidebar-accent motion-safe:animate-pulse shrink-0\" />}\r\n <div className=\"h-4 flex-1 rounded bg-sidebar-accent motion-safe:animate-pulse\" />\r\n </div>\r\n);\r\n"
916
+ "content": "import * as React from 'react';\nimport { NavLink } from 'react-router-dom';\nimport { ChevronRight } from 'lucide-react';\nimport { tv } from 'tailwind-variants';\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../tooltip/Tooltip';\nimport { cn } from '@/lib/utils/cn';\nimport { useSidebar } from './SidebarContext';\n\n// ─── Group ────────────────────────────────────────────────────────────────────\n\nexport const SidebarGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-sidebar=\"group\"\n className={cn('relative flex flex-col w-full min-w-0 px-2 py-1', className)}\n {...props}\n />\n )\n);\nSidebarGroup.displayName = 'SidebarGroup';\n\nexport const SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => {\n const { state } = useSidebar();\n return (\n <div\n ref={ref}\n data-sidebar=\"group-label\"\n className={cn(\n 'flex h-8 shrink-0 items-center rounded-md px-2',\n 'text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wider',\n 'motion-safe:transition-all motion-safe:duration-200 overflow-hidden whitespace-nowrap select-none',\n state === 'collapsed' ? 'opacity-0 h-0 mb-0 hidden' : 'opacity-100',\n className\n )}\n {...props}\n />\n );\n }\n);\nSidebarGroupLabel.displayName = 'SidebarGroupLabel';\n\nexport const SidebarGroupContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-sidebar=\"group-content\"\n className={cn('w-full', className)}\n {...props}\n />\n )\n);\nSidebarGroupContent.displayName = 'SidebarGroupContent';\n\n// ─── Menu ─────────────────────────────────────────────────────────────────────\n\nexport const SidebarMenu = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\n ({ className, ...props }, ref) => (\n <ul\n ref={ref}\n data-sidebar=\"menu\"\n className={cn('flex flex-col gap-0.5 list-none m-0 p-0 w-full', className)}\n {...props}\n />\n )\n);\nSidebarMenu.displayName = 'SidebarMenu';\n\nexport const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\n ({ className, ...props }, ref) => (\n <li\n ref={ref}\n data-sidebar=\"menu-item\"\n className={cn('group/menu-item relative', className)}\n {...props}\n />\n )\n);\nSidebarMenuItem.displayName = 'SidebarMenuItem';\n\n// ─── SidebarMenuButton ────────────────────────────────────────────────────────\n\nexport const menuButtonVariants = tv({\n base: [\n 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md',\n 'text-sm font-medium outline-none ring-sidebar-ring motion-safe:transition-all motion-safe:duration-150',\n 'hover:bg-accent hover:text-accent-foreground',\n 'focus-visible:ring-2 active:bg-accent/80',\n 'disabled:pointer-events-none disabled:opacity-50',\n 'group-has-data-[sidebar=menu-action]/menu-item:pr-8',\n // Data state active\n 'data-[active=true]:bg-accent data-[active=true]:text-accent-foreground data-[active=true]:font-semibold',\n ],\n variants: {\n size: {\n sm: 'h-7 text-xs px-2',\n md: 'h-9 px-2',\n lg: 'h-11 text-base px-3',\n },\n collapsed: {\n true: 'justify-center px-0',\n false: 'justify-start',\n },\n },\n defaultVariants: { size: 'md', collapsed: false },\n});\n\n/** Props for a sidebar menu button */\nexport interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Render as child element instead of a button */\n asChild?: boolean;\n /** Marks the button as the currently active item */\n isActive?: boolean;\n /** Tooltip text shown when the sidebar is collapsed */\n tooltip?: string;\n /** Button size variant */\n size?: 'sm' | 'md' | 'lg';\n}\n\nexport const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\n ({ className, isActive = false, tooltip, size = 'md', children, ...props }, ref) => {\n const { state } = useSidebar();\n const isCollapsed = state === 'collapsed';\n\n const button = (\n <button\n ref={ref}\n type=\"button\"\n data-sidebar=\"menu-button\"\n data-active={isActive}\n data-size={size}\n className={menuButtonVariants({ size, collapsed: isCollapsed, className })}\n {...props}\n >\n {isCollapsed\n ? React.Children.toArray(children)[0]\n : children}\n </button>\n );\n\n if (isCollapsed && tooltip) {\n return (\n <Tooltip>\n <TooltipTrigger render={button} />\n <TooltipContent side=\"right\">{tooltip}</TooltipContent>\n </Tooltip>\n );\n }\n\n return button;\n }\n);\nSidebarMenuButton.displayName = 'SidebarMenuButton';\n\n// ─── SidebarNavLink — wraps React Router NavLink ─────────────────────────────\n\n/** Props for a sidebar navigation link (wraps React Router NavLink) */\nexport interface SidebarNavLinkProps {\n /** Route path for the link */\n to: string;\n /** Icon rendered before the label */\n icon?: React.ReactNode;\n /** Display text for the link; also used as tooltip when collapsed */\n label: string;\n /** Match route exactly (React Router `end` prop) */\n end?: boolean;\n /** Badge element rendered after the label */\n badge?: React.ReactNode;\n /** Link size variant */\n size?: 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nexport const SidebarNavLink: React.FC<SidebarNavLinkProps> = ({\n to,\n icon,\n label,\n end = false,\n badge,\n size = 'md',\n className,\n}) => {\n const { state } = useSidebar();\n const isCollapsed = state === 'collapsed';\n\n const link = (\n <NavLink\n to={to}\n end={end}\n className={({ isActive }) => cn(\n menuButtonVariants({ size, collapsed: isCollapsed, className }),\n isActive ? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold' : 'text-sidebar-foreground/70'\n )}\n >\n {icon && (\n <span className=\"shrink-0 flex h-4 w-4 items-center justify-center\">\n {icon}\n </span>\n )}\n {!isCollapsed && (\n <>\n <span className=\"flex-1 truncate\">{label}</span>\n {badge && <span className=\"ml-auto shrink-0\">{badge}</span>}\n </>\n )}\n </NavLink>\n );\n\n if (isCollapsed && label) {\n return (\n <Tooltip>\n <TooltipTrigger render={link} />\n <TooltipContent side=\"right\">{label}</TooltipContent>\n </Tooltip>\n );\n }\n\n return link;\n};\nSidebarNavLink.displayName = 'SidebarNavLink';\n\n// ─── SidebarMenuCollapsible — nhóm có sub-items ───────────────────────────────\n\n/** Props for a collapsible sidebar menu group with sub-items */\nexport interface SidebarMenuCollapsibleProps {\n /** Unique identifier for the group */\n id: string;\n /** Icon displayed next to the group label */\n icon: React.ReactNode;\n /** Display text for the collapsible group header */\n label: string;\n children: React.ReactNode;\n /** Whether the group is initially expanded */\n defaultOpen?: boolean;\n /** When true, the group auto-expands and shows an active indicator */\n isChildActive?: boolean;\n}\n\nexport const SidebarMenuCollapsible: React.FC<SidebarMenuCollapsibleProps> = ({\n icon,\n label,\n children,\n defaultOpen = false,\n isChildActive = false,\n}) => {\n const { state } = useSidebar();\n const isCollapsed = state === 'collapsed';\n\n const [isOpen, setIsOpen] = React.useState(defaultOpen || isChildActive);\n const prevOpenRef = React.useRef(isOpen);\n\n // Khi sidebar collapse → đóng tất cả sub-menu, ghi nhớ state\n // Khi sidebar expand → khôi phục state cũ\n React.useEffect(() => {\n if (isCollapsed) {\n prevOpenRef.current = isOpen;\n setIsOpen(false);\n } else {\n setIsOpen(prevOpenRef.current);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isCollapsed]);\n\n // Khi có child active, mở group\n React.useEffect(() => {\n if (isChildActive && !isCollapsed) {\n setIsOpen(true);\n prevOpenRef.current = true;\n }\n }, [isChildActive, isCollapsed]);\n\n const trigger = (\n <button\n type=\"button\"\n aria-expanded={isOpen}\n data-active={isChildActive && isCollapsed}\n onClick={() => {\n if (!isCollapsed) {\n const next = !isOpen;\n setIsOpen(next);\n prevOpenRef.current = next;\n }\n }}\n className={menuButtonVariants({\n collapsed: isCollapsed,\n className:\n isChildActive && isCollapsed\n ? 'text-sidebar-accent-foreground'\n : 'text-sidebar-foreground/70',\n })}\n >\n <span className=\"shrink-0 flex h-4 w-4 items-center justify-center\">{icon}</span>\n {!isCollapsed && (\n <>\n <span className=\"flex-1 truncate text-left\">{label}</span>\n <ChevronRight\n className={cn(\n 'ml-auto h-3.5 w-3.5 shrink-0 text-sidebar-foreground/40',\n 'motion-safe:transition-transform motion-safe:duration-200',\n isOpen && 'rotate-90'\n )}\n />\n </>\n )}\n </button>\n );\n\n return (\n <>\n {isCollapsed ? (\n <Tooltip>\n <TooltipTrigger render={trigger} />\n <TooltipContent side=\"right\">{label}</TooltipContent>\n </Tooltip>\n ) : (\n trigger\n )}\n\n {/* Sub-items với animation mượt - Sử dụng SidebarMenuSub (ul) để hợp lệ HTML */}\n <SidebarMenuSub\n className={cn(\n 'overflow-hidden motion-safe:transition-all motion-safe:duration-200 motion-safe:ease-in-out',\n !isCollapsed && isOpen ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'\n )}\n >\n {children}\n </SidebarMenuSub>\n </>\n );\n};\nSidebarMenuCollapsible.displayName = 'SidebarMenuCollapsible';\n\n// ─── SidebarMenuSub ───────────────────────────────────────────────────────────\n\nexport const SidebarMenuSub = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\n ({ className, ...props }, ref) => {\n const { state } = useSidebar();\n if (state === 'collapsed') return null;\n return (\n <ul\n ref={ref}\n data-sidebar=\"menu-sub\"\n className={cn('mx-3.5 flex min-w-0 flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 list-none', className)}\n {...props}\n />\n );\n }\n);\nSidebarMenuSub.displayName = 'SidebarMenuSub';\n\nexport const SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\n (props, ref) => <li ref={ref} {...props} />\n);\nSidebarMenuSubItem.displayName = 'SidebarMenuSubItem';\n\n// ─── Badge & Skeleton ─────────────────────────────────────────────────────────\n\nexport const SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\n ({ className, ...props }, ref) => (\n <div\n ref={ref}\n data-sidebar=\"menu-badge\"\n className={cn('ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-primary/10 px-1 text-xs font-medium text-primary', className)}\n {...props}\n />\n )\n);\nSidebarMenuBadge.displayName = 'SidebarMenuBadge';\n\nexport const SidebarMenuSkeleton: React.FC<{ showIcon?: boolean }> = ({ showIcon = true }) => (\n <div className=\"flex h-9 items-center gap-2 rounded-md px-2\">\n {showIcon && <div className=\"h-4 w-4 rounded bg-sidebar-accent motion-safe:animate-pulse shrink-0\" />}\n <div className=\"h-4 flex-1 rounded bg-sidebar-accent motion-safe:animate-pulse\" />\n </div>\n);\n"
917
917
  },
918
918
  {
919
919
  "path": "src/components/ui/sidebar/SidebarUserMenu.tsx",
920
- "content": "import * as React from 'react';\r\nimport { ChevronsUpDown } from 'lucide-react';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../tooltip/Tooltip';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport { useSidebar } from './SidebarContext';\r\nimport { menuButtonVariants } from './SidebarMenu';\r\n\r\n// ─── User Menu Popover (shadcn style) ─────────────────────────────────────────\r\n\r\ninterface UserMenuPopoverProps {\r\n name: string;\r\n email: string;\r\n avatar?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nexport const UserMenuPopover: React.FC<UserMenuPopoverProps> = ({ name, email, avatar, children }) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n const [open, setOpen] = React.useState(false);\r\n\r\n const trigger = (\r\n <BasePopover.Trigger\r\n render={\r\n <button\r\n type=\"button\"\r\n data-active={open}\r\n className={cn(\r\n menuButtonVariants({ size: 'lg', collapsed: isCollapsed }),\r\n 'data-[active=true]:bg-sidebar-accent'\r\n )}\r\n >\r\n <img\r\n src={avatar || 'https://i.pravatar.cc/100'}\r\n alt={name}\r\n className=\"w-8 h-8 rounded-lg shrink-0 object-cover border border-sidebar-border\"\r\n />\r\n {!isCollapsed && (\r\n <>\r\n <div className=\"flex-1 text-left overflow-hidden grid\">\r\n <span className=\"text-sm font-semibold truncate leading-tight\">{name}</span>\r\n <span className=\"text-xs text-sidebar-foreground/50 truncate leading-tight\">{email}</span>\r\n </div>\r\n <ChevronsUpDown className=\"ml-auto h-4 w-4 shrink-0 text-sidebar-foreground/40\" />\r\n </>\r\n )}\r\n </button>\r\n }\r\n />\r\n );\r\n\r\n return (\r\n <BasePopover.Root open={open} onOpenChange={setOpen}>\r\n {isCollapsed ? (\r\n <Tooltip>\r\n <TooltipTrigger render={trigger} />\r\n <TooltipContent side=\"right\">{name}</TooltipContent>\r\n </Tooltip>\r\n ) : (\r\n trigger\r\n )}\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner side=\"right\" align=\"end\" sideOffset={8}>\r\n <BasePopover.Popup\r\n className={cn(\r\n 'z-50 w-64 rounded-xl border border-border bg-popover shadow-xl outline-none p-1',\r\n 'motion-safe:data-open:animate-in motion-safe:data-open:fade-in-0 motion-safe:data-open:zoom-in-95',\r\n 'motion-safe:data-closed:animate-out motion-safe:data-closed:fade-out-0 motion-safe:data-closed:zoom-out-95'\r\n )}\r\n >\r\n {/* User info header */}\r\n <div className=\"flex items-center gap-3 p-3 pb-2 border-b border-border/50\">\r\n <img\r\n src={avatar || 'https://i.pravatar.cc/100'}\r\n alt={name}\r\n className=\"w-10 h-10 rounded-lg object-cover border border-border\"\r\n />\r\n <div className=\"flex-1 overflow-hidden\">\r\n <p className=\"text-sm font-semibold truncate\">{name}</p>\r\n <p className=\"text-xs text-muted-foreground truncate\">{email}</p>\r\n </div>\r\n </div>\r\n\r\n {/* Menu items */}\r\n <div className=\"py-1\">{children}</div>\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n </BasePopover.Root>\r\n );\r\n};\r\n\r\n// ─── UserMenuItem (item trong popover) ───────────────────────────────────────\r\n\r\ninterface UserMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n icon?: React.ReactNode;\r\n destructive?: boolean;\r\n}\r\n\r\nexport const UserMenuItem: React.FC<UserMenuItemProps> = ({ icon, children, destructive, className, ...props }) => (\r\n <button\r\n type=\"button\"\r\n className={cn(\r\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm motion-safe:transition-colors',\r\n 'hover:bg-muted outline-none focus-visible:bg-muted',\r\n destructive ? 'text-destructive hover:text-destructive' : 'text-foreground',\r\n className\r\n )}\r\n {...props}\r\n >\r\n {icon && <span className=\"shrink-0 h-4 w-4 flex items-center justify-center\">{icon}</span>}\r\n {children}\r\n </button>\r\n);\r\n"
920
+ "content": "import * as React from 'react';\nimport { ChevronsUpDown } from 'lucide-react';\nimport { Popover as BasePopover } from '@base-ui/react';\nimport { Tooltip, TooltipTrigger, TooltipContent } from '../tooltip/Tooltip';\nimport { cn } from '@/lib/utils/cn';\nimport { useSidebar } from './SidebarContext';\nimport { menuButtonVariants } from './SidebarMenu';\n\n// ─── User Menu Popover (shadcn style) ─────────────────────────────────────────\n\ninterface UserMenuPopoverProps {\n name: string;\n email: string;\n avatar?: string;\n children?: React.ReactNode;\n}\n\nexport const UserMenuPopover: React.FC<UserMenuPopoverProps> = ({ name, email, avatar, children }) => {\n const { state } = useSidebar();\n const isCollapsed = state === 'collapsed';\n const [open, setOpen] = React.useState(false);\n\n const trigger = (\n <BasePopover.Trigger\n render={\n <button\n type=\"button\"\n data-active={open}\n className={cn(\n menuButtonVariants({ size: 'lg', collapsed: isCollapsed }),\n 'data-[active=true]:bg-sidebar-accent'\n )}\n >\n <img\n src={avatar || 'https://i.pravatar.cc/100'}\n alt={name}\n className=\"w-8 h-8 rounded-lg shrink-0 object-cover border border-sidebar-border\"\n />\n {!isCollapsed && (\n <>\n <div className=\"flex-1 text-left overflow-hidden grid\">\n <span className=\"text-sm font-semibold truncate leading-tight\">{name}</span>\n <span className=\"text-xs text-sidebar-foreground/50 truncate leading-tight\">{email}</span>\n </div>\n <ChevronsUpDown className=\"ml-auto h-4 w-4 shrink-0 text-sidebar-foreground/40\" />\n </>\n )}\n </button>\n }\n />\n );\n\n return (\n <BasePopover.Root open={open} onOpenChange={setOpen}>\n {isCollapsed ? (\n <Tooltip>\n <TooltipTrigger render={trigger} />\n <TooltipContent side=\"right\">{name}</TooltipContent>\n </Tooltip>\n ) : (\n trigger\n )}\n <BasePopover.Portal>\n <BasePopover.Positioner side=\"right\" align=\"end\" sideOffset={8}>\n <BasePopover.Popup\n className={cn(\n 'z-50 w-64 rounded-xl border border-border bg-popover shadow-xl outline-none p-1',\n 'motion-safe:data-open:animate-in motion-safe:data-open:fade-in-0 motion-safe:data-open:zoom-in-95',\n 'motion-safe:data-closed:animate-out motion-safe:data-closed:fade-out-0 motion-safe:data-closed:zoom-out-95'\n )}\n >\n {/* User info header */}\n <div className=\"flex items-center gap-3 p-3 pb-2 border-b border-border/50\">\n <img\n src={avatar || 'https://i.pravatar.cc/100'}\n alt={name}\n className=\"w-10 h-10 rounded-lg object-cover border border-border\"\n />\n <div className=\"flex-1 overflow-hidden\">\n <p className=\"text-sm font-semibold truncate\">{name}</p>\n <p className=\"text-xs text-muted-foreground truncate\">{email}</p>\n </div>\n </div>\n\n {/* Menu items */}\n <div className=\"py-1\">{children}</div>\n </BasePopover.Popup>\n </BasePopover.Positioner>\n </BasePopover.Portal>\n </BasePopover.Root>\n );\n};\n\n// ─── UserMenuItem (item trong popover) ───────────────────────────────────────\n\ninterface UserMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n icon?: React.ReactNode;\n destructive?: boolean;\n}\n\nexport const UserMenuItem: React.FC<UserMenuItemProps> = ({ icon, children, destructive, className, ...props }) => (\n <button\n type=\"button\"\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm motion-safe:transition-colors',\n 'hover:bg-muted outline-none focus-visible:bg-muted',\n destructive ? 'text-destructive hover:text-destructive' : 'text-foreground',\n className\n )}\n {...props}\n >\n {icon && <span className=\"shrink-0 h-4 w-4 flex items-center justify-center\">{icon}</span>}\n {children}\n </button>\n);\n"
921
921
  }
922
922
  ]
923
923
  },
@@ -944,7 +944,7 @@
944
944
  "files": [
945
945
  {
946
946
  "path": "src/components/ui/slider/Slider.tsx",
947
- "content": "import * as React from 'react';\r\nimport { Slider as BaseSlider } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst sliderVariants = tv({\r\n slots: {\r\n root: 'relative flex w-full touch-none select-none items-center py-4 data-disabled:opacity-50 data-disabled:cursor-not-allowed',\r\n control: 'relative flex w-full items-center',\r\n track: 'relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary data-disabled:bg-muted',\r\n indicator: 'absolute h-full bg-primary data-disabled:bg-muted-foreground/30',\r\n thumb: 'block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 data-disabled:border-muted-foreground data-disabled:bg-muted data-disabled:pointer-events-none',\r\n }\r\n});\r\n\r\nconst { root, control, track, indicator, thumb } = sliderVariants();\r\n\r\n/** Props for the Slider component */\r\nexport interface SliderProps extends React.ComponentPropsWithoutRef<typeof BaseSlider.Root> {\r\n className?: string;\r\n /** Hiển thị tooltip số khi kéo / hover thumb */\r\n showTooltip?: boolean;\r\n}\r\n\r\nconst toArray = (v: unknown): number[] => {\r\n if (Array.isArray(v)) return v as number[];\r\n if (typeof v === 'number') return [v];\r\n return [0];\r\n};\r\n\r\nconst Slider = React.forwardRef<React.ElementRef<typeof BaseSlider.Root>, SliderProps>(\r\n ({ className, showTooltip, value: valueProp, defaultValue, onValueChange, ...props }, ref) => {\r\n const isControlled = valueProp !== undefined;\r\n const [internalValues, setInternalValues] = React.useState<number[]>(\r\n () => toArray(isControlled ? valueProp : defaultValue)\r\n );\r\n\r\n // Sync khi prop value thay đổi từ bên ngoài (controlled)\r\n React.useEffect(() => {\r\n if (isControlled) setInternalValues(toArray(valueProp));\r\n }, [isControlled, valueProp]);\r\n\r\n const currentValues = isControlled ? toArray(valueProp) : internalValues;\r\n\r\n const handleValueChange: NonNullable<SliderProps['onValueChange']> = (newValues, eventDetails) => {\r\n if (!isControlled) setInternalValues(toArray(newValues));\r\n onValueChange?.(newValues, eventDetails);\r\n };\r\n\r\n return (\r\n <BaseSlider.Root\r\n ref={ref}\r\n className={root({ className })}\r\n aria-label={props['aria-label'] ?? 'Slider'}\r\n value={valueProp}\r\n defaultValue={defaultValue}\r\n onValueChange={handleValueChange}\r\n {...props}\r\n >\r\n <BaseSlider.Control className={control()}>\r\n <BaseSlider.Track className={track()}>\r\n <BaseSlider.Indicator className={indicator()} />\r\n </BaseSlider.Track>\r\n\r\n {currentValues.map((val, index) => (\r\n <BaseSlider.Thumb\r\n key={index}\r\n className={cn(thumb(), showTooltip && 'relative group')}\r\n >\r\n {showTooltip && (\r\n <span className=\"\r\n absolute -top-9 left-1/2 -translate-x-1/2\r\n min-w-[28px] text-center\r\n bg-primary text-primary-foreground\r\n text-xs font-medium leading-none\r\n px-1.5 py-1 rounded shadow-sm\r\n opacity-0 group-hover:opacity-100 group-data-[dragging]:opacity-100\r\n transition-opacity duration-150\r\n pointer-events-none select-none\r\n \">\r\n {val}\r\n </span>\r\n )}\r\n </BaseSlider.Thumb>\r\n ))}\r\n </BaseSlider.Control>\r\n </BaseSlider.Root>\r\n );\r\n }\r\n);\r\nSlider.displayName = 'Slider';\r\n\r\nexport { Slider };\r\n"
947
+ "content": "import * as React from 'react';\nimport { Slider as BaseSlider } from '@base-ui/react';\nimport { tv, type VariantProps } from 'tailwind-variants';\nimport { cn } from '@/lib/utils/cn';\n\nconst sliderVariants = tv({\n slots: {\n root: 'relative flex w-full touch-none select-none items-center py-4 data-disabled:opacity-50 data-disabled:cursor-not-allowed',\n control: 'relative flex w-full items-center',\n track: 'relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary data-disabled:bg-muted',\n indicator: 'absolute h-full bg-primary data-disabled:bg-muted-foreground/30',\n thumb: 'block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 data-disabled:border-muted-foreground data-disabled:bg-muted data-disabled:pointer-events-none',\n }\n});\n\nconst { root, control, track, indicator, thumb } = sliderVariants();\n\n/** Props for the Slider component */\nexport interface SliderProps extends React.ComponentPropsWithoutRef<typeof BaseSlider.Root> {\n className?: string;\n /** Hiển thị tooltip số khi kéo / hover thumb */\n showTooltip?: boolean;\n}\n\nconst toArray = (v: unknown): number[] => {\n if (Array.isArray(v)) return v as number[];\n if (typeof v === 'number') return [v];\n return [0];\n};\n\nconst Slider = React.forwardRef<React.ElementRef<typeof BaseSlider.Root>, SliderProps>(\n ({ className, showTooltip, value: valueProp, defaultValue, onValueChange, ...props }, ref) => {\n const isControlled = valueProp !== undefined;\n const [internalValues, setInternalValues] = React.useState<number[]>(\n () => toArray(isControlled ? valueProp : defaultValue)\n );\n\n // Sync khi prop value thay đổi từ bên ngoài (controlled)\n React.useEffect(() => {\n if (isControlled) setInternalValues(toArray(valueProp));\n }, [isControlled, valueProp]);\n\n const currentValues = isControlled ? toArray(valueProp) : internalValues;\n\n const handleValueChange: NonNullable<SliderProps['onValueChange']> = (newValues, eventDetails) => {\n if (!isControlled) setInternalValues(toArray(newValues));\n onValueChange?.(newValues, eventDetails);\n };\n\n return (\n <BaseSlider.Root\n ref={ref}\n className={root({ className })}\n aria-label={props['aria-label'] ?? 'Slider'}\n value={valueProp}\n defaultValue={defaultValue}\n onValueChange={handleValueChange}\n {...props}\n >\n <BaseSlider.Control className={control()}>\n <BaseSlider.Track className={track()}>\n <BaseSlider.Indicator className={indicator()} />\n </BaseSlider.Track>\n\n {currentValues.map((val, index) => (\n <BaseSlider.Thumb\n key={index}\n className={cn(thumb(), showTooltip && 'relative group')}\n >\n {showTooltip && (\n <span className=\"\n absolute -top-9 left-1/2 -translate-x-1/2\n min-w-[28px] text-center\n bg-primary text-primary-foreground\n text-xs font-medium leading-none\n px-1.5 py-1 rounded shadow-sm\n opacity-0 group-hover:opacity-100 group-data-[dragging]:opacity-100\n transition-opacity duration-150\n pointer-events-none select-none\n \">\n {val}\n </span>\n )}\n </BaseSlider.Thumb>\n ))}\n </BaseSlider.Control>\n </BaseSlider.Root>\n );\n }\n);\nSlider.displayName = 'Slider';\n\nexport { Slider };\n"
948
948
  }
949
949
  ]
950
950
  },
@@ -1026,7 +1026,7 @@
1026
1026
  "files": [
1027
1027
  {
1028
1028
  "path": "src/components/ui/tabs/Tabs.tsx",
1029
- "content": "import * as React from 'react';\r\nimport { Tabs as BaseTabs } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst tabsVariants = tv({\r\n slots: {\r\n rootSlots: 'flex flex-col w-full',\r\n list: 'relative inline-flex items-center justify-start rounded-lg bg-muted p-1 text-muted-foreground w-fit',\r\n indicator: 'absolute top-1 bottom-1 left-[var(--active-tab-left)] w-[var(--active-tab-width)] rounded-md bg-background shadow-sm transition-all duration-300 ease-out z-0',\r\n trigger: 'relative z-10 inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-active:text-foreground data-active:font-semibold',\r\n panel: 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',\r\n },\r\n variants: {\r\n size: {\r\n xs: { trigger: 'px-2 py-1 text-xs' },\r\n sm: { trigger: 'px-3 py-1.5 text-sm' },\r\n md: { trigger: 'px-4 py-2 text-base' },\r\n lg: { trigger: 'px-5 py-2.5 text-lg' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'sm',\r\n },\r\n});\r\n\r\ntype TabsSize = NonNullable<VariantProps<typeof tabsVariants>['size']>;\r\n\r\nconst TabsSizeContext = React.createContext<TabsSize>('sm');\r\n\r\nconst { rootSlots, list, indicator, panel } = tabsVariants();\r\n\r\ntype TabsRootProps = React.ComponentPropsWithoutRef<typeof BaseTabs.Root>;\r\n\r\nexport interface TabsProps extends TabsRootProps {}\r\n\r\nconst Tabs = React.forwardRef<React.ElementRef<typeof BaseTabs.Root>, TabsProps>(\r\n ({ className, value: valueProp, defaultValue, onValueChange, ...props }, ref) => {\r\n const isControlled = valueProp !== undefined;\r\n const [uncontrolledValue, setUncontrolledValue] = React.useState<TabsRootProps['defaultValue']>(\r\n defaultValue ?? false,\r\n );\r\n\r\n const handleValueChange = React.useCallback<NonNullable<TabsRootProps['onValueChange']>>(\r\n (val, event) => {\r\n if (!isControlled) setUncontrolledValue(val);\r\n onValueChange?.(val, event);\r\n },\r\n [isControlled, onValueChange],\r\n );\r\n\r\n return (\r\n <BaseTabs.Root\r\n ref={ref}\r\n className={cn(rootSlots(), className)}\r\n value={isControlled ? valueProp : uncontrolledValue}\r\n onValueChange={handleValueChange}\r\n {...props}\r\n />\r\n );\r\n },\r\n);\r\nTabs.displayName = 'Tabs';\r\n\r\nexport interface TabsListProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.List> {\r\n size?: TabsSize;\r\n}\r\n\r\nconst TabsList = React.forwardRef<React.ElementRef<typeof BaseTabs.List>, TabsListProps>(\r\n ({ className, size = 'sm', children, ...props }, ref) => (\r\n <TabsSizeContext.Provider value={size}>\r\n <BaseTabs.List ref={ref} className={cn(list(), className)} {...props}>\r\n <BaseTabs.Indicator className={indicator()} />\r\n {children}\r\n </BaseTabs.List>\r\n </TabsSizeContext.Provider>\r\n ),\r\n);\r\nTabsList.displayName = 'TabsList';\r\n\r\nexport interface TabsTriggerProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.Tab> {}\r\n\r\nconst TabsTrigger = React.forwardRef<React.ElementRef<typeof BaseTabs.Tab>, TabsTriggerProps>(\r\n ({ className, ...props }, ref) => {\r\n const size = React.useContext(TabsSizeContext);\r\n const { trigger } = tabsVariants({ size });\r\n return (\r\n <BaseTabs.Tab ref={ref} className={cn(trigger(), className)} {...props} />\r\n );\r\n },\r\n);\r\nTabsTrigger.displayName = 'TabsTrigger';\r\n\r\nexport interface TabsContentProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.Panel> {}\r\n\r\nconst TabsContent = React.forwardRef<React.ElementRef<typeof BaseTabs.Panel>, TabsContentProps>(\r\n ({ className, ...props }, ref) => (\r\n <BaseTabs.Panel ref={ref} className={cn(panel(), className)} {...props} />\r\n ),\r\n);\r\nTabsContent.displayName = 'TabsContent';\r\n\r\nexport { Tabs, TabsList, TabsTrigger, TabsContent, tabsVariants };\r\nexport type { TabsSize };\r\n"
1029
+ "content": "import * as React from 'react';\r\nimport { Tabs as BaseTabs } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst tabsVariants = tv({\r\n slots: {\r\n rootSlots: 'flex flex-col w-full',\r\n list: 'relative inline-flex items-center justify-start rounded-lg bg-muted p-1 text-muted-foreground w-fit',\r\n indicator: 'absolute top-1 bottom-1 left-[var(--active-tab-left)] w-[var(--active-tab-width)] rounded-md bg-background shadow-sm transition-all duration-300 ease-out z-0',\r\n trigger: 'relative z-10 inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-active:text-foreground data-active:font-semibold',\r\n panel: 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',\r\n },\r\n variants: {\r\n size: {\r\n xs: { trigger: 'px-2 py-1 text-xs' },\r\n sm: { trigger: 'px-3 py-1.5 text-sm' },\r\n md: { trigger: 'px-4 py-2 text-base' },\r\n lg: { trigger: 'px-5 py-2.5 text-lg' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'sm',\r\n },\r\n});\r\n\r\ntype TabsSize = NonNullable<VariantProps<typeof tabsVariants>['size']>;\r\n\r\nconst TabsSizeContext = React.createContext<TabsSize>('sm');\r\n\r\nconst { rootSlots, list, indicator, panel } = tabsVariants();\r\n\r\ntype TabsRootProps = React.ComponentPropsWithoutRef<typeof BaseTabs.Root>;\r\n\r\nexport type TabsProps = TabsRootProps\r\n\r\nconst Tabs = React.forwardRef<React.ElementRef<typeof BaseTabs.Root>, TabsProps>(\r\n ({ className, value: valueProp, defaultValue, onValueChange, ...props }, ref) => {\r\n const isControlled = valueProp !== undefined;\r\n const [uncontrolledValue, setUncontrolledValue] = React.useState<TabsRootProps['defaultValue']>(\r\n defaultValue ?? false,\r\n );\r\n\r\n const handleValueChange = React.useCallback<NonNullable<TabsRootProps['onValueChange']>>(\r\n (val, event) => {\r\n if (!isControlled) setUncontrolledValue(val);\r\n onValueChange?.(val, event);\r\n },\r\n [isControlled, onValueChange],\r\n );\r\n\r\n return (\r\n <BaseTabs.Root\r\n ref={ref}\r\n className={cn(rootSlots(), className)}\r\n value={isControlled ? valueProp : uncontrolledValue}\r\n onValueChange={handleValueChange}\r\n {...props}\r\n />\r\n );\r\n },\r\n);\r\nTabs.displayName = 'Tabs';\r\n\r\nexport interface TabsListProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.List> {\r\n size?: TabsSize;\r\n}\r\n\r\nconst TabsList = React.forwardRef<React.ElementRef<typeof BaseTabs.List>, TabsListProps>(\r\n ({ className, size = 'sm', children, ...props }, ref) => (\r\n <TabsSizeContext.Provider value={size}>\r\n <BaseTabs.List ref={ref} className={cn(list(), className)} {...props}>\r\n <BaseTabs.Indicator className={indicator()} />\r\n {children}\r\n </BaseTabs.List>\r\n </TabsSizeContext.Provider>\r\n ),\r\n);\r\nTabsList.displayName = 'TabsList';\r\n\r\nexport type TabsTriggerProps = React.ComponentPropsWithoutRef<typeof BaseTabs.Tab>\r\n\r\nconst TabsTrigger = React.forwardRef<React.ElementRef<typeof BaseTabs.Tab>, TabsTriggerProps>(\r\n ({ className, ...props }, ref) => {\r\n const size = React.useContext(TabsSizeContext);\r\n const { trigger } = tabsVariants({ size });\r\n return (\r\n <BaseTabs.Tab ref={ref} className={cn(trigger(), className)} {...props} />\r\n );\r\n },\r\n);\r\nTabsTrigger.displayName = 'TabsTrigger';\r\n\r\nexport type TabsContentProps = React.ComponentPropsWithoutRef<typeof BaseTabs.Panel>\r\n\r\nconst TabsContent = React.forwardRef<React.ElementRef<typeof BaseTabs.Panel>, TabsContentProps>(\r\n ({ className, ...props }, ref) => (\r\n <BaseTabs.Panel ref={ref} className={cn(panel(), className)} {...props} />\r\n ),\r\n);\r\nTabsContent.displayName = 'TabsContent';\r\n\r\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\r\nexport type { TabsSize };\r\n"
1030
1030
  }
1031
1031
  ]
1032
1032
  },
@@ -1040,7 +1040,7 @@
1040
1040
  "files": [
1041
1041
  {
1042
1042
  "path": "src/components/ui/textarea/Textarea.tsx",
1043
- "content": "import * as React from 'react';\r\nimport { Field as BaseField } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst textareaVariants = tv({\r\n base: 'flex min-h-[80px] w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:ring-0 placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow',\r\n variants: {\r\n variant: {\r\n default: '',\r\n filled: 'bg-accent border-transparent focus-visible:border-primary focus-visible:ring-0',\r\n }\r\n },\r\n defaultVariants: {\r\n variant: 'default'\r\n }\r\n});\r\n\r\n/** Props for the Textarea component */\r\nexport interface TextareaProps\r\n extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,\r\n VariantProps<typeof textareaVariants> {\r\n /** Label text displayed above the textarea */\r\n label?: string;\r\n /** Error message displayed below the textarea (replaces description) */\r\n error?: string;\r\n /** Helper text displayed below the textarea */\r\n description?: string;\r\n}\r\n\r\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\r\n ({ className, variant, label, error, description, ...props }, ref) => {\r\n return (\r\n <BaseField.Root className=\"flex flex-col gap-1.5 w-full\">\r\n {label && (\r\n <BaseField.Label className=\"text-sm font-medium text-foreground leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\r\n {label}\r\n </BaseField.Label>\r\n )}\r\n\r\n <BaseField.Control render={\r\n <textarea\r\n ref={ref}\r\n className={cn(\r\n textareaVariants({ variant }),\r\n error && 'border-danger focus-visible:ring-danger',\r\n className\r\n )}\r\n {...props}\r\n />\r\n } />\r\n \r\n {description && !error && (\r\n <BaseField.Description className=\"text-[0.8rem] text-muted-foreground\">\r\n {description}\r\n </BaseField.Description>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">\r\n {error}\r\n </p>\r\n )}\r\n </BaseField.Root>\r\n );\r\n }\r\n);\r\nTextarea.displayName = 'Textarea';\r\n\r\nexport { Textarea };\r\n"
1043
+ "content": "import * as React from 'react';\r\nimport { Field as BaseField } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst textareaVariants = tv({\r\n base: 'flex min-h-[80px] w-full rounded-lg border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:ring-0 placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow',\r\n variants: {\r\n variant: {\r\n default: '',\r\n filled: 'bg-accent border-transparent focus-visible:border-primary focus-visible:ring-0',\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'default',\r\n },\r\n});\r\n\r\n/** Props for the Textarea component */\r\nexport interface TextareaProps\r\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'minLength'>,\r\n VariantProps<typeof textareaVariants> {\r\n /** Label text displayed above the textarea */\r\n label?: string;\r\n /** Error message displayed below the textarea (replaces description) */\r\n error?: string;\r\n /** Helper text displayed below the textarea */\r\n description?: string;\r\n /** Show count of characters */\r\n showCount?: boolean;\r\n /** Max characters — enforced natively, displayed as count/max when showCount is true */\r\n maxLength?: number;\r\n /** Min characters — shows hint when current count is below min */\r\n minLength?: number;\r\n required?: boolean;\r\n}\r\n\r\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\r\n (\r\n {\r\n className,\r\n variant,\r\n label,\r\n error,\r\n description,\r\n showCount,\r\n maxLength,\r\n minLength,\r\n required,\r\n onChange,\r\n value,\r\n defaultValue,\r\n ...rest\r\n },\r\n ref,\r\n ) => {\r\n const isControlled = value !== undefined;\r\n\r\n const [charCount, setCharCount] = React.useState<number>(() => {\r\n if (isControlled) return String(value ?? '').length;\r\n if (defaultValue !== undefined) return String(defaultValue).length;\r\n return 0;\r\n });\r\n\r\n React.useEffect(() => {\r\n if (isControlled) setCharCount(String(value ?? '').length);\r\n }, [isControlled, value]);\r\n\r\n const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {\r\n setCharCount(event.target.value.length);\r\n onChange?.(event);\r\n };\r\n\r\n const isNearLimit = maxLength !== undefined && charCount >= Math.floor(maxLength * 0.9);\r\n const isAtLimit = maxLength !== undefined && charCount >= maxLength;\r\n const isBelowMin = minLength !== undefined && charCount > 0 && charCount < minLength;\r\n\r\n const hasFooter = error ?? description ?? isBelowMin;\r\n\r\n return (\r\n <BaseField.Root className=\"flex w-full flex-col gap-1.5\">\r\n {label && (\r\n <BaseField.Label className=\"text-sm font-medium leading-none text-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\r\n {label}\r\n {required && <span className=\"ml-0.5 text-danger\">*</span>}\r\n </BaseField.Label>\r\n )}\r\n\r\n <div className=\"relative\">\r\n <BaseField.Control\r\n render={\r\n <textarea\r\n ref={ref}\r\n className={cn(\r\n textareaVariants({ variant }),\r\n error && 'border-danger focus-visible:ring-danger',\r\n showCount && 'pb-6',\r\n className,\r\n )}\r\n value={value}\r\n defaultValue={!isControlled ? defaultValue : undefined}\r\n maxLength={maxLength}\r\n minLength={minLength}\r\n required={required}\r\n onChange={handleChange}\r\n {...rest}\r\n />\r\n }\r\n />\r\n\r\n {showCount && (\r\n <span\r\n className={cn(\r\n 'pointer-events-none absolute bottom-1 right-3 select-none text-[0.7rem] tabular-nums text-muted-foreground',\r\n isNearLimit && 'font-medium text-warning',\r\n isAtLimit && 'text-danger',\r\n )}\r\n >\r\n {charCount}\r\n {maxLength !== undefined && `/${maxLength}`}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {hasFooter && (\r\n <div className=\"flex flex-col gap-0.5\">\r\n {error ? (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\r\n ) : (\r\n <>\r\n {description && (\r\n <BaseField.Description className=\"text-[0.8rem] text-muted-foreground\">\r\n {description}\r\n </BaseField.Description>\r\n )}\r\n {isBelowMin && (\r\n <p className=\"text-[0.8rem] text-warning\">Tối thiểu {minLength} ký tự</p>\r\n )}\r\n </>\r\n )}\r\n </div>\r\n )}\r\n </BaseField.Root>\r\n );\r\n },\r\n);\r\nTextarea.displayName = 'Textarea';\r\n\r\nexport { Textarea };\r\n"
1044
1044
  }
1045
1045
  ]
1046
1046
  },
@@ -1093,7 +1093,7 @@
1093
1093
  "files": [
1094
1094
  {
1095
1095
  "path": "src/components/ui/tooltip/Tooltip.tsx",
1096
- "content": "'use client';\r\n\r\nimport * as React from 'react';\r\nimport { Tooltip as BaseTooltip } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst tooltipVariants = tv({\r\n slots: {\r\n popup: 'z-50 overflow-hidden rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n arrow: 'fill-popover',\r\n },\r\n});\r\n\r\nconst { popup, arrow } = tooltipVariants();\r\n\r\n// ─── Compound Components ─────────────────────────────────────────────────────\r\n\r\n/** Wrap multiple Tooltip instances in a shared provider for better performance */\r\nconst TooltipProvider = BaseTooltip.Provider;\r\n\r\nconst Tooltip = BaseTooltip.Root;\r\n\r\nconst TooltipTrigger = React.forwardRef<\r\n HTMLButtonElement,\r\n React.ComponentPropsWithoutRef<typeof BaseTooltip.Trigger>\r\n>(({ children, render, ...props }, ref) => (\r\n <BaseTooltip.Trigger\r\n ref={ref}\r\n render={render ?? (React.isValidElement(children) ? children : undefined)}\r\n {...props}\r\n >\r\n {React.isValidElement(children) ? undefined : children}\r\n </BaseTooltip.Trigger>\r\n));\r\nTooltipTrigger.displayName = 'TooltipTrigger';\r\n\r\nexport interface TooltipContentProps\r\n extends React.ComponentPropsWithoutRef<typeof BaseTooltip.Popup> {\r\n /** Side offset from the trigger (default: 4) */\r\n sideOffset?: number;\r\n /** Side to display the tooltip (default: 'top') */\r\n side?: 'top' | 'right' | 'bottom' | 'left';\r\n /** Alignment relative to the trigger (default: 'center') */\r\n align?: 'start' | 'center' | 'end';\r\n /** Show the arrow indicator */\r\n showArrow?: boolean;\r\n}\r\n\r\nconst TooltipContent = React.forwardRef<HTMLDivElement, TooltipContentProps>(\r\n ({ className, sideOffset = 4, side = 'top', align = 'center', showArrow = true, children, ...props }, ref) => (\r\n <BaseTooltip.Portal>\r\n <BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset}>\r\n <BaseTooltip.Popup ref={ref} className={cn(popup(), className)} role=\"tooltip\" {...props}>\r\n {showArrow && <BaseTooltip.Arrow className={arrow()} />}\r\n {children}\r\n </BaseTooltip.Popup>\r\n </BaseTooltip.Positioner>\r\n </BaseTooltip.Portal>\r\n )\r\n);\r\nTooltipContent.displayName = 'TooltipContent';\r\n\r\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, tooltipVariants };\r\n"
1096
+ "content": "'use client';\n\nimport * as React from 'react';\nimport { Tooltip as BaseTooltip } from '@base-ui/react';\nimport { tv } from 'tailwind-variants';\nimport { cn } from '@/lib/utils/cn';\n\nconst tooltipVariants = tv({\n slots: {\n popup: 'z-50 overflow-hidden rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\n arrow: 'fill-popover',\n },\n});\n\nconst { popup, arrow } = tooltipVariants();\n\n// ─── Compound Components ─────────────────────────────────────────────────────\n\n/** Wrap multiple Tooltip instances in a shared provider for better performance */\nconst TooltipProvider = BaseTooltip.Provider;\n\nconst Tooltip = BaseTooltip.Root;\n\nconst TooltipTrigger = React.forwardRef<\n HTMLButtonElement,\n React.ComponentPropsWithoutRef<typeof BaseTooltip.Trigger>\n>(({ children, render, ...props }, ref) => (\n <BaseTooltip.Trigger\n ref={ref}\n render={render ?? (React.isValidElement(children) ? children : undefined)}\n {...props}\n >\n {React.isValidElement(children) ? undefined : children}\n </BaseTooltip.Trigger>\n));\nTooltipTrigger.displayName = 'TooltipTrigger';\n\nexport interface TooltipContentProps\n extends React.ComponentPropsWithoutRef<typeof BaseTooltip.Popup> {\n /** Side offset from the trigger (default: 4) */\n sideOffset?: number;\n /** Side to display the tooltip (default: 'top') */\n side?: 'top' | 'right' | 'bottom' | 'left';\n /** Alignment relative to the trigger (default: 'center') */\n align?: 'start' | 'center' | 'end';\n /** Show the arrow indicator */\n showArrow?: boolean;\n}\n\nconst TooltipContent = React.forwardRef<HTMLDivElement, TooltipContentProps>(\n ({ className, sideOffset = 4, side = 'top', align = 'center', showArrow = true, children, ...props }, ref) => (\n <BaseTooltip.Portal>\n <BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset}>\n <BaseTooltip.Popup ref={ref} className={cn(popup(), className)} role=\"tooltip\" {...props}>\n {showArrow && <BaseTooltip.Arrow className={arrow()} />}\n {children}\n </BaseTooltip.Popup>\n </BaseTooltip.Positioner>\n </BaseTooltip.Portal>\n )\n);\nTooltipContent.displayName = 'TooltipContent';\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, tooltipVariants };\n"
1097
1097
  }
1098
1098
  ]
1099
1099
  },