@thangph2146/lexical-editor 0.0.11 → 0.0.13
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/README.md +2 -1
- package/dist/editor-x/editor.cjs +280 -20
- package/dist/editor-x/editor.cjs.map +1 -1
- package/dist/editor-x/editor.css +27 -4
- package/dist/editor-x/editor.css.map +1 -1
- package/dist/editor-x/editor.js +281 -21
- package/dist/editor-x/editor.js.map +1 -1
- package/dist/index.cjs +292 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +27 -4
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +293 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/lexical-editor.tsx +19 -6
- package/src/context/uploads-context.tsx +1 -0
- package/src/editor-ui/content-editable.tsx +18 -2
- package/src/editor-x/nodes.ts +2 -0
- package/src/nodes/download-link-node.tsx +118 -0
- package/src/plugins/floating-link-editor-plugin.tsx +338 -91
- package/src/themes/core/_tables.scss +0 -1
- package/src/themes/plugins/_floating-link-editor.scss +28 -2
- package/src/themes/ui-components/_button.scss +1 -1
- package/src/themes/ui-components/_flex.scss +1 -0
- package/src/ui/button-group.tsx +10 -10
- package/src/ui/button.tsx +38 -38
- package/src/ui/collapsible.tsx +67 -67
- package/src/ui/command.tsx +48 -48
- package/src/ui/dialog.tsx +146 -146
- package/src/ui/flex.tsx +45 -45
- package/src/ui/input.tsx +20 -20
- package/src/ui/label.tsx +20 -20
- package/src/ui/number-input.tsx +104 -104
- package/src/ui/popover.tsx +128 -128
- package/src/ui/scroll-area.tsx +17 -17
- package/src/ui/select.tsx +171 -171
- package/src/ui/separator.tsx +20 -20
- package/src/ui/slider.tsx +14 -14
- package/src/ui/slot.tsx +3 -3
- package/src/ui/tabs.tsx +87 -87
- package/src/ui/toggle-group.tsx +109 -109
- package/src/ui/toggle.tsx +28 -28
- package/src/ui/tooltip.tsx +28 -28
- package/src/ui/typography.tsx +44 -44
package/src/ui/number-input.tsx
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { Minus, Plus } from "lucide-react"
|
|
5
|
-
import { cn } from "../lib/utils"
|
|
6
|
-
import { Button } from "./button"
|
|
7
|
-
import { Input } from "./input"
|
|
8
|
-
|
|
9
|
-
export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
|
10
|
-
value: number
|
|
11
|
-
onValueChange: (value: number) => void
|
|
12
|
-
min?: number
|
|
13
|
-
max?: number
|
|
14
|
-
step?: number
|
|
15
|
-
unit?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
19
|
-
({ value, onValueChange, min = 0, max = 100, step = 1, unit = "px", className, ...props }, ref) => {
|
|
20
|
-
const timerRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
21
|
-
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
22
|
-
const valueRef = React.useRef(value)
|
|
23
|
-
|
|
24
|
-
// Sync ref with current value for interval closure
|
|
25
|
-
React.useEffect(() => {
|
|
26
|
-
valueRef.current = value
|
|
27
|
-
}, [value])
|
|
28
|
-
|
|
29
|
-
const updateValue = React.useCallback((delta: number) => {
|
|
30
|
-
onValueChange(Math.min(Math.max(valueRef.current + delta, min), max))
|
|
31
|
-
}, [onValueChange, min, max])
|
|
32
|
-
|
|
33
|
-
const startLongPress = (delta: number) => {
|
|
34
|
-
updateValue(delta)
|
|
35
|
-
timerRef.current = setTimeout(() => {
|
|
36
|
-
intervalRef.current = setInterval(() => {
|
|
37
|
-
updateValue(delta)
|
|
38
|
-
}, 50)
|
|
39
|
-
}, 300) // Reduced initial delay to 300ms
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const stopLongPress = React.useCallback(() => {
|
|
43
|
-
if (timerRef.current) clearTimeout(timerRef.current)
|
|
44
|
-
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
45
|
-
}, [])
|
|
46
|
-
|
|
47
|
-
const handleBlur = () => {
|
|
48
|
-
onValueChange(Math.min(Math.max(value, min), max))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
React.useEffect(() => {
|
|
52
|
-
return () => stopLongPress()
|
|
53
|
-
}, [stopLongPress])
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div className={cn("editor-number-input-container", className)}>
|
|
57
|
-
<Button
|
|
58
|
-
variant="ghost"
|
|
59
|
-
size="icon"
|
|
60
|
-
className="editor-number-input-btn"
|
|
61
|
-
onMouseDown={() => startLongPress(-step)}
|
|
62
|
-
onMouseUp={stopLongPress}
|
|
63
|
-
onMouseLeave={stopLongPress}
|
|
64
|
-
onTouchStart={() => startLongPress(-step)}
|
|
65
|
-
onTouchEnd={stopLongPress}
|
|
66
|
-
tabIndex={-1} // Prevent tabbing into buttons
|
|
67
|
-
>
|
|
68
|
-
<Minus size={14} />
|
|
69
|
-
</Button>
|
|
70
|
-
<div className="editor-number-input-wrapper">
|
|
71
|
-
<Input
|
|
72
|
-
{...props}
|
|
73
|
-
ref={ref}
|
|
74
|
-
type="number"
|
|
75
|
-
value={value}
|
|
76
|
-
onBlur={handleBlur}
|
|
77
|
-
onChange={(e) => {
|
|
78
|
-
const val = parseInt(e.target.value, 10)
|
|
79
|
-
if (!isNaN(val)) onValueChange(val)
|
|
80
|
-
else onValueChange(0) // Default to 0 for invalid input while typing
|
|
81
|
-
}}
|
|
82
|
-
className="editor-number-input-field"
|
|
83
|
-
/>
|
|
84
|
-
{unit && <span className="editor-number-input-unit">{unit}</span>}
|
|
85
|
-
</div>
|
|
86
|
-
<Button
|
|
87
|
-
variant="ghost"
|
|
88
|
-
size="icon"
|
|
89
|
-
className="editor-number-input-btn"
|
|
90
|
-
onMouseDown={() => startLongPress(step)}
|
|
91
|
-
onMouseUp={stopLongPress}
|
|
92
|
-
onMouseLeave={stopLongPress}
|
|
93
|
-
onTouchStart={() => startLongPress(step)}
|
|
94
|
-
onTouchEnd={stopLongPress}
|
|
95
|
-
tabIndex={-1}
|
|
96
|
-
>
|
|
97
|
-
<Plus size={14} />
|
|
98
|
-
</Button>
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
NumberInput.displayName = "NumberInput"
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Minus, Plus } from "lucide-react"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
import { Button } from "./button"
|
|
7
|
+
import { Input } from "./input"
|
|
8
|
+
|
|
9
|
+
export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
|
|
10
|
+
value: number
|
|
11
|
+
onValueChange: (value: number) => void
|
|
12
|
+
min?: number
|
|
13
|
+
max?: number
|
|
14
|
+
step?: number
|
|
15
|
+
unit?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
|
|
19
|
+
({ value, onValueChange, min = 0, max = 100, step = 1, unit = "px", className, ...props }, ref) => {
|
|
20
|
+
const timerRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
21
|
+
const intervalRef = React.useRef<NodeJS.Timeout | null>(null)
|
|
22
|
+
const valueRef = React.useRef(value)
|
|
23
|
+
|
|
24
|
+
// Sync ref with current value for interval closure
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
valueRef.current = value
|
|
27
|
+
}, [value])
|
|
28
|
+
|
|
29
|
+
const updateValue = React.useCallback((delta: number) => {
|
|
30
|
+
onValueChange(Math.min(Math.max(valueRef.current + delta, min), max))
|
|
31
|
+
}, [onValueChange, min, max])
|
|
32
|
+
|
|
33
|
+
const startLongPress = (delta: number) => {
|
|
34
|
+
updateValue(delta)
|
|
35
|
+
timerRef.current = setTimeout(() => {
|
|
36
|
+
intervalRef.current = setInterval(() => {
|
|
37
|
+
updateValue(delta)
|
|
38
|
+
}, 50)
|
|
39
|
+
}, 300) // Reduced initial delay to 300ms
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const stopLongPress = React.useCallback(() => {
|
|
43
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
44
|
+
if (intervalRef.current) clearInterval(intervalRef.current)
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
const handleBlur = () => {
|
|
48
|
+
onValueChange(Math.min(Math.max(value, min), max))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
return () => stopLongPress()
|
|
53
|
+
}, [stopLongPress])
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn("editor-number-input-container", className)}>
|
|
57
|
+
<Button
|
|
58
|
+
variant="ghost"
|
|
59
|
+
size="icon"
|
|
60
|
+
className="editor-number-input-btn"
|
|
61
|
+
onMouseDown={() => startLongPress(-step)}
|
|
62
|
+
onMouseUp={stopLongPress}
|
|
63
|
+
onMouseLeave={stopLongPress}
|
|
64
|
+
onTouchStart={() => startLongPress(-step)}
|
|
65
|
+
onTouchEnd={stopLongPress}
|
|
66
|
+
tabIndex={-1} // Prevent tabbing into buttons
|
|
67
|
+
>
|
|
68
|
+
<Minus size={14} />
|
|
69
|
+
</Button>
|
|
70
|
+
<div className="editor-number-input-wrapper">
|
|
71
|
+
<Input
|
|
72
|
+
{...props}
|
|
73
|
+
ref={ref}
|
|
74
|
+
type="number"
|
|
75
|
+
value={value}
|
|
76
|
+
onBlur={handleBlur}
|
|
77
|
+
onChange={(e) => {
|
|
78
|
+
const val = parseInt(e.target.value, 10)
|
|
79
|
+
if (!isNaN(val)) onValueChange(val)
|
|
80
|
+
else onValueChange(0) // Default to 0 for invalid input while typing
|
|
81
|
+
}}
|
|
82
|
+
className="editor-number-input-field"
|
|
83
|
+
/>
|
|
84
|
+
{unit && <span className="editor-number-input-unit">{unit}</span>}
|
|
85
|
+
</div>
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="icon"
|
|
89
|
+
className="editor-number-input-btn"
|
|
90
|
+
onMouseDown={() => startLongPress(step)}
|
|
91
|
+
onMouseUp={stopLongPress}
|
|
92
|
+
onMouseLeave={stopLongPress}
|
|
93
|
+
onTouchStart={() => startLongPress(step)}
|
|
94
|
+
onTouchEnd={stopLongPress}
|
|
95
|
+
tabIndex={-1}
|
|
96
|
+
>
|
|
97
|
+
<Plus size={14} />
|
|
98
|
+
</Button>
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
NumberInput.displayName = "NumberInput"
|
package/src/ui/popover.tsx
CHANGED
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { createPortal } from "react-dom"
|
|
3
|
-
import { cn } from "../lib/utils"
|
|
4
|
-
|
|
5
|
-
interface PopoverContextValue {
|
|
6
|
-
open: boolean
|
|
7
|
-
setOpen: (open: boolean) => void
|
|
8
|
-
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
9
|
-
modal?: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const PopoverContext = React.createContext<PopoverContextValue | null>(null)
|
|
13
|
-
|
|
14
|
-
export function Popover({ children, open: controlledOpen, defaultOpen = false, onOpenChange, modal = false }: {
|
|
15
|
-
children: React.ReactNode
|
|
16
|
-
open?: boolean
|
|
17
|
-
defaultOpen?: boolean
|
|
18
|
-
onOpenChange?: (open: boolean) => void
|
|
19
|
-
modal?: boolean
|
|
20
|
-
}) {
|
|
21
|
-
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
22
|
-
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
23
|
-
|
|
24
|
-
const open = controlledOpen ?? uncontrolledOpen
|
|
25
|
-
const setOpen = onOpenChange ?? setUncontrolledOpen
|
|
26
|
-
|
|
27
|
-
const contextValue = React.useMemo(() => ({
|
|
28
|
-
open, setOpen, triggerRef, modal
|
|
29
|
-
}), [open, setOpen, modal])
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<PopoverContext.Provider value={contextValue}>
|
|
33
|
-
{children}
|
|
34
|
-
</PopoverContext.Provider>
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function PopoverTrigger({ children, asChild, ...props }: React.HTMLAttributes<HTMLElement> & { asChild?: boolean, children: React.ReactNode, disabled?: boolean }) {
|
|
39
|
-
const context = React.useContext(PopoverContext)
|
|
40
|
-
if (!context) throw new Error("PopoverTrigger must be used within Popover")
|
|
41
|
-
|
|
42
|
-
const { triggerRef, open, setOpen } = context
|
|
43
|
-
|
|
44
|
-
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
45
|
-
setOpen(!open)
|
|
46
|
-
if (React.isValidElement(children)) {
|
|
47
|
-
(children as React.ReactElement<{ onClick?: React.MouseEventHandler<HTMLElement> }>).props.onClick?.(e)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (asChild && React.isValidElement(children)) {
|
|
52
|
-
// eslint-disable-next-line react-hooks/refs
|
|
53
|
-
return React.cloneElement(children as React.ReactElement<React.HTMLAttributes<HTMLElement> & { ref?: React.Ref<HTMLElement> }>, {
|
|
54
|
-
ref: triggerRef,
|
|
55
|
-
onClick: handleClick,
|
|
56
|
-
...props
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<button
|
|
62
|
-
type="button"
|
|
63
|
-
ref={triggerRef}
|
|
64
|
-
onClick={handleClick}
|
|
65
|
-
{...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
|
|
66
|
-
>
|
|
67
|
-
{children}
|
|
68
|
-
</button>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function PopoverContent({ children, className, align = "center", sideOffset = 4, ...props }: React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end", sideOffset?: number }) {
|
|
73
|
-
const context = React.useContext(PopoverContext)
|
|
74
|
-
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
75
|
-
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
76
|
-
|
|
77
|
-
React.useEffect(() => {
|
|
78
|
-
if (context?.open && context.triggerRef.current) {
|
|
79
|
-
const rect = context.triggerRef.current.getBoundingClientRect()
|
|
80
|
-
// Simple positioning logic (bottom-center or bottom-left)
|
|
81
|
-
// Real popover needs collision detection, but this is "lite"
|
|
82
|
-
let left = rect.left
|
|
83
|
-
if (align === "center") {
|
|
84
|
-
left = rect.left + rect.width / 2 // Will need to subtract half content width after render
|
|
85
|
-
} else if (align === "end") {
|
|
86
|
-
left = rect.right
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
setPosition({
|
|
90
|
-
top: rect.bottom + window.scrollY + sideOffset,
|
|
91
|
-
left: left + window.scrollX
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
95
|
-
if (contentRef.current && !contentRef.current.contains(e.target as Node) &&
|
|
96
|
-
!context.triggerRef.current?.contains(e.target as Node)) {
|
|
97
|
-
context.setOpen(false)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
document.addEventListener("mousedown", handleClickOutside)
|
|
101
|
-
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
102
|
-
}
|
|
103
|
-
}, [context, context?.open, align, sideOffset])
|
|
104
|
-
|
|
105
|
-
if (!context?.open) return null
|
|
106
|
-
|
|
107
|
-
return createPortal(
|
|
108
|
-
<div
|
|
109
|
-
ref={contentRef}
|
|
110
|
-
className={cn("editor-popover-content", className)}
|
|
111
|
-
style={{
|
|
112
|
-
position: "absolute",
|
|
113
|
-
top: position.top,
|
|
114
|
-
left: position.left,
|
|
115
|
-
zIndex: 9999
|
|
116
|
-
}}
|
|
117
|
-
{...props}
|
|
118
|
-
>
|
|
119
|
-
{children}
|
|
120
|
-
</div>,
|
|
121
|
-
document.body
|
|
122
|
-
)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function PopoverPortal({ children, container }: { children: React.ReactNode, container?: HTMLElement | null }) {
|
|
126
|
-
if (!container) return <>{children}</>
|
|
127
|
-
return createPortal(children, container)
|
|
128
|
-
}
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { createPortal } from "react-dom"
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
interface PopoverContextValue {
|
|
6
|
+
open: boolean
|
|
7
|
+
setOpen: (open: boolean) => void
|
|
8
|
+
triggerRef: React.RefObject<HTMLButtonElement | null>
|
|
9
|
+
modal?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PopoverContext = React.createContext<PopoverContextValue | null>(null)
|
|
13
|
+
|
|
14
|
+
export function Popover({ children, open: controlledOpen, defaultOpen = false, onOpenChange, modal = false }: {
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
open?: boolean
|
|
17
|
+
defaultOpen?: boolean
|
|
18
|
+
onOpenChange?: (open: boolean) => void
|
|
19
|
+
modal?: boolean
|
|
20
|
+
}) {
|
|
21
|
+
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
|
|
22
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
23
|
+
|
|
24
|
+
const open = controlledOpen ?? uncontrolledOpen
|
|
25
|
+
const setOpen = onOpenChange ?? setUncontrolledOpen
|
|
26
|
+
|
|
27
|
+
const contextValue = React.useMemo(() => ({
|
|
28
|
+
open, setOpen, triggerRef, modal
|
|
29
|
+
}), [open, setOpen, modal])
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<PopoverContext.Provider value={contextValue}>
|
|
33
|
+
{children}
|
|
34
|
+
</PopoverContext.Provider>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function PopoverTrigger({ children, asChild, ...props }: React.HTMLAttributes<HTMLElement> & { asChild?: boolean, children: React.ReactNode, disabled?: boolean }) {
|
|
39
|
+
const context = React.useContext(PopoverContext)
|
|
40
|
+
if (!context) throw new Error("PopoverTrigger must be used within Popover")
|
|
41
|
+
|
|
42
|
+
const { triggerRef, open, setOpen } = context
|
|
43
|
+
|
|
44
|
+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
45
|
+
setOpen(!open)
|
|
46
|
+
if (React.isValidElement(children)) {
|
|
47
|
+
(children as React.ReactElement<{ onClick?: React.MouseEventHandler<HTMLElement> }>).props.onClick?.(e)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (asChild && React.isValidElement(children)) {
|
|
52
|
+
// eslint-disable-next-line react-hooks/refs
|
|
53
|
+
return React.cloneElement(children as React.ReactElement<React.HTMLAttributes<HTMLElement> & { ref?: React.Ref<HTMLElement> }>, {
|
|
54
|
+
ref: triggerRef,
|
|
55
|
+
onClick: handleClick,
|
|
56
|
+
...props
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
ref={triggerRef}
|
|
64
|
+
onClick={handleClick}
|
|
65
|
+
{...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</button>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function PopoverContent({ children, className, align = "center", sideOffset = 4, ...props }: React.HTMLAttributes<HTMLDivElement> & { align?: "start" | "center" | "end", sideOffset?: number }) {
|
|
73
|
+
const context = React.useContext(PopoverContext)
|
|
74
|
+
const [position, setPosition] = React.useState({ top: 0, left: 0 })
|
|
75
|
+
const contentRef = React.useRef<HTMLDivElement>(null)
|
|
76
|
+
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
if (context?.open && context.triggerRef.current) {
|
|
79
|
+
const rect = context.triggerRef.current.getBoundingClientRect()
|
|
80
|
+
// Simple positioning logic (bottom-center or bottom-left)
|
|
81
|
+
// Real popover needs collision detection, but this is "lite"
|
|
82
|
+
let left = rect.left
|
|
83
|
+
if (align === "center") {
|
|
84
|
+
left = rect.left + rect.width / 2 // Will need to subtract half content width after render
|
|
85
|
+
} else if (align === "end") {
|
|
86
|
+
left = rect.right
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setPosition({
|
|
90
|
+
top: rect.bottom + window.scrollY + sideOffset,
|
|
91
|
+
left: left + window.scrollX
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
95
|
+
if (contentRef.current && !contentRef.current.contains(e.target as Node) &&
|
|
96
|
+
!context.triggerRef.current?.contains(e.target as Node)) {
|
|
97
|
+
context.setOpen(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
document.addEventListener("mousedown", handleClickOutside)
|
|
101
|
+
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
102
|
+
}
|
|
103
|
+
}, [context, context?.open, align, sideOffset])
|
|
104
|
+
|
|
105
|
+
if (!context?.open) return null
|
|
106
|
+
|
|
107
|
+
return createPortal(
|
|
108
|
+
<div
|
|
109
|
+
ref={contentRef}
|
|
110
|
+
className={cn("editor-popover-content", className)}
|
|
111
|
+
style={{
|
|
112
|
+
position: "absolute",
|
|
113
|
+
top: position.top,
|
|
114
|
+
left: position.left,
|
|
115
|
+
zIndex: 9999
|
|
116
|
+
}}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</div>,
|
|
121
|
+
document.body
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function PopoverPortal({ children, container }: { children: React.ReactNode, container?: HTMLElement | null }) {
|
|
126
|
+
if (!container) return <>{children}</>
|
|
127
|
+
return createPortal(children, container)
|
|
128
|
+
}
|
package/src/ui/scroll-area.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import * as React from "react"
|
|
2
|
-
import { cn } from "../lib/utils"
|
|
3
|
-
|
|
4
|
-
export const ScrollArea = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
-
({ className, children, ...props }, ref) => (
|
|
6
|
-
<div
|
|
7
|
-
ref={ref}
|
|
8
|
-
className={cn("editor-scroll-area-viewport", className)}
|
|
9
|
-
{...props}
|
|
10
|
-
>
|
|
11
|
-
{children}
|
|
12
|
-
</div>
|
|
13
|
-
)
|
|
14
|
-
)
|
|
15
|
-
ScrollArea.displayName = "ScrollArea"
|
|
16
|
-
|
|
17
|
-
export const ScrollBar = () => null // No-op for now, or use custom scrollbar styles
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "../lib/utils"
|
|
3
|
+
|
|
4
|
+
export const ScrollArea = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5
|
+
({ className, children, ...props }, ref) => (
|
|
6
|
+
<div
|
|
7
|
+
ref={ref}
|
|
8
|
+
className={cn("editor-scroll-area-viewport", className)}
|
|
9
|
+
{...props}
|
|
10
|
+
>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
ScrollArea.displayName = "ScrollArea"
|
|
16
|
+
|
|
17
|
+
export const ScrollBar = () => null // No-op for now, or use custom scrollbar styles
|