@srcroot/ui 0.0.2 → 0.0.3

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.
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"
6
6
  // HoverCard Context
7
7
  interface HoverCardContextValue {
8
8
  open: boolean
9
- triggerRef: React.RefObject<HTMLDivElement>
9
+ triggerRef: React.RefObject<HTMLDivElement | null>
10
10
  }
11
11
 
12
12
  const HoverCardContext = React.createContext<HoverCardContextValue | null>(null)
@@ -69,8 +69,8 @@ const Image = React.forwardRef<HTMLImageElement, ImageProps>(
69
69
  }, [src])
70
70
 
71
71
  const containerStyle: React.CSSProperties = aspectRatio
72
- ? { paddingBottom: `${100 / aspectRatio}%`, ...style }
73
- : style
72
+ ? { paddingBottom: `${100 / aspectRatio}%`, ...(style || {}) }
73
+ : (style || {})
74
74
 
75
75
  // Render fallback
76
76
  if (status === "error" && fallback) {
@@ -22,7 +22,7 @@ function useMenubar() {
22
22
  // MenubarMenu Context
23
23
  interface MenubarMenuContextValue {
24
24
  menuId: string
25
- triggerRef: React.RefObject<HTMLButtonElement>
25
+ triggerRef: React.RefObject<HTMLButtonElement | null>
26
26
  }
27
27
 
28
28
  const MenubarMenuContext = React.createContext<MenubarMenuContextValue | null>(null)
@@ -9,6 +9,7 @@ interface ResizablePanelGroupContextValue {
9
9
  sizes: number[]
10
10
  setSizes: React.Dispatch<React.SetStateAction<number[]>>
11
11
  registerPanel: () => number
12
+ getPanelCount: () => number
12
13
  }
13
14
 
14
15
  const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
@@ -32,12 +33,21 @@ const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroup
32
33
  const [sizes, setSizes] = React.useState<number[]>([])
33
34
  const panelCountRef = React.useRef(0)
34
35
 
36
+ // Reset panel count on each render to handle re-renders properly
37
+ React.useLayoutEffect(() => {
38
+ panelCountRef.current = 0
39
+ })
40
+
35
41
  const registerPanel = React.useCallback(() => {
36
42
  const index = panelCountRef.current
37
43
  panelCountRef.current += 1
38
44
  return index
39
45
  }, [])
40
46
 
47
+ const getPanelCount = React.useCallback(() => {
48
+ return panelCountRef.current
49
+ }, [])
50
+
41
51
  // Notify layout changes
42
52
  React.useEffect(() => {
43
53
  if (sizes.length > 0) {
@@ -46,7 +56,7 @@ const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroup
46
56
  }, [sizes, onLayout])
47
57
 
48
58
  return (
49
- <ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, registerPanel }}>
59
+ <ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, registerPanel, getPanelCount }}>
50
60
  <div
51
61
  ref={ref}
52
62
  data-panel-group
@@ -76,32 +86,30 @@ interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
76
86
  const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
77
87
  ({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
78
88
  const { direction, sizes, setSizes, registerPanel } = useResizablePanelGroup()
79
- const indexRef = React.useRef<number | null>(null)
89
+ const [index] = React.useState(() => registerPanel())
80
90
 
81
- React.useEffect(() => {
82
- if (indexRef.current === null) {
83
- indexRef.current = registerPanel()
84
- setSizes(prev => {
85
- const newSizes = [...prev]
86
- newSizes[indexRef.current!] = defaultSize
87
- return newSizes
88
- })
89
- }
90
- }, [registerPanel, setSizes, defaultSize])
91
+ // Initialize size
92
+ React.useLayoutEffect(() => {
93
+ setSizes(prev => {
94
+ const newSizes = [...prev]
95
+ if (newSizes[index] === undefined) {
96
+ newSizes[index] = defaultSize
97
+ }
98
+ return newSizes
99
+ })
100
+ }, [index, defaultSize, setSizes])
91
101
 
92
- const size = indexRef.current !== null ? sizes[indexRef.current] ?? defaultSize : defaultSize
102
+ const size = sizes[index] ?? defaultSize
93
103
 
94
104
  return (
95
105
  <div
96
106
  ref={ref}
97
107
  data-panel
98
- data-panel-index={indexRef.current}
108
+ data-panel-index={index}
99
109
  className={cn("overflow-hidden", className)}
100
110
  style={{
101
111
  ...style,
102
- [direction === "horizontal" ? "width" : "height"]: `${size}%`,
103
- flexShrink: 0,
104
- flexGrow: 0,
112
+ flex: `0 0 ${size}%`,
105
113
  }}
106
114
  {...props}
107
115
  >
@@ -119,7 +127,7 @@ interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
119
127
 
120
128
  const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
121
129
  ({ className, withHandle = false, ...props }, ref) => {
122
- const { direction, sizes, setSizes } = useResizablePanelGroup()
130
+ const { direction, setSizes } = useResizablePanelGroup()
123
131
  const [isDragging, setIsDragging] = React.useState(false)
124
132
  const handleRef = React.useRef<HTMLDivElement>(null)
125
133
 
@@ -127,29 +135,59 @@ const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
127
135
  e.preventDefault()
128
136
  setIsDragging(true)
129
137
 
130
- const startX = e.clientX
131
- const startY = e.clientY
132
- const startSizes = [...sizes]
133
- const container = handleRef.current?.parentElement
138
+ const startPos = direction === "horizontal" ? e.clientX : e.clientY
139
+ const handle = handleRef.current
140
+ if (!handle) return
141
+
142
+ // Find adjacent panels
143
+ const prevPanel = handle.previousElementSibling as HTMLElement
144
+ const nextPanel = handle.nextElementSibling as HTMLElement
145
+ if (!prevPanel || !nextPanel) return
134
146
 
135
- if (!container) return
147
+ const prevIndex = parseInt(prevPanel.dataset.panelIndex || "0")
148
+ const nextIndex = parseInt(nextPanel.dataset.panelIndex || "1")
136
149
 
137
- const containerRect = container.getBoundingClientRect()
138
- const containerSize = direction === "horizontal" ? containerRect.width : containerRect.height
150
+ // Get current sizes
151
+ const prevRect = prevPanel.getBoundingClientRect()
152
+ const nextRect = nextPanel.getBoundingClientRect()
153
+ const totalSize = direction === "horizontal"
154
+ ? prevRect.width + nextRect.width
155
+ : prevRect.height + nextRect.height
139
156
 
140
157
  const handleMouseMove = (moveEvent: MouseEvent) => {
141
- const delta = direction === "horizontal"
142
- ? moveEvent.clientX - startX
143
- : moveEvent.clientY - startY
144
- const deltaPercent = (delta / containerSize) * 100
158
+ const currentPos = direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
159
+ const delta = currentPos - startPos
160
+ const deltaPercent = (delta / totalSize) * 100
145
161
 
146
162
  setSizes(prev => {
147
163
  const newSizes = [...prev]
148
- // Adjust first two panels (simple implementation)
149
- if (newSizes.length >= 2) {
150
- newSizes[0] = Math.max(10, Math.min(90, startSizes[0] + deltaPercent))
151
- newSizes[1] = Math.max(10, Math.min(90, startSizes[1] - deltaPercent))
164
+ const prevSize = prev[prevIndex] ?? 50
165
+ const nextSize = prev[nextIndex] ?? 50
166
+
167
+ // Calculate new sizes with constraints
168
+ let newPrevSize = prevSize + deltaPercent
169
+ let newNextSize = nextSize - deltaPercent
170
+
171
+ // Apply min/max constraints
172
+ if (newPrevSize < 10) {
173
+ newPrevSize = 10
174
+ newNextSize = prevSize + nextSize - 10
152
175
  }
176
+ if (newNextSize < 10) {
177
+ newNextSize = 10
178
+ newPrevSize = prevSize + nextSize - 10
179
+ }
180
+ if (newPrevSize > 90) {
181
+ newPrevSize = 90
182
+ newNextSize = prevSize + nextSize - 90
183
+ }
184
+ if (newNextSize > 90) {
185
+ newNextSize = 90
186
+ newPrevSize = prevSize + nextSize - 90
187
+ }
188
+
189
+ newSizes[prevIndex] = newPrevSize
190
+ newSizes[nextIndex] = newNextSize
153
191
  return newSizes
154
192
  })
155
193
  }
@@ -3,17 +3,47 @@ import { cn } from "@/lib/utils"
3
3
 
4
4
  interface ScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  /** Orientation of scrollbar */
6
- orientation?: "vertical" | "horizontal" | "both"
6
+ orientation?: "vertical" | "horizontal"
7
+ /** Scrollbar size: "thin" (4px), "default" (8px), "thick" (12px) */
8
+ scrollbarSize?: "thin" | "default" | "thick"
9
+ /** Hide scrollbar until hover */
10
+ hideScrollbar?: boolean
11
+ }
12
+
13
+ // CSS for custom scrollbar styling
14
+ const scrollbarStyles = {
15
+ thin: {
16
+ width: "4px",
17
+ height: "4px",
18
+ },
19
+ default: {
20
+ width: "8px",
21
+ height: "8px",
22
+ },
23
+ thick: {
24
+ width: "12px",
25
+ height: "12px",
26
+ },
7
27
  }
8
28
 
9
29
  /**
10
30
  * ScrollArea - Custom scrollbar container
11
31
  *
12
32
  * Provides a styled scrollbar that is thin and consistent across browsers.
13
- * Uses CSS scrollbar styling for webkit and Firefox.
33
+ * Supports customizable scrollbar size and orientation.
14
34
  */
15
35
  const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
16
- ({ className, children, orientation = "vertical", ...props }, ref) => {
36
+ ({ className, children, orientation = "vertical", scrollbarSize = "thin", hideScrollbar = false, style, ...props }, ref) => {
37
+ const sizes = scrollbarStyles[scrollbarSize]
38
+
39
+ const scrollbarCSS: React.CSSProperties = {
40
+ ...style,
41
+ // Webkit browsers (Chrome, Safari, Edge)
42
+ // @ts-ignore - CSS custom properties for scrollbar
43
+ "--scrollbar-width": sizes.width,
44
+ "--scrollbar-height": sizes.height,
45
+ }
46
+
17
47
  return (
18
48
  <div
19
49
  ref={ref}
@@ -22,13 +52,42 @@ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
22
52
  // Container overflow based on orientation
23
53
  orientation === "vertical" && "overflow-y-auto overflow-x-hidden",
24
54
  orientation === "horizontal" && "overflow-x-auto overflow-y-hidden",
25
- orientation === "both" && "overflow-auto",
26
- // Custom scrollbar styling
27
- "scrollbar-thin",
55
+ // Custom scrollbar classes
56
+ "scrollbar-custom",
57
+ hideScrollbar && "scrollbar-hide hover:scrollbar-show",
28
58
  className
29
59
  )}
60
+ style={scrollbarCSS}
30
61
  {...props}
31
62
  >
63
+ <style>{`
64
+ .scrollbar-custom::-webkit-scrollbar {
65
+ width: var(--scrollbar-width, 4px);
66
+ height: var(--scrollbar-height, 4px);
67
+ }
68
+ .scrollbar-custom::-webkit-scrollbar-track {
69
+ background: transparent;
70
+ border-radius: 9999px;
71
+ }
72
+ .scrollbar-custom::-webkit-scrollbar-thumb {
73
+ background: hsl(var(--muted-foreground) / 0.3);
74
+ border-radius: 9999px;
75
+ }
76
+ .scrollbar-custom::-webkit-scrollbar-thumb:hover {
77
+ background: hsl(var(--muted-foreground) / 0.5);
78
+ }
79
+ .scrollbar-custom {
80
+ scrollbar-width: thin;
81
+ scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
82
+ }
83
+ .scrollbar-hide::-webkit-scrollbar {
84
+ opacity: 0;
85
+ }
86
+ .scrollbar-hide:hover::-webkit-scrollbar,
87
+ .scrollbar-show::-webkit-scrollbar {
88
+ opacity: 1;
89
+ }
90
+ `}</style>
32
91
  {children}
33
92
  </div>
34
93
  )
@@ -36,7 +95,7 @@ const ScrollArea = React.forwardRef<HTMLDivElement, ScrollAreaProps>(
36
95
  )
37
96
  ScrollArea.displayName = "ScrollArea"
38
97
 
39
- // ScrollBar component for explicit scrollbar styling reference
98
+ // ScrollBar component for explicit scrollbar styling reference (optional usage)
40
99
  interface ScrollBarProps extends React.HTMLAttributes<HTMLDivElement> {
41
100
  orientation?: "vertical" | "horizontal"
42
101
  }
@@ -18,20 +18,7 @@ interface SheetProps {
18
18
  }
19
19
 
20
20
  /**
21
- * Sheet (slide-in panel) component
22
- *
23
- * @example
24
- * <Sheet>
25
- * <SheetTrigger asChild>
26
- * <Button>Open Sheet</Button>
27
- * </SheetTrigger>
28
- * <SheetContent side="right">
29
- * <SheetHeader>
30
- * <SheetTitle>Title</SheetTitle>
31
- * <SheetDescription>Description</SheetDescription>
32
- * </SheetHeader>
33
- * </SheetContent>
34
- * </Sheet>
21
+ * Sheet (slide-in panel) component with smooth animations
35
22
  */
36
23
  function Sheet({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: SheetProps) {
37
24
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
@@ -77,7 +64,7 @@ const SheetTrigger = React.forwardRef<HTMLButtonElement, SheetTriggerProps>(
77
64
  SheetTrigger.displayName = "SheetTrigger"
78
65
 
79
66
  const sheetVariants = cva(
80
- "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out",
67
+ "fixed z-50 gap-4 bg-background p-6 shadow-lg",
81
68
  {
82
69
  variants: {
83
70
  side: {
@@ -93,6 +80,26 @@ const sheetVariants = cva(
93
80
  }
94
81
  )
95
82
 
83
+ // Animation classes for each side
84
+ const animationClasses = {
85
+ top: {
86
+ open: "translate-y-0",
87
+ closed: "-translate-y-full",
88
+ },
89
+ bottom: {
90
+ open: "translate-y-0",
91
+ closed: "translate-y-full",
92
+ },
93
+ left: {
94
+ open: "translate-x-0",
95
+ closed: "-translate-x-full",
96
+ },
97
+ right: {
98
+ open: "translate-x-0",
99
+ closed: "translate-x-full",
100
+ },
101
+ }
102
+
96
103
  interface SheetContentProps
97
104
  extends React.HTMLAttributes<HTMLDivElement>,
98
105
  VariantProps<typeof sheetVariants> { }
@@ -102,6 +109,29 @@ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
102
109
  const context = React.useContext(SheetContext)
103
110
  if (!context) throw new Error("SheetContent must be used within Sheet")
104
111
 
112
+ const [isVisible, setIsVisible] = React.useState(false)
113
+ const [isAnimating, setIsAnimating] = React.useState(false)
114
+
115
+ React.useEffect(() => {
116
+ if (context.open) {
117
+ // First make visible (off-screen)
118
+ setIsVisible(true)
119
+ // Use a small timeout to ensure the browser has painted the initial state
120
+ const timer = setTimeout(() => {
121
+ setIsAnimating(true)
122
+ }, 10)
123
+ return () => clearTimeout(timer)
124
+ } else {
125
+ // Start close animation
126
+ setIsAnimating(false)
127
+ // Wait for animation to complete before hiding
128
+ const timer = setTimeout(() => {
129
+ setIsVisible(false)
130
+ }, 300)
131
+ return () => clearTimeout(timer)
132
+ }
133
+ }, [context.open])
134
+
105
135
  React.useEffect(() => {
106
136
  const handleEscape = (e: KeyboardEvent) => {
107
137
  if (e.key === "Escape") {
@@ -120,19 +150,33 @@ const SheetContent = React.forwardRef<HTMLDivElement, SheetContentProps>(
120
150
  }
121
151
  }, [context.open, context])
122
152
 
123
- if (!context.open) return null
153
+ if (!isVisible) return null
154
+
155
+ const sideKey = side || "right"
124
156
 
125
157
  return (
126
158
  <>
159
+ {/* Overlay with fade animation */}
127
160
  <div
128
- className="fixed inset-0 z-50 bg-black/80"
161
+ className={cn(
162
+ "fixed inset-0 z-50 bg-black/80 transition-opacity duration-300",
163
+ isAnimating ? "opacity-100" : "opacity-0"
164
+ )}
129
165
  onClick={() => context.onOpenChange(false)}
130
166
  />
167
+ {/* Sheet content with slide animation */}
131
168
  <div
132
169
  ref={ref}
133
170
  role="dialog"
134
171
  aria-modal="true"
135
- className={cn(sheetVariants({ side }), className)}
172
+ className={cn(
173
+ sheetVariants({ side }),
174
+ "transition-transform duration-300 ease-out",
175
+ isAnimating
176
+ ? animationClasses[sideKey].open
177
+ : animationClasses[sideKey].closed,
178
+ className
179
+ )}
136
180
  {...props}
137
181
  >
138
182
  {children}
@@ -281,6 +281,13 @@ const SidebarInset = React.forwardRef<
281
281
  ref={ref}
282
282
  className={cn(
283
283
  "relative flex min-h-svh flex-1 flex-col bg-background",
284
+ // Width calculation based on sidebar state
285
+ "w-full md:w-[calc(100%-var(--sidebar-width))]",
286
+ "md:peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon))]",
287
+ "md:peer-data-[collapsible=offcanvas]:peer-data-[state=collapsed]:w-full",
288
+ // Transition for smooth resize
289
+ "transition-[width] duration-200 ease-linear",
290
+ // Inset variant styles
284
291
  "peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
285
292
  className
286
293
  )}