@srcroot/ui 0.0.1 → 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.
@@ -0,0 +1,60 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ interface KbdProps extends React.HTMLAttributes<HTMLElement> {
5
+ /** Array of keys to display (e.g., ["Ctrl", "K"]) */
6
+ keys?: string[]
7
+ }
8
+
9
+ /**
10
+ * Kbd - Keyboard key display component
11
+ *
12
+ * Usage:
13
+ * <Kbd>⌘</Kbd>
14
+ * <Kbd keys={["Ctrl", "Shift", "P"]} />
15
+ */
16
+ const Kbd = React.forwardRef<HTMLElement, KbdProps>(
17
+ ({ className, children, keys, ...props }, ref) => {
18
+ // If keys array is provided, render each key
19
+ if (keys && keys.length > 0) {
20
+ return (
21
+ <span className="inline-flex items-center gap-1">
22
+ {keys.map((key, index) => (
23
+ <React.Fragment key={index}>
24
+ <kbd
25
+ ref={index === 0 ? ref : undefined}
26
+ className={cn(
27
+ "pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
28
+ className
29
+ )}
30
+ {...props}
31
+ >
32
+ {key}
33
+ </kbd>
34
+ {index < keys.length - 1 && (
35
+ <span className="text-muted-foreground text-xs">+</span>
36
+ )}
37
+ </React.Fragment>
38
+ ))}
39
+ </span>
40
+ )
41
+ }
42
+
43
+ // Single key rendering
44
+ return (
45
+ <kbd
46
+ ref={ref}
47
+ className={cn(
48
+ "pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
49
+ className
50
+ )}
51
+ {...props}
52
+ >
53
+ {children}
54
+ </kbd>
55
+ )
56
+ }
57
+ )
58
+ Kbd.displayName = "Kbd"
59
+
60
+ export { Kbd }
@@ -0,0 +1,246 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Menubar Context
7
+ interface MenubarContextValue {
8
+ activeMenu: string | null
9
+ setActiveMenu: (menu: string | null) => void
10
+ }
11
+
12
+ const MenubarContext = React.createContext<MenubarContextValue | null>(null)
13
+
14
+ function useMenubar() {
15
+ const context = React.useContext(MenubarContext)
16
+ if (!context) {
17
+ throw new Error("useMenubar must be used within a Menubar")
18
+ }
19
+ return context
20
+ }
21
+
22
+ // MenubarMenu Context
23
+ interface MenubarMenuContextValue {
24
+ menuId: string
25
+ triggerRef: React.RefObject<HTMLButtonElement | null>
26
+ }
27
+
28
+ const MenubarMenuContext = React.createContext<MenubarMenuContextValue | null>(null)
29
+
30
+ function useMenubarMenu() {
31
+ const context = React.useContext(MenubarMenuContext)
32
+ if (!context) {
33
+ throw new Error("useMenubarMenu must be used within a MenubarMenu")
34
+ }
35
+ return context
36
+ }
37
+
38
+ // Menubar Root
39
+ const Menubar = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
40
+ ({ className, children, ...props }, ref) => {
41
+ const [activeMenu, setActiveMenu] = React.useState<string | null>(null)
42
+
43
+ // Close on outside click
44
+ React.useEffect(() => {
45
+ if (!activeMenu) return
46
+ const handleClick = (e: MouseEvent) => {
47
+ const target = e.target as Element
48
+ if (!target.closest('[data-menubar]')) {
49
+ setActiveMenu(null)
50
+ }
51
+ }
52
+ document.addEventListener("click", handleClick)
53
+ return () => document.removeEventListener("click", handleClick)
54
+ }, [activeMenu])
55
+
56
+ return (
57
+ <MenubarContext.Provider value={{ activeMenu, setActiveMenu }}>
58
+ <div
59
+ ref={ref}
60
+ data-menubar
61
+ className={cn(
62
+ "flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
63
+ className
64
+ )}
65
+ {...props}
66
+ >
67
+ {children}
68
+ </div>
69
+ </MenubarContext.Provider>
70
+ )
71
+ }
72
+ )
73
+ Menubar.displayName = "Menubar"
74
+
75
+ // MenubarMenu
76
+ interface MenubarMenuProps {
77
+ children: React.ReactNode
78
+ }
79
+
80
+ const MenubarMenu = ({ children }: MenubarMenuProps) => {
81
+ const menuId = React.useId()
82
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
83
+
84
+ return (
85
+ <MenubarMenuContext.Provider value={{ menuId, triggerRef }}>
86
+ <div className="relative">
87
+ {children}
88
+ </div>
89
+ </MenubarMenuContext.Provider>
90
+ )
91
+ }
92
+ MenubarMenu.displayName = "MenubarMenu"
93
+
94
+ // MenubarTrigger
95
+ const MenubarTrigger = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
96
+ ({ className, children, ...props }, ref) => {
97
+ const { activeMenu, setActiveMenu } = useMenubar()
98
+ const { menuId, triggerRef } = useMenubarMenu()
99
+ const isOpen = activeMenu === menuId
100
+
101
+ const handleClick = () => {
102
+ setActiveMenu(isOpen ? null : menuId)
103
+ }
104
+
105
+ const handleMouseEnter = () => {
106
+ if (activeMenu && activeMenu !== menuId) {
107
+ setActiveMenu(menuId)
108
+ }
109
+ }
110
+
111
+ return (
112
+ <button
113
+ ref={triggerRef}
114
+ className={cn(
115
+ "flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none",
116
+ "focus:bg-accent focus:text-accent-foreground",
117
+ isOpen && "bg-accent text-accent-foreground",
118
+ className
119
+ )}
120
+ onClick={handleClick}
121
+ onMouseEnter={handleMouseEnter}
122
+ {...props}
123
+ >
124
+ {children}
125
+ </button>
126
+ )
127
+ }
128
+ )
129
+ MenubarTrigger.displayName = "MenubarTrigger"
130
+
131
+ // MenubarContent
132
+ const MenubarContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
133
+ ({ className, children, ...props }, ref) => {
134
+ const { activeMenu, setActiveMenu } = useMenubar()
135
+ const { menuId, triggerRef } = useMenubarMenu()
136
+ const isOpen = activeMenu === menuId
137
+ const [position, setPosition] = React.useState({ top: 0, left: 0 })
138
+ const contentRef = React.useRef<HTMLDivElement>(null)
139
+
140
+ React.useEffect(() => {
141
+ if (!isOpen || !triggerRef.current) return
142
+ const rect = triggerRef.current.getBoundingClientRect()
143
+ setPosition({
144
+ top: rect.bottom + 4,
145
+ left: rect.left,
146
+ })
147
+ }, [isOpen, triggerRef])
148
+
149
+ // Close on Escape
150
+ React.useEffect(() => {
151
+ if (!isOpen) return
152
+ const handleEscape = (e: KeyboardEvent) => {
153
+ if (e.key === "Escape") setActiveMenu(null)
154
+ }
155
+ document.addEventListener("keydown", handleEscape)
156
+ return () => document.removeEventListener("keydown", handleEscape)
157
+ }, [isOpen, setActiveMenu])
158
+
159
+ if (!isOpen) return null
160
+
161
+ return (
162
+ <div
163
+ ref={contentRef}
164
+ className={cn(
165
+ "fixed z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
166
+ "animate-in fade-in-0 slide-in-from-top-2",
167
+ className
168
+ )}
169
+ style={{
170
+ top: position.top,
171
+ left: position.left,
172
+ }}
173
+ {...props}
174
+ >
175
+ {children}
176
+ </div>
177
+ )
178
+ }
179
+ )
180
+ MenubarContent.displayName = "MenubarContent"
181
+
182
+ // MenubarItem
183
+ interface MenubarItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
184
+ inset?: boolean
185
+ }
186
+
187
+ const MenubarItem = React.forwardRef<HTMLButtonElement, MenubarItemProps>(
188
+ ({ className, inset, children, ...props }, ref) => {
189
+ const { setActiveMenu } = useMenubar()
190
+
191
+ return (
192
+ <button
193
+ ref={ref}
194
+ className={cn(
195
+ "relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
196
+ "focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
197
+ "disabled:pointer-events-none disabled:opacity-50",
198
+ inset && "pl-8",
199
+ className
200
+ )}
201
+ onClick={() => setActiveMenu(null)}
202
+ {...props}
203
+ >
204
+ {children}
205
+ </button>
206
+ )
207
+ }
208
+ )
209
+ MenubarItem.displayName = "MenubarItem"
210
+
211
+ // MenubarSeparator
212
+ const MenubarSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
213
+ ({ className, ...props }, ref) => (
214
+ <div ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
215
+ )
216
+ )
217
+ MenubarSeparator.displayName = "MenubarSeparator"
218
+
219
+ // MenubarLabel
220
+ const MenubarLabel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }>(
221
+ ({ className, inset, ...props }, ref) => (
222
+ <div
223
+ ref={ref}
224
+ className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
225
+ {...props}
226
+ />
227
+ )
228
+ )
229
+ MenubarLabel.displayName = "MenubarLabel"
230
+
231
+ // MenubarShortcut
232
+ const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
233
+ <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
234
+ )
235
+ MenubarShortcut.displayName = "MenubarShortcut"
236
+
237
+ export {
238
+ Menubar,
239
+ MenubarMenu,
240
+ MenubarTrigger,
241
+ MenubarContent,
242
+ MenubarItem,
243
+ MenubarSeparator,
244
+ MenubarLabel,
245
+ MenubarShortcut,
246
+ }
@@ -0,0 +1,49 @@
1
+ import * as React from "react"
2
+ import { cn } from "@/lib/utils"
3
+
4
+ export interface NativeSelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> { }
5
+
6
+ /**
7
+ * NativeSelect - Styled browser-native select element
8
+ *
9
+ * Uses the browser's native <select> for accessibility and mobile UX,
10
+ * with custom styling to match the design system.
11
+ */
12
+ const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
13
+ ({ className, children, ...props }, ref) => {
14
+ return (
15
+ <div className="relative">
16
+ <select
17
+ ref={ref}
18
+ className={cn(
19
+ "flex h-10 w-full appearance-none rounded-md border border-input bg-background px-3 py-2 pr-8 text-sm ring-offset-background",
20
+ "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
21
+ "disabled:cursor-not-allowed disabled:opacity-50",
22
+ className
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ </select>
28
+ {/* Custom chevron icon */}
29
+ <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
30
+ <svg
31
+ className="h-4 w-4 opacity-50"
32
+ xmlns="http://www.w3.org/2000/svg"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ strokeWidth="2"
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ >
40
+ <path d="m6 9 6 6 6-6" />
41
+ </svg>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
46
+ )
47
+ NativeSelect.displayName = "NativeSelect"
48
+
49
+ export { NativeSelect }
@@ -58,6 +58,9 @@ PaginationItem.displayName = "PaginationItem"
58
58
  interface PaginationLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
59
59
  isActive?: boolean
60
60
  size?: "default" | "sm" | "lg" | "icon"
61
+ children?: React.ReactNode
62
+ className?: string
63
+ href?: string
61
64
  }
62
65
 
63
66
  const PaginationLink = ({
@@ -0,0 +1,251 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "@/lib/utils"
5
+
6
+ // Resizable Context
7
+ interface ResizablePanelGroupContextValue {
8
+ direction: "horizontal" | "vertical"
9
+ sizes: number[]
10
+ setSizes: React.Dispatch<React.SetStateAction<number[]>>
11
+ registerPanel: () => number
12
+ getPanelCount: () => number
13
+ }
14
+
15
+ const ResizablePanelGroupContext = React.createContext<ResizablePanelGroupContextValue | null>(null)
16
+
17
+ function useResizablePanelGroup() {
18
+ const context = React.useContext(ResizablePanelGroupContext)
19
+ if (!context) {
20
+ throw new Error("useResizablePanelGroup must be used within a ResizablePanelGroup")
21
+ }
22
+ return context
23
+ }
24
+
25
+ // ResizablePanelGroup
26
+ interface ResizablePanelGroupProps extends React.HTMLAttributes<HTMLDivElement> {
27
+ direction?: "horizontal" | "vertical"
28
+ onLayout?: (sizes: number[]) => void
29
+ }
30
+
31
+ const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
32
+ ({ className, direction = "horizontal", children, onLayout, ...props }, ref) => {
33
+ const [sizes, setSizes] = React.useState<number[]>([])
34
+ const panelCountRef = React.useRef(0)
35
+
36
+ // Reset panel count on each render to handle re-renders properly
37
+ React.useLayoutEffect(() => {
38
+ panelCountRef.current = 0
39
+ })
40
+
41
+ const registerPanel = React.useCallback(() => {
42
+ const index = panelCountRef.current
43
+ panelCountRef.current += 1
44
+ return index
45
+ }, [])
46
+
47
+ const getPanelCount = React.useCallback(() => {
48
+ return panelCountRef.current
49
+ }, [])
50
+
51
+ // Notify layout changes
52
+ React.useEffect(() => {
53
+ if (sizes.length > 0) {
54
+ onLayout?.(sizes)
55
+ }
56
+ }, [sizes, onLayout])
57
+
58
+ return (
59
+ <ResizablePanelGroupContext.Provider value={{ direction, sizes, setSizes, registerPanel, getPanelCount }}>
60
+ <div
61
+ ref={ref}
62
+ data-panel-group
63
+ data-direction={direction}
64
+ className={cn(
65
+ "flex h-full w-full",
66
+ direction === "horizontal" ? "flex-row" : "flex-col",
67
+ className
68
+ )}
69
+ {...props}
70
+ >
71
+ {children}
72
+ </div>
73
+ </ResizablePanelGroupContext.Provider>
74
+ )
75
+ }
76
+ )
77
+ ResizablePanelGroup.displayName = "ResizablePanelGroup"
78
+
79
+ // ResizablePanel
80
+ interface ResizablePanelProps extends React.HTMLAttributes<HTMLDivElement> {
81
+ defaultSize?: number
82
+ minSize?: number
83
+ maxSize?: number
84
+ }
85
+
86
+ const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
87
+ ({ className, defaultSize = 50, minSize = 10, maxSize = 90, children, style, ...props }, ref) => {
88
+ const { direction, sizes, setSizes, registerPanel } = useResizablePanelGroup()
89
+ const [index] = React.useState(() => registerPanel())
90
+
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])
101
+
102
+ const size = sizes[index] ?? defaultSize
103
+
104
+ return (
105
+ <div
106
+ ref={ref}
107
+ data-panel
108
+ data-panel-index={index}
109
+ className={cn("overflow-hidden", className)}
110
+ style={{
111
+ ...style,
112
+ flex: `0 0 ${size}%`,
113
+ }}
114
+ {...props}
115
+ >
116
+ {children}
117
+ </div>
118
+ )
119
+ }
120
+ )
121
+ ResizablePanel.displayName = "ResizablePanel"
122
+
123
+ // ResizableHandle
124
+ interface ResizableHandleProps extends React.HTMLAttributes<HTMLDivElement> {
125
+ withHandle?: boolean
126
+ }
127
+
128
+ const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
129
+ ({ className, withHandle = false, ...props }, ref) => {
130
+ const { direction, setSizes } = useResizablePanelGroup()
131
+ const [isDragging, setIsDragging] = React.useState(false)
132
+ const handleRef = React.useRef<HTMLDivElement>(null)
133
+
134
+ const handleMouseDown = (e: React.MouseEvent) => {
135
+ e.preventDefault()
136
+ setIsDragging(true)
137
+
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
146
+
147
+ const prevIndex = parseInt(prevPanel.dataset.panelIndex || "0")
148
+ const nextIndex = parseInt(nextPanel.dataset.panelIndex || "1")
149
+
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
156
+
157
+ const handleMouseMove = (moveEvent: MouseEvent) => {
158
+ const currentPos = direction === "horizontal" ? moveEvent.clientX : moveEvent.clientY
159
+ const delta = currentPos - startPos
160
+ const deltaPercent = (delta / totalSize) * 100
161
+
162
+ setSizes(prev => {
163
+ const newSizes = [...prev]
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
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
191
+ return newSizes
192
+ })
193
+ }
194
+
195
+ const handleMouseUp = () => {
196
+ setIsDragging(false)
197
+ document.removeEventListener("mousemove", handleMouseMove)
198
+ document.removeEventListener("mouseup", handleMouseUp)
199
+ }
200
+
201
+ document.addEventListener("mousemove", handleMouseMove)
202
+ document.addEventListener("mouseup", handleMouseUp)
203
+ }
204
+
205
+ return (
206
+ <div
207
+ ref={handleRef}
208
+ data-panel-resize-handle
209
+ className={cn(
210
+ "relative flex items-center justify-center bg-border",
211
+ direction === "horizontal"
212
+ ? "w-px cursor-col-resize hover:w-1 hover:bg-primary/50"
213
+ : "h-px cursor-row-resize hover:h-1 hover:bg-primary/50",
214
+ isDragging && (direction === "horizontal" ? "w-1 bg-primary" : "h-1 bg-primary"),
215
+ "transition-all",
216
+ className
217
+ )}
218
+ onMouseDown={handleMouseDown}
219
+ {...props}
220
+ >
221
+ {withHandle && (
222
+ <div
223
+ className={cn(
224
+ "z-10 flex items-center justify-center rounded-sm border bg-border",
225
+ direction === "horizontal" ? "h-4 w-3" : "h-3 w-4"
226
+ )}
227
+ >
228
+ <svg
229
+ className={cn(
230
+ "h-2.5 w-2.5 text-muted-foreground",
231
+ direction === "vertical" && "rotate-90"
232
+ )}
233
+ viewBox="0 0 6 10"
234
+ fill="currentColor"
235
+ >
236
+ <circle cx="1" cy="2" r="1" />
237
+ <circle cx="1" cy="5" r="1" />
238
+ <circle cx="1" cy="8" r="1" />
239
+ <circle cx="5" cy="2" r="1" />
240
+ <circle cx="5" cy="5" r="1" />
241
+ <circle cx="5" cy="8" r="1" />
242
+ </svg>
243
+ </div>
244
+ )}
245
+ </div>
246
+ )
247
+ }
248
+ )
249
+ ResizableHandle.displayName = "ResizableHandle"
250
+
251
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }