@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.
- package/dist/index.js +91 -0
- package/package.json +9 -3
- package/registry/badge.tsx +9 -25
- package/registry/breadcrumb.tsx +1 -1
- package/registry/button-group.tsx +9 -29
- package/registry/button.tsx +20 -46
- package/registry/calendar.tsx +416 -142
- package/registry/card.tsx +21 -47
- package/registry/combobox.tsx +171 -0
- package/registry/command.tsx +300 -0
- package/registry/container.tsx +9 -25
- package/registry/context-menu.tsx +221 -0
- package/registry/date-picker.tsx +179 -0
- package/registry/drawer.tsx +241 -0
- package/registry/dropdown-menu.tsx +93 -74
- package/registry/file-upload.tsx +240 -0
- package/registry/hover-card.tsx +165 -0
- package/registry/image.tsx +2 -2
- package/registry/kbd.tsx +60 -0
- package/registry/menubar.tsx +246 -0
- package/registry/native-select.tsx +49 -0
- package/registry/pagination.tsx +3 -0
- package/registry/resizable.tsx +251 -0
- package/registry/scroll-area.tsx +119 -0
- package/registry/search.tsx +2 -1
- package/registry/sheet.tsx +63 -18
- package/registry/sidebar.tsx +512 -0
- package/registry/slider.tsx +133 -54
- package/registry/text.tsx +7 -16
- package/registry/toggle-group.tsx +129 -0
- package/registry/toggle.tsx +72 -0
- package/registry/tooltip.tsx +21 -3
package/registry/slider.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import { cn } from "@/lib/utils"
|
|
3
3
|
|
|
4
|
-
interface SliderProps
|
|
4
|
+
interface SliderProps {
|
|
5
5
|
value?: number[]
|
|
6
6
|
onValueChange?: (value: number[]) => void
|
|
7
7
|
defaultValue?: number[]
|
|
@@ -9,14 +9,20 @@ interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChan
|
|
|
9
9
|
max?: number
|
|
10
10
|
step?: number
|
|
11
11
|
disabled?: boolean
|
|
12
|
+
minStepsBetweenThumbs?: number
|
|
13
|
+
className?: string
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
|
-
* Slider component with
|
|
17
|
+
* Slider component with support for multiple thumbs (range selection)
|
|
16
18
|
*
|
|
17
19
|
* @example
|
|
18
|
-
*
|
|
19
|
-
* <Slider value={
|
|
20
|
+
* // Single value
|
|
21
|
+
* <Slider value={[50]} onValueChange={setValue} max={100} step={1} />
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Range
|
|
25
|
+
* <Slider value={[25, 75]} onValueChange={setValue} max={100} step={1} />
|
|
20
26
|
*/
|
|
21
27
|
const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
22
28
|
({
|
|
@@ -28,84 +34,157 @@ const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
|
|
28
34
|
max = 100,
|
|
29
35
|
step = 1,
|
|
30
36
|
disabled,
|
|
37
|
+
minStepsBetweenThumbs = 0,
|
|
31
38
|
...props
|
|
32
39
|
}, ref) => {
|
|
33
40
|
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue)
|
|
41
|
+
const trackRef = React.useRef<HTMLDivElement>(null)
|
|
42
|
+
const activeThumbIndex = React.useRef<number | null>(null)
|
|
34
43
|
|
|
35
44
|
const value = controlledValue !== undefined ? controlledValue : uncontrolledValue
|
|
36
|
-
const setValue =
|
|
37
|
-
|
|
45
|
+
const setValue = (newValue: number[]) => {
|
|
46
|
+
if (controlledValue === undefined) {
|
|
47
|
+
setUncontrolledValue(newValue)
|
|
48
|
+
}
|
|
49
|
+
onValueChange?.(newValue)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const updateValue = (clientX: number, thumbIndex: number) => {
|
|
53
|
+
if (!trackRef.current) return
|
|
54
|
+
|
|
55
|
+
const rect = trackRef.current.getBoundingClientRect()
|
|
56
|
+
const percentage = (clientX - rect.left) / rect.width
|
|
57
|
+
const rawValue = min + percentage * (max - min)
|
|
58
|
+
const steppedValue = Math.round(rawValue / step) * step
|
|
59
|
+
const clampedValue = Math.min(Math.max(steppedValue, min), max)
|
|
60
|
+
|
|
61
|
+
const newValue = [...value]
|
|
62
|
+
newValue[thumbIndex] = clampedValue
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
// Sort logic to prevent crossover if preferred, or just allow it but sorted
|
|
65
|
+
newValue.sort((a, b) => a - b)
|
|
66
|
+
|
|
67
|
+
setValue(newValue)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find closest thumb to a point
|
|
71
|
+
const getClosestThumbIndex = (clientX: number) => {
|
|
72
|
+
if (!trackRef.current) return 0
|
|
73
|
+
const rect = trackRef.current.getBoundingClientRect()
|
|
74
|
+
const percentage = (clientX - rect.left) / rect.width
|
|
75
|
+
const clickedValue = min + percentage * (max - min)
|
|
76
|
+
|
|
77
|
+
let closestIndex = 0
|
|
78
|
+
let minDiff = Infinity
|
|
79
|
+
|
|
80
|
+
value.forEach((val, index) => {
|
|
81
|
+
const diff = Math.abs(val - clickedValue)
|
|
82
|
+
if (diff < minDiff) {
|
|
83
|
+
minDiff = diff
|
|
84
|
+
closestIndex = index
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return closestIndex
|
|
89
|
+
}
|
|
40
90
|
|
|
41
|
-
const
|
|
91
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
42
92
|
if (disabled) return
|
|
93
|
+
const thumbIndex = getClosestThumbIndex(e.clientX)
|
|
94
|
+
activeThumbIndex.current = thumbIndex
|
|
43
95
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
switch (e.key) {
|
|
47
|
-
case "ArrowRight":
|
|
48
|
-
case "ArrowUp":
|
|
49
|
-
newValue = Math.min(currentValue + step, max)
|
|
50
|
-
break
|
|
51
|
-
case "ArrowLeft":
|
|
52
|
-
case "ArrowDown":
|
|
53
|
-
newValue = Math.max(currentValue - step, min)
|
|
54
|
-
break
|
|
55
|
-
case "Home":
|
|
56
|
-
newValue = min
|
|
57
|
-
break
|
|
58
|
-
case "End":
|
|
59
|
-
newValue = max
|
|
60
|
-
break
|
|
61
|
-
default:
|
|
62
|
-
return
|
|
63
|
-
}
|
|
96
|
+
updateValue(e.clientX, thumbIndex)
|
|
64
97
|
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
99
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
103
|
+
if (activeThumbIndex.current === null) return
|
|
104
|
+
updateValue(e.clientX, activeThumbIndex.current)
|
|
67
105
|
}
|
|
68
106
|
|
|
69
|
-
const
|
|
107
|
+
const handleMouseUp = () => {
|
|
108
|
+
activeThumbIndex.current = null
|
|
109
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
110
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleTouchStart = (e: React.TouchEvent) => {
|
|
70
114
|
if (disabled) return
|
|
115
|
+
const thumbIndex = getClosestThumbIndex(e.touches[0].clientX)
|
|
116
|
+
activeThumbIndex.current = thumbIndex
|
|
117
|
+
|
|
118
|
+
updateValue(e.touches[0].clientX, thumbIndex)
|
|
71
119
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
120
|
+
document.addEventListener('touchmove', handleTouchMove)
|
|
121
|
+
document.addEventListener('touchend', handleTouchEnd)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const handleTouchMove = (e: TouchEvent) => {
|
|
125
|
+
if (activeThumbIndex.current === null) return
|
|
126
|
+
updateValue(e.touches[0].clientX, activeThumbIndex.current)
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const handleTouchEnd = () => {
|
|
131
|
+
activeThumbIndex.current = null
|
|
132
|
+
document.removeEventListener('touchmove', handleTouchMove)
|
|
133
|
+
document.removeEventListener('touchend', handleTouchEnd)
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
return (
|
|
80
137
|
<div
|
|
81
138
|
ref={ref}
|
|
82
|
-
role="
|
|
83
|
-
aria-valuenow={currentValue}
|
|
84
|
-
aria-valuemin={min}
|
|
85
|
-
aria-valuemax={max}
|
|
86
|
-
aria-disabled={disabled}
|
|
87
|
-
tabIndex={disabled ? -1 : 0}
|
|
139
|
+
role="group"
|
|
88
140
|
className={cn(
|
|
89
|
-
"relative flex w-full touch-none select-none items-center",
|
|
141
|
+
"relative flex w-full touch-none select-none items-center py-4 cursor-pointer",
|
|
90
142
|
disabled && "opacity-50 cursor-not-allowed",
|
|
91
143
|
className
|
|
92
144
|
)}
|
|
93
|
-
|
|
145
|
+
onMouseDown={handleMouseDown}
|
|
146
|
+
onTouchStart={handleTouchStart}
|
|
94
147
|
{...props}
|
|
95
148
|
>
|
|
96
149
|
<div
|
|
97
|
-
|
|
98
|
-
|
|
150
|
+
ref={trackRef}
|
|
151
|
+
className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary"
|
|
99
152
|
>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
153
|
+
{/* Render active tracks between ranges if multiple, or from 0 if single */}
|
|
154
|
+
{value.length > 1 ? (
|
|
155
|
+
<div
|
|
156
|
+
className="absolute h-full bg-primary transition-all duration-75"
|
|
157
|
+
style={{
|
|
158
|
+
left: `${((value[0] - min) / (max - min)) * 100}%`,
|
|
159
|
+
right: `${100 - ((value[value.length - 1] - min) / (max - min)) * 100}%`,
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
) : (
|
|
163
|
+
<div
|
|
164
|
+
className="absolute h-full bg-primary transition-all duration-75"
|
|
165
|
+
style={{ width: `${((value[0] - min) / (max - min)) * 100}%` }}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
104
168
|
</div>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
169
|
+
|
|
170
|
+
{value.map((val, index) => {
|
|
171
|
+
const percentage = ((val - min) / (max - min)) * 100
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
key={index}
|
|
175
|
+
role="slider"
|
|
176
|
+
aria-valuemin={min}
|
|
177
|
+
aria-valuemax={max}
|
|
178
|
+
aria-valuenow={val}
|
|
179
|
+
tabIndex={disabled ? -1 : 0}
|
|
180
|
+
className={cn(
|
|
181
|
+
"absolute block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
182
|
+
"hover:bg-accent hover:border-primary"
|
|
183
|
+
)}
|
|
184
|
+
style={{ left: `calc(${percentage}% - 8px)` }}
|
|
185
|
+
/>
|
|
186
|
+
)
|
|
187
|
+
})}
|
|
109
188
|
</div>
|
|
110
189
|
)
|
|
111
190
|
}
|
package/registry/text.tsx
CHANGED
|
@@ -26,13 +26,14 @@ const textVariants = cva("", {
|
|
|
26
26
|
|
|
27
27
|
type TextVariants = VariantProps<typeof textVariants>
|
|
28
28
|
|
|
29
|
-
interface
|
|
29
|
+
interface TextProps extends TextVariants {
|
|
30
30
|
className?: string
|
|
31
31
|
children?: React.ReactNode
|
|
32
|
+
as?: "p" | "span" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "label"
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
|
-
*
|
|
36
|
+
* Text component for typography
|
|
36
37
|
*
|
|
37
38
|
* @example
|
|
38
39
|
* // As a heading
|
|
@@ -44,22 +45,12 @@ interface TextBaseProps extends TextVariants {
|
|
|
44
45
|
* // Muted text
|
|
45
46
|
* <Text variant="muted">Secondary information</Text>
|
|
46
47
|
*/
|
|
47
|
-
const Text = React.forwardRef(
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
as,
|
|
51
|
-
className,
|
|
52
|
-
variant,
|
|
53
|
-
...props
|
|
54
|
-
}: TextBaseProps & { as?: T } & Omit<React.ComponentPropsWithoutRef<T>, keyof TextBaseProps | "as">,
|
|
55
|
-
ref: React.ForwardedRef<React.ElementRef<T>>
|
|
56
|
-
) => {
|
|
57
|
-
const Comp = as || "p"
|
|
58
|
-
|
|
48
|
+
const Text = React.forwardRef<HTMLElement, TextProps>(
|
|
49
|
+
({ as: Component = "p", className, variant, ...props }, ref) => {
|
|
59
50
|
return (
|
|
60
|
-
<
|
|
51
|
+
<Component
|
|
61
52
|
ref={ref as any}
|
|
62
|
-
className={cn(textVariants({ variant, className
|
|
53
|
+
className={cn(textVariants({ variant }), className)}
|
|
63
54
|
{...props}
|
|
64
55
|
/>
|
|
65
56
|
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "@/lib/utils"
|
|
5
|
+
import { toggleVariants } from "./toggle"
|
|
6
|
+
import { type VariantProps } from "class-variance-authority"
|
|
7
|
+
|
|
8
|
+
// ToggleGroup Context
|
|
9
|
+
interface ToggleGroupContextValue {
|
|
10
|
+
type: "single" | "multiple"
|
|
11
|
+
value: string[]
|
|
12
|
+
onValueChange: (value: string) => void
|
|
13
|
+
variant?: VariantProps<typeof toggleVariants>["variant"]
|
|
14
|
+
size?: VariantProps<typeof toggleVariants>["size"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ToggleGroupContext = React.createContext<ToggleGroupContextValue | null>(null)
|
|
18
|
+
|
|
19
|
+
function useToggleGroup() {
|
|
20
|
+
const context = React.useContext(ToggleGroupContext)
|
|
21
|
+
if (!context) {
|
|
22
|
+
throw new Error("useToggleGroup must be used within a ToggleGroup")
|
|
23
|
+
}
|
|
24
|
+
return context
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ToggleGroup Props
|
|
28
|
+
interface ToggleGroupSingleProps {
|
|
29
|
+
type: "single"
|
|
30
|
+
value?: string
|
|
31
|
+
defaultValue?: string
|
|
32
|
+
onValueChange?: (value: string) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ToggleGroupMultipleProps {
|
|
36
|
+
type: "multiple"
|
|
37
|
+
value?: string[]
|
|
38
|
+
defaultValue?: string[]
|
|
39
|
+
onValueChange?: (value: string[]) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ToggleGroupProps = (ToggleGroupSingleProps | ToggleGroupMultipleProps) &
|
|
43
|
+
React.HTMLAttributes<HTMLDivElement> &
|
|
44
|
+
VariantProps<typeof toggleVariants>
|
|
45
|
+
|
|
46
|
+
const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
|
|
47
|
+
({ className, type, value: controlledValue, defaultValue, onValueChange, variant, size, children, ...props }, ref) => {
|
|
48
|
+
// Handle both single and multiple types
|
|
49
|
+
const [uncontrolledValue, setUncontrolledValue] = React.useState<string[]>(() => {
|
|
50
|
+
if (type === "single") {
|
|
51
|
+
return defaultValue ? [defaultValue as string] : []
|
|
52
|
+
}
|
|
53
|
+
return (defaultValue as string[]) || []
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const value = controlledValue !== undefined
|
|
57
|
+
? (type === "single" ? [controlledValue as string] : controlledValue as string[])
|
|
58
|
+
: uncontrolledValue
|
|
59
|
+
|
|
60
|
+
const handleValueChange = (itemValue: string) => {
|
|
61
|
+
let newValue: string[]
|
|
62
|
+
|
|
63
|
+
if (type === "single") {
|
|
64
|
+
// Toggle off if clicking same value, otherwise set new value
|
|
65
|
+
newValue = value.includes(itemValue) ? [] : [itemValue]
|
|
66
|
+
if (controlledValue === undefined) {
|
|
67
|
+
setUncontrolledValue(newValue)
|
|
68
|
+
}
|
|
69
|
+
; (onValueChange as ((value: string) => void))?.(newValue[0] || "")
|
|
70
|
+
} else {
|
|
71
|
+
// Toggle item in array
|
|
72
|
+
newValue = value.includes(itemValue)
|
|
73
|
+
? value.filter(v => v !== itemValue)
|
|
74
|
+
: [...value, itemValue]
|
|
75
|
+
if (controlledValue === undefined) {
|
|
76
|
+
setUncontrolledValue(newValue)
|
|
77
|
+
}
|
|
78
|
+
; (onValueChange as ((value: string[]) => void))?.(newValue)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<ToggleGroupContext.Provider value={{ type, value, onValueChange: handleValueChange, variant, size }}>
|
|
84
|
+
<div
|
|
85
|
+
ref={ref}
|
|
86
|
+
role="group"
|
|
87
|
+
className={cn("flex items-center justify-center gap-1", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</div>
|
|
92
|
+
</ToggleGroupContext.Provider>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
ToggleGroup.displayName = "ToggleGroup"
|
|
97
|
+
|
|
98
|
+
// ToggleGroupItem
|
|
99
|
+
interface ToggleGroupItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
100
|
+
value: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ToggleGroupItem = React.forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
|
|
104
|
+
({ className, value, children, ...props }, ref) => {
|
|
105
|
+
const context = useToggleGroup()
|
|
106
|
+
const pressed = context.value.includes(value)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
ref={ref}
|
|
111
|
+
type="button"
|
|
112
|
+
aria-pressed={pressed}
|
|
113
|
+
data-state={pressed ? "on" : "off"}
|
|
114
|
+
className={cn(
|
|
115
|
+
toggleVariants({ variant: context.variant, size: context.size }),
|
|
116
|
+
pressed && "bg-accent text-accent-foreground",
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
onClick={() => context.onValueChange(value)}
|
|
120
|
+
{...props}
|
|
121
|
+
>
|
|
122
|
+
{children}
|
|
123
|
+
</button>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
ToggleGroupItem.displayName = "ToggleGroupItem"
|
|
128
|
+
|
|
129
|
+
export { ToggleGroup, ToggleGroupItem }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const toggleVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-transparent",
|
|
13
|
+
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
default: "h-10 px-3",
|
|
17
|
+
sm: "h-9 px-2.5",
|
|
18
|
+
lg: "h-11 px-5",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: "default",
|
|
23
|
+
size: "default",
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
export interface ToggleProps
|
|
29
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
30
|
+
VariantProps<typeof toggleVariants> {
|
|
31
|
+
/** Controlled pressed state */
|
|
32
|
+
pressed?: boolean
|
|
33
|
+
/** Default pressed state for uncontrolled usage */
|
|
34
|
+
defaultPressed?: boolean
|
|
35
|
+
/** Callback when pressed state changes */
|
|
36
|
+
onPressedChange?: (pressed: boolean) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
|
|
40
|
+
({ className, variant, size, pressed: controlledPressed, defaultPressed = false, onPressedChange, ...props }, ref) => {
|
|
41
|
+
const [uncontrolledPressed, setUncontrolledPressed] = React.useState(defaultPressed)
|
|
42
|
+
const pressed = controlledPressed ?? uncontrolledPressed
|
|
43
|
+
|
|
44
|
+
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
45
|
+
const newPressed = !pressed
|
|
46
|
+
if (controlledPressed === undefined) {
|
|
47
|
+
setUncontrolledPressed(newPressed)
|
|
48
|
+
}
|
|
49
|
+
onPressedChange?.(newPressed)
|
|
50
|
+
props.onClick?.(e)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<button
|
|
55
|
+
ref={ref}
|
|
56
|
+
type="button"
|
|
57
|
+
aria-pressed={pressed}
|
|
58
|
+
data-state={pressed ? "on" : "off"}
|
|
59
|
+
className={cn(
|
|
60
|
+
toggleVariants({ variant, size }),
|
|
61
|
+
pressed && "bg-accent text-accent-foreground",
|
|
62
|
+
className
|
|
63
|
+
)}
|
|
64
|
+
onClick={handleClick}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
Toggle.displayName = "Toggle"
|
|
71
|
+
|
|
72
|
+
export { Toggle, toggleVariants }
|
package/registry/tooltip.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { cn } from "@/lib/utils"
|
|
|
4
4
|
interface TooltipContextValue {
|
|
5
5
|
open: boolean
|
|
6
6
|
setOpen: (open: boolean) => void
|
|
7
|
+
mousePosition: { x: number; y: number }
|
|
8
|
+
setMousePosition: (pos: { x: number; y: number }) => void
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
const TooltipContext = React.createContext<TooltipContextValue | null>(null)
|
|
@@ -28,7 +30,7 @@ interface TooltipProps {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
* Tooltip component for hover hints
|
|
33
|
+
* Tooltip component for hover hints - appears at mouse cursor position
|
|
32
34
|
*
|
|
33
35
|
* @example
|
|
34
36
|
* <TooltipProvider>
|
|
@@ -40,12 +42,13 @@ interface TooltipProps {
|
|
|
40
42
|
*/
|
|
41
43
|
function Tooltip({ children, open: controlledOpen, onOpenChange, defaultOpen = false }: TooltipProps) {
|
|
42
44
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
45
|
+
const [mousePosition, setMousePosition] = React.useState({ x: 0, y: 0 })
|
|
43
46
|
|
|
44
47
|
const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen
|
|
45
48
|
const setOpen = onOpenChange || setUncontrolledOpen
|
|
46
49
|
|
|
47
50
|
return (
|
|
48
|
-
<TooltipContext.Provider value={{ open, setOpen }}>
|
|
51
|
+
<TooltipContext.Provider value={{ open, setOpen, mousePosition, setMousePosition }}>
|
|
49
52
|
<span className="relative inline-block">
|
|
50
53
|
{children}
|
|
51
54
|
</span>
|
|
@@ -64,6 +67,9 @@ const TooltipTrigger = React.forwardRef<HTMLSpanElement, TooltipTriggerProps>(
|
|
|
64
67
|
|
|
65
68
|
const handleMouseEnter = () => context.setOpen(true)
|
|
66
69
|
const handleMouseLeave = () => context.setOpen(false)
|
|
70
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
71
|
+
context.setMousePosition({ x: e.clientX, y: e.clientY })
|
|
72
|
+
}
|
|
67
73
|
const handleFocus = () => context.setOpen(true)
|
|
68
74
|
const handleBlur = () => context.setOpen(false)
|
|
69
75
|
|
|
@@ -71,6 +77,7 @@ const TooltipTrigger = React.forwardRef<HTMLSpanElement, TooltipTriggerProps>(
|
|
|
71
77
|
return React.cloneElement(children as React.ReactElement<any>, {
|
|
72
78
|
onMouseEnter: handleMouseEnter,
|
|
73
79
|
onMouseLeave: handleMouseLeave,
|
|
80
|
+
onMouseMove: handleMouseMove,
|
|
74
81
|
onFocus: handleFocus,
|
|
75
82
|
onBlur: handleBlur,
|
|
76
83
|
ref,
|
|
@@ -82,6 +89,7 @@ const TooltipTrigger = React.forwardRef<HTMLSpanElement, TooltipTriggerProps>(
|
|
|
82
89
|
ref={ref}
|
|
83
90
|
onMouseEnter={handleMouseEnter}
|
|
84
91
|
onMouseLeave={handleMouseLeave}
|
|
92
|
+
onMouseMove={handleMouseMove}
|
|
85
93
|
onFocus={handleFocus}
|
|
86
94
|
onBlur={handleBlur}
|
|
87
95
|
tabIndex={0}
|
|
@@ -103,12 +111,22 @@ const TooltipContent = React.forwardRef<
|
|
|
103
111
|
|
|
104
112
|
if (!context.open) return null
|
|
105
113
|
|
|
114
|
+
// Offset from cursor (slightly above and to the right)
|
|
115
|
+
const offsetX = 10
|
|
116
|
+
const offsetY = -10
|
|
117
|
+
|
|
106
118
|
return (
|
|
107
119
|
<div
|
|
108
120
|
ref={ref}
|
|
109
121
|
role="tooltip"
|
|
122
|
+
style={{
|
|
123
|
+
position: 'fixed',
|
|
124
|
+
left: context.mousePosition.x + offsetX,
|
|
125
|
+
top: context.mousePosition.y + offsetY,
|
|
126
|
+
transform: 'translateY(-100%)',
|
|
127
|
+
}}
|
|
110
128
|
className={cn(
|
|
111
|
-
"
|
|
129
|
+
"z-[9999] overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 pointer-events-none whitespace-nowrap",
|
|
112
130
|
className
|
|
113
131
|
)}
|
|
114
132
|
{...props}
|