@thangph2146/lexical-editor 0.0.1

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.
Files changed (183) hide show
  1. package/dist/editor-x/editor.cjs +33121 -0
  2. package/dist/editor-x/editor.cjs.map +1 -0
  3. package/dist/editor-x/editor.css +2854 -0
  4. package/dist/editor-x/editor.css.map +1 -0
  5. package/dist/editor-x/editor.d.cts +12 -0
  6. package/dist/editor-x/editor.d.ts +12 -0
  7. package/dist/editor-x/editor.js +33095 -0
  8. package/dist/editor-x/editor.js.map +1 -0
  9. package/dist/index.cjs +33210 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.css +2854 -0
  12. package/dist/index.css.map +1 -0
  13. package/dist/index.d.cts +15 -0
  14. package/dist/index.d.ts +15 -0
  15. package/dist/index.js +33183 -0
  16. package/dist/index.js.map +1 -0
  17. package/package.json +84 -0
  18. package/src/components/lexical-editor.tsx +123 -0
  19. package/src/context/editor-container-context.tsx +29 -0
  20. package/src/context/priority-image-context.tsx +7 -0
  21. package/src/context/toolbar-context.tsx +60 -0
  22. package/src/context/uploads-context.tsx +53 -0
  23. package/src/editor-hooks/use-debounce.ts +80 -0
  24. package/src/editor-hooks/use-modal.tsx +64 -0
  25. package/src/editor-hooks/use-report.ts +57 -0
  26. package/src/editor-hooks/use-update-toolbar.ts +41 -0
  27. package/src/editor-ui/broken-image.tsx +18 -0
  28. package/src/editor-ui/caption-composer.tsx +45 -0
  29. package/src/editor-ui/code-button.tsx +75 -0
  30. package/src/editor-ui/color-picker.tsx +2010 -0
  31. package/src/editor-ui/content-editable.tsx +37 -0
  32. package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
  33. package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
  34. package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
  35. package/src/editor-ui/image-component.tsx +321 -0
  36. package/src/editor-ui/image-placeholder.tsx +57 -0
  37. package/src/editor-ui/image-resizer.tsx +499 -0
  38. package/src/editor-ui/image-sizing.ts +120 -0
  39. package/src/editor-ui/lazy-image.tsx +136 -0
  40. package/src/editor-x/editor.tsx +117 -0
  41. package/src/editor-x/nodes.ts +79 -0
  42. package/src/editor-x/plugins.tsx +380 -0
  43. package/src/hooks/use-click-outside.ts +27 -0
  44. package/src/hooks/use-element-size.ts +54 -0
  45. package/src/hooks/use-header-height.ts +95 -0
  46. package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
  47. package/src/index.ts +4 -0
  48. package/src/lib/logger.ts +6 -0
  49. package/src/lib/utils.ts +19 -0
  50. package/src/nodes/autocomplete-node.tsx +94 -0
  51. package/src/nodes/embeds/tweet-node.tsx +224 -0
  52. package/src/nodes/embeds/youtube-node.tsx +519 -0
  53. package/src/nodes/emoji-node.tsx +83 -0
  54. package/src/nodes/image-node.tsx +328 -0
  55. package/src/nodes/keyword-node.tsx +58 -0
  56. package/src/nodes/layout-container-node.tsx +128 -0
  57. package/src/nodes/layout-item-node.tsx +118 -0
  58. package/src/nodes/list-with-color-node.tsx +160 -0
  59. package/src/nodes/mention-node.ts +122 -0
  60. package/src/plugins/actions/actions-plugin.tsx +3 -0
  61. package/src/plugins/actions/character-limit-plugin.tsx +27 -0
  62. package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
  63. package/src/plugins/actions/counter-character-plugin.tsx +80 -0
  64. package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
  65. package/src/plugins/actions/import-export-plugin.tsx +61 -0
  66. package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
  67. package/src/plugins/actions/max-length-plugin.tsx +59 -0
  68. package/src/plugins/actions/share-content-plugin.tsx +72 -0
  69. package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
  70. package/src/plugins/actions/tree-view-plugin.tsx +63 -0
  71. package/src/plugins/align-plugin.tsx +86 -0
  72. package/src/plugins/auto-link-plugin.tsx +34 -0
  73. package/src/plugins/autocomplete-plugin.tsx +2574 -0
  74. package/src/plugins/code-action-menu-plugin.tsx +240 -0
  75. package/src/plugins/code-highlight-plugin.tsx +22 -0
  76. package/src/plugins/component-picker-menu-plugin.tsx +427 -0
  77. package/src/plugins/context-menu-plugin.tsx +311 -0
  78. package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
  79. package/src/plugins/draggable-block-plugin.tsx +50 -0
  80. package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
  81. package/src/plugins/embeds/twitter-plugin.tsx +45 -0
  82. package/src/plugins/embeds/youtube-plugin.tsx +84 -0
  83. package/src/plugins/emoji-picker-plugin.tsx +206 -0
  84. package/src/plugins/emojis-plugin.tsx +84 -0
  85. package/src/plugins/floating-link-editor-plugin.tsx +791 -0
  86. package/src/plugins/floating-text-format-plugin.tsx +710 -0
  87. package/src/plugins/images-plugin.tsx +671 -0
  88. package/src/plugins/keywords-plugin.tsx +59 -0
  89. package/src/plugins/layout-plugin.tsx +658 -0
  90. package/src/plugins/link-plugin.tsx +18 -0
  91. package/src/plugins/list-color-plugin.tsx +178 -0
  92. package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
  93. package/src/plugins/mentions-plugin.tsx +714 -0
  94. package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
  95. package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
  96. package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
  97. package/src/plugins/picker/code-picker-plugin.tsx +30 -0
  98. package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
  99. package/src/plugins/picker/component-picker-option.tsx +47 -0
  100. package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
  101. package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
  102. package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
  103. package/src/plugins/picker/image-picker-plugin.tsx +16 -0
  104. package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
  105. package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
  106. package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
  107. package/src/plugins/picker/table-picker-plugin.tsx +56 -0
  108. package/src/plugins/tab-focus-plugin.tsx +66 -0
  109. package/src/plugins/table-column-resizer-plugin.tsx +309 -0
  110. package/src/plugins/table-plugin.tsx +299 -0
  111. package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
  112. package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
  113. package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
  114. package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
  115. package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
  116. package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
  117. package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
  118. package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
  119. package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
  120. package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
  121. package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
  122. package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
  123. package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
  124. package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
  125. package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
  126. package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
  127. package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
  128. package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
  129. package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
  130. package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
  131. package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
  132. package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
  133. package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
  134. package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
  135. package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
  136. package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
  137. package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
  138. package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
  139. package/src/plugins/typing-pref-plugin.tsx +118 -0
  140. package/src/shared/can-use-dom.ts +4 -0
  141. package/src/shared/environment.ts +47 -0
  142. package/src/shared/invariant.ts +16 -0
  143. package/src/shared/use-layout-effect.ts +12 -0
  144. package/src/themes/_mixins.scss +107 -0
  145. package/src/themes/_variables.scss +33 -0
  146. package/src/themes/editor-theme.scss +622 -0
  147. package/src/themes/editor-theme.ts +118 -0
  148. package/src/themes/plugins.scss +1180 -0
  149. package/src/themes/ui-components.scss +936 -0
  150. package/src/transformers/markdown-emoji-transformer.ts +20 -0
  151. package/src/transformers/markdown-hr-transformer.ts +28 -0
  152. package/src/transformers/markdown-image-transformer.ts +31 -0
  153. package/src/transformers/markdown-list-transformer.ts +51 -0
  154. package/src/transformers/markdown-table-transformer.ts +200 -0
  155. package/src/transformers/markdown-tweet-transformer.ts +26 -0
  156. package/src/ui/button-group.tsx +10 -0
  157. package/src/ui/button.tsx +29 -0
  158. package/src/ui/collapsible.tsx +67 -0
  159. package/src/ui/command.tsx +48 -0
  160. package/src/ui/dialog.tsx +146 -0
  161. package/src/ui/flex.tsx +38 -0
  162. package/src/ui/input.tsx +20 -0
  163. package/src/ui/label.tsx +20 -0
  164. package/src/ui/popover.tsx +128 -0
  165. package/src/ui/scroll-area.tsx +17 -0
  166. package/src/ui/select.tsx +171 -0
  167. package/src/ui/separator.tsx +20 -0
  168. package/src/ui/slider.tsx +14 -0
  169. package/src/ui/slot.tsx +3 -0
  170. package/src/ui/tabs.tsx +87 -0
  171. package/src/ui/toggle-group.tsx +109 -0
  172. package/src/ui/toggle.tsx +28 -0
  173. package/src/ui/tooltip.tsx +28 -0
  174. package/src/ui/typography.tsx +44 -0
  175. package/src/utils/doc-serialization.ts +68 -0
  176. package/src/utils/emoji-list.ts +16604 -0
  177. package/src/utils/get-dom-range-rect.ts +20 -0
  178. package/src/utils/get-selected-node.ts +20 -0
  179. package/src/utils/is-mobile-width.ts +0 -0
  180. package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
  181. package/src/utils/set-floating-elem-position.ts +44 -0
  182. package/src/utils/swipe.ts +119 -0
  183. package/src/utils/url.ts +32 -0
@@ -0,0 +1,38 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ export interface FlexProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ align?: "start" | "center" | "end" | "baseline" | "stretch"
6
+ justify?: "start" | "center" | "end" | "between" | "around" | "evenly"
7
+ direction?: "row" | "column" | "row-reverse" | "column-reverse"
8
+ wrap?: "nowrap" | "wrap" | "wrap-reverse"
9
+ gap?: number | string
10
+ }
11
+
12
+ export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
13
+ ({ className, align, justify, direction, wrap, gap, style, ...props }, ref) => {
14
+ const gapStyle =
15
+ typeof gap === "number"
16
+ ? { gap: `${gap * 0.25}rem` }
17
+ : typeof gap !== "undefined"
18
+ ? { gap }
19
+ : undefined
20
+
21
+ return (
22
+ <div
23
+ ref={ref}
24
+ className={cn(
25
+ "editor-flex",
26
+ align && `editor-flex--items-${align}`,
27
+ justify && `editor-flex--justify-${justify}`,
28
+ direction && `editor-flex--flex-${direction}`,
29
+ wrap && `editor-flex--flex-${wrap}`,
30
+ className
31
+ )}
32
+ style={{ ...gapStyle, ...style }}
33
+ {...props}
34
+ />
35
+ )
36
+ }
37
+ )
38
+ Flex.displayName = "Flex"
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement>
5
+
6
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
7
+ ({ className, type, ...props }, ref) => {
8
+ return (
9
+ <input
10
+ type={type}
11
+ className={cn("editor-input", className)}
12
+ ref={ref}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+ )
18
+ Input.displayName = "Input"
19
+
20
+ export { Input }
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ const Label = React.forwardRef<
5
+ HTMLLabelElement,
6
+ React.LabelHTMLAttributes<HTMLLabelElement>
7
+ >(({ className, ...props }, ref) => (
8
+ <label
9
+ ref={ref}
10
+ className={cn(
11
+ "editor-label",
12
+ props["aria-disabled"] && "editor-label--disabled",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Label.displayName = "Label"
19
+
20
+ export { Label }
@@ -0,0 +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<any>, {
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
+ }
@@ -0,0 +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
@@ -0,0 +1,171 @@
1
+ import * as React from "react"
2
+ import { createPortal } from "react-dom"
3
+ import { cn } from "../lib/utils"
4
+
5
+ interface SelectContextValue {
6
+ value: string
7
+ onValueChange?: (value: string) => void
8
+ open: boolean
9
+ setOpen: (open: boolean) => void
10
+ triggerRef: React.RefObject<HTMLButtonElement | null>
11
+ disabled?: boolean
12
+ }
13
+
14
+ const SelectContext = React.createContext<SelectContextValue | null>(null)
15
+
16
+ export interface SelectProps {
17
+ value: string
18
+ onValueChange?: (value: string) => void
19
+ children: React.ReactNode
20
+ disabled?: boolean
21
+ modal?: boolean
22
+ }
23
+
24
+ export function Select({ value, onValueChange, children, disabled }: SelectProps) {
25
+ const [open, setOpen] = React.useState(false)
26
+ const triggerRef = React.useRef<HTMLButtonElement>(null)
27
+
28
+ const contextValue = React.useMemo(() => ({
29
+ value, onValueChange, open, setOpen, triggerRef, disabled
30
+ }), [value, onValueChange, open, setOpen, disabled])
31
+
32
+ return (
33
+ <SelectContext.Provider value={contextValue}>
34
+ {children}
35
+ </SelectContext.Provider>
36
+ )
37
+ }
38
+
39
+ export interface SelectTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
40
+ size?: "default" | "sm" | "lg" | "icon"
41
+ }
42
+
43
+ export function SelectTrigger({ className, children, size = "default", ...props }: SelectTriggerProps) {
44
+ const context = React.useContext(SelectContext)
45
+ if (!context) throw new Error("SelectTrigger must be used within Select")
46
+
47
+ const { triggerRef, disabled, open, setOpen } = context
48
+
49
+ return (
50
+ <button
51
+ ref={triggerRef}
52
+ type="button"
53
+ className={cn(
54
+ "editor-select",
55
+ size !== "default" && `editor-select--size-${size}`,
56
+ className
57
+ )}
58
+ onClick={() => !disabled && setOpen(!open)}
59
+ disabled={disabled || props.disabled}
60
+ {...props}
61
+ >
62
+ {children}
63
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="editor-select-icon">
64
+ <path d="M4.93179 5.43179C4.75605 5.60753 4.75605 5.89245 4.93179 6.06819C5.10753 6.24392 5.39245 6.24392 5.56819 6.06819L7.49999 4.13638L9.43179 6.06819C9.60753 6.24392 9.89245 6.24392 10.0682 6.06819C10.2439 5.89245 10.2439 5.60753 10.0682 5.43179L7.81819 3.18179C7.73379 3.0974 7.61933 3.04999 7.49999 3.04999C7.38064 3.04999 7.26618 3.0974 7.18179 3.18179L4.93179 5.43179ZM10.0682 9.56819C10.2439 9.39245 10.2439 9.10753 10.0682 8.93179C9.89245 8.75606 9.60753 8.75606 9.43179 8.93179L7.49999 10.8636L5.56819 8.93179C5.39245 8.75606 5.10753 8.75606 4.93179 8.93179C4.75605 9.10753 4.75605 9.39245 4.93179 9.56819L7.18179 11.8182C7.26618 11.9026 7.38064 11.95 7.49999 11.95C7.61933 11.95 7.73379 11.9026 7.81819 11.8182L10.0682 9.56819Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path>
65
+ </svg>
66
+ </button>
67
+ )
68
+ }
69
+
70
+ export function SelectValue({ placeholder }: { placeholder?: string }) {
71
+ const context = React.useContext(SelectContext)
72
+ if (!context) throw new Error("SelectValue must be used within Select")
73
+
74
+ return <span>{context.value || placeholder}</span>
75
+ }
76
+
77
+ export function SelectContent({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
78
+ const context = React.useContext(SelectContext)
79
+ const [position, setPosition] = React.useState({ top: 0, left: 0, width: 0 })
80
+ const contentRef = React.useRef<HTMLDivElement>(null)
81
+
82
+ React.useEffect(() => {
83
+ if (context?.open && context.triggerRef.current) {
84
+ const rect = context.triggerRef.current.getBoundingClientRect()
85
+ setPosition({
86
+ top: rect.bottom + window.scrollY + 4,
87
+ left: rect.left + window.scrollX,
88
+ width: rect.width
89
+ })
90
+
91
+ // Simple click outside for portal
92
+ const handleClickOutside = (e: MouseEvent) => {
93
+ if (contentRef.current && !contentRef.current.contains(e.target as Node) &&
94
+ !context.triggerRef.current?.contains(e.target as Node)) {
95
+ context.setOpen(false)
96
+ }
97
+ }
98
+ document.addEventListener("mousedown", handleClickOutside)
99
+ return () => document.removeEventListener("mousedown", handleClickOutside)
100
+ }
101
+ }, [context, context?.open, context?.triggerRef, context?.setOpen])
102
+
103
+ if (!context?.open) return null
104
+
105
+ return createPortal(
106
+ <div
107
+ ref={contentRef}
108
+ className={cn("editor-popover-content", className)}
109
+ style={{
110
+ position: "absolute",
111
+ top: position.top,
112
+ left: position.left,
113
+ minWidth: position.width,
114
+ width: "auto", // allow wider
115
+ padding: "4px",
116
+ zIndex: 9999
117
+ }}
118
+ {...props}
119
+ >
120
+ {children}
121
+ </div>,
122
+ document.body
123
+ )
124
+ }
125
+
126
+ export function SelectItem({ value, children, className, ...props }: React.HTMLAttributes<HTMLDivElement> & { value: string }) {
127
+ const context = React.useContext(SelectContext)
128
+ if (!context) throw new Error("SelectItem must be used within Select")
129
+
130
+ const isSelected = context.value === value
131
+
132
+ return (
133
+ <div
134
+ className={cn(
135
+ "editor-select-item",
136
+ className
137
+ )}
138
+ data-selected={isSelected}
139
+ onClick={() => {
140
+ context.onValueChange?.(value)
141
+ context.setOpen(false)
142
+ }}
143
+ {...props}
144
+ >
145
+ {isSelected && (
146
+ <span className="editor-select-item__check">
147
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="editor-icon-sm">
148
+ <path d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z" fill="currentColor" fillRule="evenodd" clipRule="evenodd"></path>
149
+ </svg>
150
+ </span>
151
+ )}
152
+ {children}
153
+ </div>
154
+ )
155
+ }
156
+
157
+ export function SelectGroup({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
158
+ return (
159
+ <div role="group" className={className} {...props}>
160
+ {children}
161
+ </div>
162
+ )
163
+ }
164
+
165
+ export function SelectLabel({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
166
+ return (
167
+ <div className={cn("editor-select-label", className)} {...props}>
168
+ {children}
169
+ </div>
170
+ )
171
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ export const Separator = React.forwardRef<
5
+ HTMLDivElement,
6
+ React.HTMLAttributes<HTMLDivElement> & { orientation?: "horizontal" | "vertical" }
7
+ >(({ className, orientation = "horizontal", ...props }, ref) => (
8
+ <div
9
+ ref={ref}
10
+ role="separator"
11
+ aria-orientation={orientation}
12
+ className={cn(
13
+ "editor-separator",
14
+ `editor-separator--${orientation}`,
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ ))
20
+ Separator.displayName = "Separator"
@@ -0,0 +1,14 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ export const Slider = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <input
7
+ type="range"
8
+ ref={ref}
9
+ className={cn("editor-slider-input", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ )
14
+ Slider.displayName = "Slider"
@@ -0,0 +1,3 @@
1
+ import { Slot as RadixSlot } from "@radix-ui/react-slot"
2
+
3
+ export const Slot = RadixSlot
@@ -0,0 +1,87 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ const TabsContext = React.createContext<{
5
+ value: string
6
+ onValueChange: (value: string) => void
7
+ } | null>(null)
8
+
9
+ export function Tabs({
10
+ defaultValue,
11
+ value: controlledValue,
12
+ onValueChange,
13
+ children,
14
+ className,
15
+ ...props
16
+ }: {
17
+ defaultValue?: string
18
+ value?: string
19
+ onValueChange?: (value: string) => void
20
+ children: React.ReactNode
21
+ className?: string
22
+ }) {
23
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue || "")
24
+ const value = controlledValue ?? uncontrolledValue
25
+ const setValue = onValueChange ?? setUncontrolledValue
26
+
27
+ return (
28
+ <TabsContext.Provider value={{ value, onValueChange: setValue }}>
29
+ <div className={cn(className)} {...props}>
30
+ {children}
31
+ </div>
32
+ </TabsContext.Provider>
33
+ )
34
+ }
35
+
36
+ export function TabsList({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
37
+ return (
38
+ <div
39
+ className={cn(
40
+ "editor-tabs-list",
41
+ className
42
+ )}
43
+ {...props}
44
+ >
45
+ {children}
46
+ </div>
47
+ )
48
+ }
49
+
50
+ export function TabsTrigger({ value, className, children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string }) {
51
+ const context = React.useContext(TabsContext)
52
+ const isSelected = context?.value === value
53
+
54
+ return (
55
+ <button
56
+ type="button"
57
+ className={cn(
58
+ "editor-tabs-trigger",
59
+ className
60
+ )}
61
+ data-state={isSelected ? "active" : "inactive"}
62
+ onClick={() => context?.onValueChange(value)}
63
+ {...props}
64
+ >
65
+ {children}
66
+ </button>
67
+ )
68
+ }
69
+
70
+ export function TabsContent({ value, className, children, ...props }: React.HTMLAttributes<HTMLDivElement> & { value: string }) {
71
+ const context = React.useContext(TabsContext)
72
+ const isSelected = context?.value === value
73
+
74
+ if (!isSelected) return null
75
+
76
+ return (
77
+ <div
78
+ className={cn(
79
+ "editor-tabs-content",
80
+ className
81
+ )}
82
+ {...props}
83
+ >
84
+ {children}
85
+ </div>
86
+ )
87
+ }
@@ -0,0 +1,109 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ type ToggleGroupContextValue = {
5
+ type: "single" | "multiple"
6
+ value: string | string[]
7
+ onValueChange: (value: string | string[]) => void
8
+ size?: "default" | "sm" | "lg"
9
+ variant?: "default" | "outline"
10
+ }
11
+
12
+ const ToggleGroupContext = React.createContext<ToggleGroupContextValue | null>(null)
13
+
14
+ type ToggleGroupProps = (
15
+ | { type: "single"; value?: string; onValueChange?: (value: string) => void }
16
+ | { type: "multiple"; value?: string[]; onValueChange?: (value: string[]) => void }
17
+ ) & Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> & {
18
+ size?: "default" | "sm" | "lg"
19
+ variant?: "default" | "outline"
20
+ }
21
+
22
+ export const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
23
+ ({ className, type = "single", value, onValueChange, children, size, variant, ...props }, ref) => {
24
+ const contextValue: ToggleGroupContextValue = React.useMemo(() => ({
25
+ type: type as "single" | "multiple",
26
+ value: value || (type === "multiple" ? [] : ""),
27
+ onValueChange: (val: string | string[]) => {
28
+ if (type === "single" && typeof val === "string") {
29
+ (onValueChange as ((v: string) => void))?.(val)
30
+ } else if (type === "multiple" && Array.isArray(val)) {
31
+ (onValueChange as ((v: string[]) => void))?.(val)
32
+ }
33
+ },
34
+ size,
35
+ variant
36
+ }), [type, value, onValueChange, size, variant])
37
+
38
+ return (
39
+ <ToggleGroupContext.Provider value={contextValue}>
40
+ <div ref={ref} className={cn("editor-toggle-group", className)} {...props}>
41
+ {children}
42
+ </div>
43
+ </ToggleGroupContext.Provider>
44
+ )
45
+ }
46
+ )
47
+ ToggleGroup.displayName = "ToggleGroup"
48
+
49
+ export const ToggleGroupItem = React.forwardRef<
50
+ HTMLButtonElement,
51
+ React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string, size?: "default" | "sm" | "lg", variant?: "default" | "outline" }
52
+ >(({ className, value, onClick, size: itemSize, variant: itemVariant, ...props }, ref) => {
53
+ const context = React.useContext(ToggleGroupContext)
54
+
55
+ const size = itemSize || context?.size || "default"
56
+ // variant is not used in styles yet but good to have in props
57
+
58
+ if (!context) {
59
+ // Fallback if used outside group
60
+ return (
61
+ <button
62
+ ref={ref}
63
+ type="button"
64
+ className={cn(
65
+ "editor-toggle-group-item",
66
+ size !== "default" && `editor-toggle-group-item--size-${size}`,
67
+ className
68
+ )}
69
+ onClick={onClick}
70
+ {...props}
71
+ />
72
+ )
73
+ }
74
+
75
+ const isSelected = context.type === "single"
76
+ ? context.value === value
77
+ : Array.isArray(context.value) && context.value.includes(value)
78
+
79
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
80
+ if (context.type === "single") {
81
+ context.onValueChange(isSelected ? "" : value)
82
+ } else {
83
+ const currentValues = Array.isArray(context.value) ? context.value : []
84
+ const newValues = isSelected
85
+ ? currentValues.filter((v) => v !== value)
86
+ : [...currentValues, value]
87
+ context.onValueChange(newValues)
88
+ }
89
+ onClick?.(e)
90
+ }
91
+
92
+ return (
93
+ <button
94
+ ref={ref}
95
+ type="button"
96
+ aria-pressed={isSelected}
97
+ data-state={isSelected ? "on" : "off"}
98
+ className={cn(
99
+ "editor-toggle-group-item",
100
+ size !== "default" && `editor-toggle-group-item--size-${size}`,
101
+ (itemVariant === "outline" || (!itemVariant && context?.variant === "outline")) && "editor-toggle-group-item--outline",
102
+ className
103
+ )}
104
+ onClick={handleClick}
105
+ {...props}
106
+ />
107
+ )
108
+ })
109
+ ToggleGroupItem.displayName = "ToggleGroupItem"
@@ -0,0 +1,28 @@
1
+ import * as React from "react"
2
+ import { cn } from "../lib/utils"
3
+
4
+ export const Toggle = React.forwardRef<
5
+ HTMLButtonElement,
6
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
7
+ pressed?: boolean
8
+ onPressedChange?: (pressed: boolean) => void
9
+ variant?: "default" | "outline" | "ghost"
10
+ size?: "default" | "sm" | "lg"
11
+ }
12
+ >(({ className, pressed, onPressedChange, variant = "default", size = "default", ...props }, ref) => (
13
+ <button
14
+ ref={ref}
15
+ type="button"
16
+ aria-pressed={pressed}
17
+ data-state={pressed ? "on" : "off"}
18
+ className={cn(
19
+ "editor-toggle",
20
+ variant !== "default" && `editor-toggle--${variant}`,
21
+ size !== "default" && `editor-toggle--size-${size}`,
22
+ className
23
+ )}
24
+ onClick={() => onPressedChange?.(!pressed)}
25
+ {...props}
26
+ />
27
+ ))
28
+ Toggle.displayName = "Toggle"