@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,2010 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as SliderPrimitive from "@radix-ui/react-slider"
5
+ import { Slot } from "../ui/slot"
6
+ import { PipetteIcon } from "lucide-react"
7
+
8
+ import { cn } from "../lib/utils"
9
+ import { logger } from "../lib/logger"
10
+ import { Button } from "../ui/button"
11
+ import { Input } from "../ui/input"
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from "../ui/popover"
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from "../ui/select"
24
+
25
+ /**
26
+ * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx
27
+ */
28
+
29
+ type PossibleRef<T> = React.Ref<T> | undefined
30
+
31
+ /**
32
+ * Set a given ref to a given value
33
+ * This utility takes care of different types of refs: callback refs and RefObject(s)
34
+ */
35
+ function setRef<T>(ref: PossibleRef<T>, value: T) {
36
+ if (typeof ref === "function") {
37
+ return ref(value)
38
+ }
39
+
40
+ if (ref !== null && ref !== undefined) {
41
+ ref.current = value
42
+ }
43
+ }
44
+
45
+ /**
46
+ * A utility to compose multiple refs together
47
+ * Accepts callback refs and RefObject(s)
48
+ */
49
+ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
50
+ return (node) => {
51
+ let hasCleanup = false
52
+ const cleanups = refs.map((ref) => {
53
+ const cleanup = setRef(ref, node)
54
+ if (!hasCleanup && typeof cleanup === "function") {
55
+ hasCleanup = true
56
+ }
57
+ return cleanup
58
+ })
59
+
60
+ // React <19 will log an error to the console if a callback ref returns a
61
+ // value. We don't use ref cleanups internally so this will only happen if a
62
+ // user's ref callback returns a value, which we only expect if they are
63
+ // using the cleanup functionality added in React 19.
64
+ if (hasCleanup) {
65
+ return () => {
66
+ for (let i = 0; i < cleanups.length; i++) {
67
+ const cleanup = cleanups[i]
68
+ if (typeof cleanup === "function") {
69
+ cleanup()
70
+ } else {
71
+ setRef(refs[i], null)
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * A custom hook that composes multiple refs
81
+ * Accepts callback refs and RefObject(s)
82
+ */
83
+ function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
84
+ // biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
85
+ return React.useCallback(
86
+ (value: T) => {
87
+ composeRefs(...refs)(value)
88
+ },
89
+ // eslint-disable-next-line react-hooks/exhaustive-deps
90
+ refs
91
+ )
92
+ }
93
+
94
+ type InputValue = string[] | string
95
+
96
+ interface VisuallyHiddenInputProps<T = InputValue>
97
+ extends Omit<
98
+ React.InputHTMLAttributes<HTMLInputElement>,
99
+ "value" | "checked" | "onReset"
100
+ > {
101
+ value?: T
102
+ checked?: boolean
103
+ control: HTMLElement | null
104
+ bubbles?: boolean
105
+ }
106
+
107
+ function VisuallyHiddenInput<T = InputValue>(
108
+ props: VisuallyHiddenInputProps<T>
109
+ ) {
110
+ const {
111
+ control,
112
+ value,
113
+ checked,
114
+ bubbles = true,
115
+ type = "hidden",
116
+ style,
117
+ ...inputProps
118
+ } = props
119
+
120
+ const isCheckInput = React.useMemo(
121
+ () => type === "checkbox" || type === "radio" || type === "switch",
122
+ [type]
123
+ )
124
+ const inputRef = React.useRef<HTMLInputElement>(null)
125
+
126
+ const prevValueRef = React.useRef<{
127
+ value: T | boolean | undefined
128
+ previous: T | boolean | undefined
129
+ }>({
130
+ value: isCheckInput ? checked : value,
131
+ previous: isCheckInput ? checked : value,
132
+ })
133
+
134
+ // Use useEffect to update ref instead of useMemo to avoid accessing refs during render
135
+ const [prevValue, setPrevValue] = React.useState<T | boolean | undefined>(
136
+ isCheckInput ? checked : value
137
+ )
138
+
139
+ React.useEffect(() => {
140
+ const currentValue = isCheckInput ? checked : value
141
+ if (prevValueRef.current.value !== currentValue) {
142
+ prevValueRef.current.previous = prevValueRef.current.value
143
+ prevValueRef.current.value = currentValue
144
+ setPrevValue(prevValueRef.current.previous)
145
+ }
146
+ }, [isCheckInput, value, checked])
147
+
148
+ const [controlSize, setControlSize] = React.useState<{
149
+ width?: number
150
+ height?: number
151
+ }>({})
152
+
153
+ React.useLayoutEffect(() => {
154
+ if (!control) {
155
+ setControlSize({})
156
+ return
157
+ }
158
+
159
+ setControlSize({
160
+ width: control.offsetWidth,
161
+ height: control.offsetHeight,
162
+ })
163
+
164
+ if (typeof window === "undefined") return
165
+
166
+ const resizeObserver = new ResizeObserver((entries) => {
167
+ if (!Array.isArray(entries) || !entries.length) return
168
+
169
+ const entry = entries[0]
170
+ if (!entry) return
171
+
172
+ let width: number
173
+ let height: number
174
+
175
+ if ("borderBoxSize" in entry) {
176
+ const borderSizeEntry = entry.borderBoxSize
177
+ const borderSize = Array.isArray(borderSizeEntry)
178
+ ? borderSizeEntry[0]
179
+ : borderSizeEntry
180
+ width = borderSize.inlineSize
181
+ height = borderSize.blockSize
182
+ } else {
183
+ width = control.offsetWidth
184
+ height = control.offsetHeight
185
+ }
186
+
187
+ setControlSize({ width, height })
188
+ })
189
+
190
+ resizeObserver.observe(control, { box: "border-box" })
191
+ return () => {
192
+ resizeObserver.disconnect()
193
+ }
194
+ }, [control])
195
+
196
+ React.useEffect(() => {
197
+ const input = inputRef.current
198
+ if (!input) return
199
+
200
+ const inputProto = window.HTMLInputElement.prototype
201
+ const propertyKey = isCheckInput ? "checked" : "value"
202
+ const eventType = isCheckInput ? "click" : "input"
203
+ const currentValue = isCheckInput ? checked : value
204
+
205
+ const serializedCurrentValue = isCheckInput
206
+ ? checked
207
+ : typeof value === "object" && value !== null
208
+ ? JSON.stringify(value)
209
+ : value
210
+
211
+ const descriptor = Object.getOwnPropertyDescriptor(inputProto, propertyKey)
212
+
213
+ const setter = descriptor?.set
214
+
215
+ if (prevValue !== currentValue && setter) {
216
+ const event = new Event(eventType, { bubbles })
217
+ setter.call(input, serializedCurrentValue)
218
+ input.dispatchEvent(event)
219
+ }
220
+ }, [prevValue, value, checked, bubbles, isCheckInput])
221
+
222
+ const composedStyle = React.useMemo<React.CSSProperties>(() => {
223
+ return {
224
+ ...style,
225
+ ...(controlSize.width !== undefined && controlSize.height !== undefined
226
+ ? controlSize
227
+ : {}),
228
+ border: 0,
229
+ clip: "rect(0 0 0 0)",
230
+ clipPath: "inset(50%)",
231
+ height: "1px",
232
+ margin: "-1px",
233
+ overflow: "hidden",
234
+ padding: 0,
235
+ position: "absolute",
236
+ whiteSpace: "nowrap",
237
+ width: "1px",
238
+ }
239
+ }, [style, controlSize])
240
+
241
+ return (
242
+ <input
243
+ type={type}
244
+ {...inputProps}
245
+ ref={inputRef}
246
+ aria-hidden={isCheckInput}
247
+ tabIndex={-1}
248
+ defaultChecked={isCheckInput ? checked : undefined}
249
+ style={composedStyle}
250
+ />
251
+ )
252
+ }
253
+
254
+ /**
255
+ * @see https://gist.github.com/bkrmendy/f4582173f50fab209ddfef1377ab31e3
256
+ */
257
+ interface EyeDropper {
258
+ open: (options?: { signal?: AbortSignal }) => Promise<{ sRGBHex: string }>
259
+ }
260
+
261
+ declare global {
262
+ interface Window {
263
+ EyeDropper?: {
264
+ new (): EyeDropper
265
+ }
266
+ }
267
+ }
268
+
269
+ const colorFormats = ["hex", "rgb", "hsl", "hsb"] as const
270
+ type ColorFormat = (typeof colorFormats)[number]
271
+ const colorPresets = [
272
+ { label: "Primary", value: "#1F3368" },
273
+ { label: "Secondary", value: "#A71B29" },
274
+ // --muted in light theme is #F5F5F5, /50 -> alpha 0.5
275
+ { label: "bg-muted/50", value: "#F5F5F5", alpha: 0.5 },
276
+ ] as const
277
+
278
+ interface ColorValue {
279
+ r: number
280
+ g: number
281
+ b: number
282
+ a: number
283
+ }
284
+
285
+ interface HSVColorValue {
286
+ h: number
287
+ s: number
288
+ v: number
289
+ a: number
290
+ }
291
+
292
+ function hexToRgb(hex: string, alpha?: number): ColorValue {
293
+ const trimmed = hex.trim().replace(/^#/, "")
294
+
295
+ // 3-digit hex
296
+ if (trimmed.length === 3) {
297
+ const r = Number.parseInt((trimmed[0] ?? "") + (trimmed[0] ?? ""), 16)
298
+ const g = Number.parseInt((trimmed[1] ?? "") + (trimmed[1] ?? ""), 16)
299
+ const b = Number.parseInt((trimmed[2] ?? "") + (trimmed[2] ?? ""), 16)
300
+ return { r, g, b, a: alpha ?? 1 }
301
+ }
302
+
303
+ // 6-digit hex
304
+ if (trimmed.length === 6) {
305
+ const r = Number.parseInt(trimmed.slice(0, 2), 16)
306
+ const g = Number.parseInt(trimmed.slice(2, 4), 16)
307
+ const b = Number.parseInt(trimmed.slice(4, 6), 16)
308
+ return { r, g, b, a: alpha ?? 1 }
309
+ }
310
+
311
+ // Default black for invalid input
312
+ return { r: 0, g: 0, b: 0, a: alpha ?? 1 }
313
+ }
314
+
315
+ function rgbToHex(color: ColorValue): string {
316
+ const toHex = (n: number) => {
317
+ const hex = Math.round(n).toString(16)
318
+ return hex.length === 1 ? `0${hex}` : hex
319
+ }
320
+ return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`
321
+ }
322
+
323
+ function rgbToHsv(color: ColorValue): HSVColorValue {
324
+ const r = color.r / 255
325
+ const g = color.g / 255
326
+ const b = color.b / 255
327
+
328
+ const max = Math.max(r, g, b)
329
+ const min = Math.min(r, g, b)
330
+ const diff = max - min
331
+
332
+ let h = 0
333
+ if (diff !== 0) {
334
+ switch (max) {
335
+ case r:
336
+ h = ((g - b) / diff) % 6
337
+ break
338
+ case g:
339
+ h = (b - r) / diff + 2
340
+ break
341
+ case b:
342
+ h = (r - g) / diff + 4
343
+ break
344
+ }
345
+ }
346
+ h = Math.round(h * 60)
347
+ if (h < 0) h += 360
348
+
349
+ const s = max === 0 ? 0 : diff / max
350
+ const v = max
351
+
352
+ return {
353
+ h,
354
+ s: Math.round(s * 100),
355
+ v: Math.round(v * 100),
356
+ a: color.a,
357
+ }
358
+ }
359
+
360
+ function hsvToRgb(hsv: HSVColorValue): ColorValue {
361
+ const h = hsv.h / 360
362
+ const s = hsv.s / 100
363
+ const v = hsv.v / 100
364
+
365
+ const i = Math.floor(h * 6)
366
+ const f = h * 6 - i
367
+ const p = v * (1 - s)
368
+ const q = v * (1 - f * s)
369
+ const t = v * (1 - (1 - f) * s)
370
+
371
+ let r: number
372
+ let g: number
373
+ let b: number
374
+
375
+ switch (i % 6) {
376
+ case 0: {
377
+ r = v
378
+ g = t
379
+ b = p
380
+ break
381
+ }
382
+ case 1: {
383
+ r = q
384
+ g = v
385
+ b = p
386
+ break
387
+ }
388
+ case 2: {
389
+ r = p
390
+ g = v
391
+ b = t
392
+ break
393
+ }
394
+ case 3: {
395
+ r = p
396
+ g = q
397
+ b = v
398
+ break
399
+ }
400
+ case 4: {
401
+ r = t
402
+ g = p
403
+ b = v
404
+ break
405
+ }
406
+ case 5: {
407
+ r = v
408
+ g = p
409
+ b = q
410
+ break
411
+ }
412
+ default: {
413
+ r = 0
414
+ g = 0
415
+ b = 0
416
+ }
417
+ }
418
+
419
+ return {
420
+ r: Math.round(r * 255),
421
+ g: Math.round(g * 255),
422
+ b: Math.round(b * 255),
423
+ a: hsv.a,
424
+ }
425
+ }
426
+
427
+ function colorToString(color: ColorValue, format: ColorFormat = "hex"): string {
428
+ switch (format) {
429
+ case "hex":
430
+ return rgbToHex(color)
431
+ case "rgb":
432
+ return color.a < 1
433
+ ? `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`
434
+ : `rgb(${color.r}, ${color.g}, ${color.b})`
435
+ case "hsl": {
436
+ const hsl = rgbToHsl(color)
437
+ return color.a < 1
438
+ ? `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${color.a})`
439
+ : `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`
440
+ }
441
+ case "hsb": {
442
+ const hsv = rgbToHsv(color)
443
+ return color.a < 1
444
+ ? `hsba(${hsv.h}, ${hsv.s}%, ${hsv.v}%, ${color.a})`
445
+ : `hsb(${hsv.h}, ${hsv.s}%, ${hsv.v}%)`
446
+ }
447
+ default:
448
+ return rgbToHex(color)
449
+ }
450
+ }
451
+
452
+ function rgbToHsl(color: ColorValue) {
453
+ const r = color.r / 255
454
+ const g = color.g / 255
455
+ const b = color.b / 255
456
+
457
+ const max = Math.max(r, g, b)
458
+ const min = Math.min(r, g, b)
459
+ const diff = max - min
460
+ const sum = max + min
461
+
462
+ const l = sum / 2
463
+
464
+ let h = 0
465
+ let s = 0
466
+
467
+ if (diff !== 0) {
468
+ s = l > 0.5 ? diff / (2 - sum) : diff / sum
469
+
470
+ if (max === r) {
471
+ h = (g - b) / diff + (g < b ? 6 : 0)
472
+ } else if (max === g) {
473
+ h = (b - r) / diff + 2
474
+ } else if (max === b) {
475
+ h = (r - g) / diff + 4
476
+ }
477
+ h /= 6
478
+ }
479
+
480
+ return {
481
+ h: Math.round(h * 360),
482
+ s: Math.round(s * 100),
483
+ l: Math.round(l * 100),
484
+ }
485
+ }
486
+
487
+ function hslToRgb(
488
+ hsl: { h: number; s: number; l: number },
489
+ alpha = 1
490
+ ): ColorValue {
491
+ const h = hsl.h / 360
492
+ const s = hsl.s / 100
493
+ const l = hsl.l / 100
494
+
495
+ const c = (1 - Math.abs(2 * l - 1)) * s
496
+ const x = c * (1 - Math.abs(((h * 6) % 2) - 1))
497
+ const m = l - c / 2
498
+
499
+ let r = 0
500
+ let g = 0
501
+ let b = 0
502
+
503
+ if (h >= 0 && h < 1 / 6) {
504
+ r = c
505
+ g = x
506
+ b = 0
507
+ } else if (h >= 1 / 6 && h < 2 / 6) {
508
+ r = x
509
+ g = c
510
+ b = 0
511
+ } else if (h >= 2 / 6 && h < 3 / 6) {
512
+ r = 0
513
+ g = c
514
+ b = x
515
+ } else if (h >= 3 / 6 && h < 4 / 6) {
516
+ r = 0
517
+ g = x
518
+ b = c
519
+ } else if (h >= 4 / 6 && h < 5 / 6) {
520
+ r = x
521
+ g = 0
522
+ b = c
523
+ } else if (h >= 5 / 6 && h < 1) {
524
+ r = c
525
+ g = 0
526
+ b = x
527
+ }
528
+
529
+ return {
530
+ r: Math.round((r + m) * 255),
531
+ g: Math.round((g + m) * 255),
532
+ b: Math.round((b + m) * 255),
533
+ a: alpha,
534
+ }
535
+ }
536
+
537
+ function parseColorString(value: string): ColorValue | null {
538
+ const trimmed = value.trim()
539
+
540
+ // Parse hex colors
541
+ if (trimmed.startsWith("#")) {
542
+ const hexMatch = trimmed.match(/^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$/)
543
+ if (hexMatch) {
544
+ return hexToRgb(trimmed)
545
+ }
546
+ }
547
+
548
+ // Parse rgb/rgba colors
549
+ const rgbMatch = trimmed.match(
550
+ /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)$/
551
+ )
552
+ if (rgbMatch) {
553
+ return {
554
+ r: Number.parseInt(rgbMatch[1] ?? "0", 10),
555
+ g: Number.parseInt(rgbMatch[2] ?? "0", 10),
556
+ b: Number.parseInt(rgbMatch[3] ?? "0", 10),
557
+ a: rgbMatch[4] ? Number.parseFloat(rgbMatch[4]) : 1,
558
+ }
559
+ }
560
+
561
+ // Parse hsl/hsla colors
562
+ const hslMatch = trimmed.match(
563
+ /^hsla?\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([\d.]+))?\s*\)$/
564
+ )
565
+ if (hslMatch) {
566
+ const h = Number.parseInt(hslMatch[1] ?? "0", 10)
567
+ const s = Number.parseInt(hslMatch[2] ?? "0", 10) / 100
568
+ const l = Number.parseInt(hslMatch[3] ?? "0", 10) / 100
569
+ const a = hslMatch[4] ? Number.parseFloat(hslMatch[4]) : 1
570
+
571
+ // Convert HSL to RGB
572
+ const c = (1 - Math.abs(2 * l - 1)) * s
573
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
574
+ const m = l - c / 2
575
+
576
+ let r = 0
577
+ let g = 0
578
+ let b = 0
579
+
580
+ if (h >= 0 && h < 60) {
581
+ r = c
582
+ g = x
583
+ b = 0
584
+ } else if (h >= 60 && h < 120) {
585
+ r = x
586
+ g = c
587
+ b = 0
588
+ } else if (h >= 120 && h < 180) {
589
+ r = 0
590
+ g = c
591
+ b = x
592
+ } else if (h >= 180 && h < 240) {
593
+ r = 0
594
+ g = x
595
+ b = c
596
+ } else if (h >= 240 && h < 300) {
597
+ r = x
598
+ g = 0
599
+ b = c
600
+ } else if (h >= 300 && h < 360) {
601
+ r = c
602
+ g = 0
603
+ b = x
604
+ }
605
+
606
+ return {
607
+ r: Math.round((r + m) * 255),
608
+ g: Math.round((g + m) * 255),
609
+ b: Math.round((b + m) * 255),
610
+ a,
611
+ }
612
+ }
613
+
614
+ // Parse hsb/hsba colors
615
+ const hsbMatch = trimmed.match(
616
+ /^hsba?\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*(?:,\s*([\d.]+))?\s*\)$/
617
+ )
618
+ if (hsbMatch) {
619
+ const h = Number.parseInt(hsbMatch[1] ?? "0", 10)
620
+ const s = Number.parseInt(hsbMatch[2] ?? "0", 10)
621
+ const v = Number.parseInt(hsbMatch[3] ?? "0", 10)
622
+ const a = hsbMatch[4] ? Number.parseFloat(hsbMatch[4]) : 1
623
+
624
+ return hsvToRgb({ h, s, v, a })
625
+ }
626
+
627
+ return null
628
+ }
629
+
630
+ type Direction = "ltr" | "rtl"
631
+
632
+ const DirectionContext = React.createContext<Direction | undefined>(undefined)
633
+
634
+ function useDirection(dirProp?: Direction): Direction {
635
+ const contextDir = React.useContext(DirectionContext)
636
+ return dirProp ?? contextDir ?? "ltr"
637
+ }
638
+
639
+ function useLazyRef<T>(fn: () => T) {
640
+ const ref = React.useRef<T | null>(null)
641
+
642
+ if (ref.current === null) {
643
+ ref.current = fn()
644
+ }
645
+
646
+ return ref as React.RefObject<T>
647
+ }
648
+
649
+ interface ColorPickerStoreState {
650
+ color: ColorValue
651
+ hsv: HSVColorValue
652
+ open: boolean
653
+ format: ColorFormat
654
+ }
655
+
656
+ interface ColorPickerStoreCallbacks {
657
+ onColorChange?: (colorString: string) => void
658
+ onOpenChange?: (open: boolean) => void
659
+ onFormatChange?: (format: ColorFormat) => void
660
+ }
661
+
662
+ interface ColorPickerStore {
663
+ subscribe: (cb: () => void) => () => void
664
+ getState: () => ColorPickerStoreState
665
+ setColor: (value: ColorValue) => void
666
+ setHsv: (value: HSVColorValue) => void
667
+ setOpen: (value: boolean) => void
668
+ setFormat: (value: ColorFormat) => void
669
+ notify: () => void
670
+ }
671
+
672
+ function createColorPickerStore(
673
+ listenersRef: React.RefObject<Set<() => void>>,
674
+ stateRef: React.RefObject<ColorPickerStoreState>,
675
+ callbacks?: ColorPickerStoreCallbacks
676
+ ): ColorPickerStore {
677
+ const store: ColorPickerStore = {
678
+ subscribe: (cb) => {
679
+ if (listenersRef.current) {
680
+ listenersRef.current.add(cb)
681
+ return () => listenersRef.current?.delete(cb)
682
+ }
683
+ return () => {}
684
+ },
685
+ getState: () =>
686
+ stateRef.current || {
687
+ color: { r: 0, g: 0, b: 0, a: 1 },
688
+ hsv: { h: 0, s: 0, v: 0, a: 1 },
689
+ open: false,
690
+ format: "hex" as ColorFormat,
691
+ },
692
+ setColor: (value: ColorValue) => {
693
+ if (!stateRef.current) return
694
+ if (Object.is(stateRef.current.color, value)) return
695
+
696
+ const prevState = { ...stateRef.current }
697
+ stateRef.current.color = value
698
+
699
+ if (callbacks?.onColorChange) {
700
+ const colorString = colorToString(value, prevState.format)
701
+ callbacks.onColorChange(colorString)
702
+ }
703
+
704
+ store.notify()
705
+ },
706
+ setHsv: (value: HSVColorValue) => {
707
+ if (!stateRef.current) return
708
+ if (Object.is(stateRef.current.hsv, value)) return
709
+
710
+ const prevState = { ...stateRef.current }
711
+ stateRef.current.hsv = value
712
+
713
+ if (callbacks?.onColorChange) {
714
+ const colorValue = hsvToRgb(value)
715
+ const colorString = colorToString(colorValue, prevState.format)
716
+ callbacks.onColorChange(colorString)
717
+ }
718
+
719
+ store.notify()
720
+ },
721
+ setOpen: (value: boolean) => {
722
+ if (!stateRef.current) return
723
+ if (Object.is(stateRef.current.open, value)) return
724
+
725
+ stateRef.current.open = value
726
+
727
+ if (callbacks?.onOpenChange) {
728
+ callbacks.onOpenChange(value)
729
+ }
730
+
731
+ store.notify()
732
+ },
733
+ setFormat: (value: ColorFormat) => {
734
+ if (!stateRef.current) return
735
+ if (Object.is(stateRef.current.format, value)) return
736
+
737
+ stateRef.current.format = value
738
+
739
+ if (callbacks?.onFormatChange) {
740
+ callbacks.onFormatChange(value)
741
+ }
742
+
743
+ store.notify()
744
+ },
745
+ notify: () => {
746
+ if (listenersRef.current) {
747
+ for (const cb of listenersRef.current) {
748
+ cb()
749
+ }
750
+ }
751
+ },
752
+ }
753
+
754
+ return store
755
+ }
756
+
757
+ function useColorPickerStoreContext(consumerName: string) {
758
+ const context = React.useContext(ColorPickerStoreContext)
759
+ if (!context) {
760
+ throw new Error(
761
+ `\`${consumerName}\` must be used within \`ColorPickerRoot\``
762
+ )
763
+ }
764
+ return context
765
+ }
766
+
767
+ function useColorPickerStore<U>(
768
+ selector: (state: ColorPickerStoreState) => U
769
+ ): U {
770
+ const store = useColorPickerStoreContext("useColorPickerStoreSelector")
771
+
772
+ const getSnapshot = React.useCallback(
773
+ () => selector(store.getState()),
774
+ [store, selector]
775
+ )
776
+
777
+ return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
778
+ }
779
+
780
+ interface ColorPickerContextValue {
781
+ dir: Direction
782
+ disabled?: boolean
783
+ inline?: boolean
784
+ readOnly?: boolean
785
+ required?: boolean
786
+ }
787
+
788
+ const ColorPickerStoreContext = React.createContext<ColorPickerStore | null>(
789
+ null
790
+ )
791
+ const ColorPickerContext = React.createContext<ColorPickerContextValue | null>(
792
+ null
793
+ )
794
+
795
+ function useColorPickerContext(consumerName: string) {
796
+ const context = React.useContext(ColorPickerContext)
797
+ if (!context) {
798
+ throw new Error(
799
+ `\`${consumerName}\` must be used within \`ColorPickerRoot\``
800
+ )
801
+ }
802
+ return context
803
+ }
804
+
805
+ interface ColorPickerRootProps
806
+ extends Omit<React.ComponentProps<"div">, "onValueChange">,
807
+ Pick<
808
+ React.ComponentProps<typeof Popover>,
809
+ "defaultOpen" | "open" | "onOpenChange" | "modal"
810
+ > {
811
+ value?: string
812
+ defaultValue?: string
813
+ onValueChange?: (value: string) => void
814
+ dir?: Direction
815
+ format?: ColorFormat
816
+ defaultFormat?: ColorFormat
817
+ onFormatChange?: (format: ColorFormat) => void
818
+ name?: string
819
+ asChild?: boolean
820
+ disabled?: boolean
821
+ inline?: boolean
822
+ readOnly?: boolean
823
+ required?: boolean
824
+ }
825
+
826
+ const ColorPickerRoot = React.memo(function ColorPickerRoot(props: ColorPickerRootProps) {
827
+ const {
828
+ value: valueProp,
829
+ defaultValue = "#000000",
830
+ onValueChange,
831
+ format: formatProp,
832
+ defaultFormat = "hex",
833
+ onFormatChange,
834
+ defaultOpen,
835
+ open: openProp,
836
+ onOpenChange,
837
+ name,
838
+ disabled,
839
+ inline,
840
+ readOnly,
841
+ required,
842
+ ...rootProps
843
+ } = props
844
+
845
+ const initialColor = React.useMemo(() => {
846
+ const colorString = valueProp ?? defaultValue
847
+ const color = parseColorString(colorString) ?? hexToRgb(defaultValue)
848
+
849
+ return {
850
+ color,
851
+ hsv: rgbToHsv(color),
852
+ open: openProp ?? defaultOpen ?? false,
853
+ format: formatProp ?? defaultFormat,
854
+ }
855
+ }, [
856
+ valueProp,
857
+ defaultValue,
858
+ formatProp,
859
+ defaultFormat,
860
+ openProp,
861
+ defaultOpen,
862
+ ])
863
+
864
+ const stateRef = useLazyRef(() => initialColor)
865
+ const listenersRef = useLazyRef(() => new Set<() => void>())
866
+
867
+ const storeCallbacks = React.useMemo<ColorPickerStoreCallbacks>(
868
+ () => ({
869
+ onColorChange: onValueChange,
870
+ onOpenChange: onOpenChange,
871
+ onFormatChange: onFormatChange,
872
+ }),
873
+ [onValueChange, onOpenChange, onFormatChange]
874
+ )
875
+
876
+ const store = React.useMemo(
877
+ () => createColorPickerStore(listenersRef, stateRef, storeCallbacks),
878
+ [listenersRef, stateRef, storeCallbacks]
879
+ )
880
+
881
+ return (
882
+ <ColorPickerStoreContext.Provider value={store}>
883
+ <ColorPickerRootImpl
884
+ {...rootProps}
885
+ value={valueProp}
886
+ defaultOpen={defaultOpen}
887
+ open={openProp}
888
+ onOpenChange={onOpenChange}
889
+ name={name}
890
+ disabled={disabled}
891
+ inline={inline}
892
+ readOnly={readOnly}
893
+ required={required}
894
+ />
895
+ </ColorPickerStoreContext.Provider>
896
+ )
897
+ })
898
+
899
+ // Type alias instead of empty interface to avoid lint error
900
+ type ColorPickerRootImplProps = Omit<
901
+ ColorPickerRootProps,
902
+ | "defaultValue"
903
+ | "onValueChange"
904
+ | "format"
905
+ | "defaultFormat"
906
+ | "onFormatChange"
907
+ >
908
+
909
+ function ColorPickerRootImpl(props: ColorPickerRootImplProps) {
910
+ const {
911
+ value: valueProp,
912
+ dir: dirProp,
913
+ defaultOpen,
914
+ open: openProp,
915
+ onOpenChange,
916
+ name,
917
+ ref,
918
+ asChild,
919
+ disabled,
920
+ inline,
921
+ modal,
922
+ readOnly,
923
+ required,
924
+ ...rootProps
925
+ } = props
926
+
927
+ const store = useColorPickerStoreContext("ColorPickerRootImpl")
928
+
929
+ const dir = useDirection(dirProp)
930
+
931
+ const [formTrigger, setFormTrigger] = React.useState<HTMLDivElement | null>(
932
+ null
933
+ )
934
+ const composedRef = useComposedRefs(ref, (node) => setFormTrigger(node))
935
+
936
+ const isFormControl = formTrigger ? !!formTrigger.closest("form") : true
937
+
938
+ React.useEffect(() => {
939
+ if (valueProp !== undefined) {
940
+ const color = parseColorString(valueProp)
941
+
942
+ // Only update if it's a valid color string (not "inherit" or others)
943
+ if (color) {
944
+ const hsv = rgbToHsv(color)
945
+ store.setColor(color)
946
+ store.setHsv(hsv)
947
+ }
948
+ }
949
+ }, [valueProp, store])
950
+
951
+ React.useEffect(() => {
952
+ if (openProp !== undefined) {
953
+ store.setOpen(openProp)
954
+ }
955
+ }, [openProp, store])
956
+
957
+ const contextValue = React.useMemo<ColorPickerContextValue>(
958
+ () => ({
959
+ dir,
960
+ disabled,
961
+ inline,
962
+ readOnly,
963
+ required,
964
+ }),
965
+ [dir, disabled, inline, readOnly, required]
966
+ )
967
+
968
+ const value = useColorPickerStore((state) => rgbToHex(state.color))
969
+
970
+ const open = useColorPickerStore((state) => state.open)
971
+
972
+ const onPopoverOpenChange = React.useCallback(
973
+ (newOpen: boolean) => {
974
+ store.setOpen(newOpen)
975
+ onOpenChange?.(newOpen)
976
+ },
977
+ [store, onOpenChange]
978
+ )
979
+
980
+ const RootPrimitive = asChild ? Slot : "div"
981
+
982
+ if (inline) {
983
+ return (
984
+ <ColorPickerContext.Provider value={contextValue}>
985
+ <RootPrimitive {...rootProps} ref={composedRef} />
986
+ {isFormControl && (
987
+ <VisuallyHiddenInput
988
+ type="hidden"
989
+ control={formTrigger}
990
+ name={name}
991
+ value={value}
992
+ disabled={disabled}
993
+ readOnly={readOnly}
994
+ required={required}
995
+ />
996
+ )}
997
+ </ColorPickerContext.Provider>
998
+ )
999
+ }
1000
+
1001
+ return (
1002
+ <ColorPickerContext.Provider value={contextValue}>
1003
+ <Popover
1004
+ defaultOpen={defaultOpen}
1005
+ open={open}
1006
+ onOpenChange={onPopoverOpenChange}
1007
+ modal={modal}
1008
+ >
1009
+ <RootPrimitive {...rootProps} ref={composedRef} />
1010
+ {isFormControl && (
1011
+ <VisuallyHiddenInput
1012
+ type="hidden"
1013
+ control={formTrigger}
1014
+ name={name}
1015
+ value={value}
1016
+ disabled={disabled}
1017
+ readOnly={readOnly}
1018
+ required={required}
1019
+ />
1020
+ )}
1021
+ </Popover>
1022
+ </ColorPickerContext.Provider>
1023
+ )
1024
+ }
1025
+
1026
+ // Type alias instead of empty interface to avoid lint error
1027
+ type ColorPickerTriggerProps = React.ComponentProps<typeof PopoverTrigger>
1028
+
1029
+ function ColorPickerTrigger(props: ColorPickerTriggerProps) {
1030
+ const { asChild, ...triggerProps } = props
1031
+ const context = useColorPickerContext("ColorPickerTrigger")
1032
+
1033
+ const TriggerPrimitive = asChild ? Slot : Button
1034
+
1035
+ return (
1036
+ <PopoverTrigger asChild disabled={context.disabled}>
1037
+ <TriggerPrimitive data-slot="color-picker-trigger" {...triggerProps} />
1038
+ </PopoverTrigger>
1039
+ )
1040
+ }
1041
+
1042
+ // Type alias instead of empty interface to avoid lint error
1043
+ type ColorPickerContentProps = React.ComponentProps<typeof PopoverContent> & { asChild?: boolean }
1044
+
1045
+ function ColorPickerContent(props: ColorPickerContentProps) {
1046
+ const { asChild, className, children, ...popoverContentProps } = props
1047
+ const context = useColorPickerContext("ColorPickerContent")
1048
+
1049
+ if (context.inline) {
1050
+ const ContentPrimitive = asChild ? Slot : "div"
1051
+
1052
+ return (
1053
+ <ContentPrimitive
1054
+ data-slot="color-picker-content"
1055
+ {...popoverContentProps}
1056
+ className={cn("editor-color-picker-content", className)}
1057
+ >
1058
+ {children}
1059
+ </ContentPrimitive>
1060
+ )
1061
+ }
1062
+
1063
+ return (
1064
+ <PopoverContent
1065
+ data-slot="color-picker-content"
1066
+ {...popoverContentProps}
1067
+ className={cn("editor-color-picker-content", className)}
1068
+ >
1069
+ {children}
1070
+ </PopoverContent>
1071
+ )
1072
+ }
1073
+
1074
+ interface ColorPickerAreaProps extends React.ComponentProps<"div"> {
1075
+ asChild?: boolean
1076
+ }
1077
+
1078
+ function ColorPickerArea(props: ColorPickerAreaProps) {
1079
+ const { asChild, className, ref, ...areaProps } = props
1080
+ const context = useColorPickerContext("ColorPickerArea")
1081
+ const store = useColorPickerStoreContext("ColorPickerArea")
1082
+
1083
+ const hsv = useColorPickerStore((state) => state.hsv)
1084
+
1085
+ const isDraggingRef = React.useRef(false)
1086
+ const areaRef = React.useRef<HTMLDivElement>(null)
1087
+ const composedRef = useComposedRefs(ref, areaRef)
1088
+
1089
+ const updateColorFromPosition = React.useCallback(
1090
+ (clientX: number, clientY: number) => {
1091
+ if (!areaRef.current) return
1092
+
1093
+ const rect = areaRef.current.getBoundingClientRect()
1094
+ const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
1095
+ const y = Math.max(0, Math.min(1, 1 - (clientY - rect.top) / rect.height))
1096
+
1097
+ const newHsv: HSVColorValue = {
1098
+ h: hsv?.h ?? 0,
1099
+ s: Math.round(x * 100),
1100
+ v: Math.round(y * 100),
1101
+ a: hsv?.a ?? 1,
1102
+ }
1103
+
1104
+ store.setHsv(newHsv)
1105
+ store.setColor(hsvToRgb(newHsv))
1106
+ },
1107
+ [hsv, store]
1108
+ )
1109
+
1110
+ const onPointerDown = React.useCallback(
1111
+ (event: React.PointerEvent) => {
1112
+ if (context.disabled) return
1113
+ event.preventDefault()
1114
+
1115
+ isDraggingRef.current = true
1116
+ areaRef.current?.setPointerCapture(event.pointerId)
1117
+ updateColorFromPosition(event.clientX, event.clientY)
1118
+ },
1119
+ [context.disabled, updateColorFromPosition]
1120
+ )
1121
+
1122
+ const onPointerMove = React.useCallback(
1123
+ (event: React.PointerEvent) => {
1124
+ if (isDraggingRef.current) {
1125
+ updateColorFromPosition(event.clientX, event.clientY)
1126
+ }
1127
+ },
1128
+ [updateColorFromPosition]
1129
+ )
1130
+
1131
+ const onPointerUp = React.useCallback((event: React.PointerEvent) => {
1132
+ isDraggingRef.current = false
1133
+ areaRef.current?.releasePointerCapture(event.pointerId)
1134
+ }, [])
1135
+
1136
+ const hue = hsv?.h ?? 0
1137
+ const backgroundHue = hsvToRgb({ h: hue, s: 100, v: 100, a: 1 })
1138
+
1139
+ const AreaPrimitive = asChild ? Slot : "div"
1140
+
1141
+ return (
1142
+ <AreaPrimitive
1143
+ data-slot="color-picker-area"
1144
+ {...areaProps}
1145
+ className={cn(
1146
+ "editor-color-picker-area",
1147
+ context.disabled && "pointer-events-none opacity-50",
1148
+ className
1149
+ )}
1150
+ ref={composedRef}
1151
+ onPointerDown={onPointerDown}
1152
+ onPointerMove={onPointerMove}
1153
+ onPointerUp={onPointerUp}
1154
+ >
1155
+ <div className="editor-absolute-full editor-overflow-hidden editor-rounded-sm">
1156
+ <div
1157
+ className="editor-absolute-full"
1158
+ style={{
1159
+ backgroundColor: `rgb(${backgroundHue.r}, ${backgroundHue.g}, ${backgroundHue.b})`,
1160
+ }}
1161
+ />
1162
+ <div
1163
+ className="editor-absolute-full"
1164
+ style={{
1165
+ background: "linear-gradient(to right, #fff, transparent)",
1166
+ }}
1167
+ />
1168
+ <div
1169
+ className="editor-absolute-full"
1170
+ style={{
1171
+ background: "linear-gradient(to bottom, transparent, #000)",
1172
+ }}
1173
+ />
1174
+ </div>
1175
+ <div
1176
+ className="editor-color-thumb-indicator"
1177
+ style={{
1178
+ left: `${hsv?.s ?? 0}%`,
1179
+ top: `${100 - (hsv?.v ?? 0)}%`,
1180
+ }}
1181
+ />
1182
+ </AreaPrimitive>
1183
+ )
1184
+ }
1185
+
1186
+ // Type alias instead of empty interface to avoid lint error
1187
+ type ColorPickerHueSliderProps = React.ComponentProps<typeof SliderPrimitive.Root>
1188
+
1189
+ function ColorPickerHueSlider(props: ColorPickerHueSliderProps) {
1190
+ const { className, ...sliderProps } = props
1191
+ const context = useColorPickerContext("ColorPickerHueSlider")
1192
+ const store = useColorPickerStoreContext("ColorPickerHueSlider")
1193
+
1194
+ const hsv = useColorPickerStore((state) => state.hsv)
1195
+
1196
+ const onValueChange = React.useCallback(
1197
+ (values: number[]) => {
1198
+ const newHsv: HSVColorValue = {
1199
+ h: values[0] ?? 0,
1200
+ s: hsv?.s ?? 0,
1201
+ v: hsv?.v ?? 0,
1202
+ a: hsv?.a ?? 1,
1203
+ }
1204
+ store.setHsv(newHsv)
1205
+ store.setColor(hsvToRgb(newHsv))
1206
+ },
1207
+ [hsv, store]
1208
+ )
1209
+
1210
+ return (
1211
+ <SliderPrimitive.Root
1212
+ data-slot="color-picker-hue-slider"
1213
+ {...sliderProps}
1214
+ max={360}
1215
+ step={1}
1216
+ className={cn(
1217
+ "editor-slider-root",
1218
+ className
1219
+ )}
1220
+ value={[hsv?.h ?? 0]}
1221
+ onValueChange={onValueChange}
1222
+ disabled={context.disabled}
1223
+ >
1224
+ <SliderPrimitive.Track className="editor-slider-track editor-hue-slider-track">
1225
+ <SliderPrimitive.Range className="editor-absolute editor-h-full" />
1226
+ </SliderPrimitive.Track>
1227
+ <SliderPrimitive.Thumb className="editor-slider-thumb" />
1228
+ </SliderPrimitive.Root>
1229
+ )
1230
+ }
1231
+
1232
+ // Type alias instead of empty interface to avoid lint error
1233
+ type ColorPickerAlphaSliderProps = React.ComponentProps<typeof SliderPrimitive.Root>
1234
+
1235
+ function ColorPickerAlphaSlider(props: ColorPickerAlphaSliderProps) {
1236
+ const { className, ...sliderProps } = props
1237
+ const context = useColorPickerContext("ColorPickerAlphaSlider")
1238
+ const store = useColorPickerStoreContext("ColorPickerAlphaSlider")
1239
+
1240
+ const color = useColorPickerStore((state) => state.color)
1241
+ const hsv = useColorPickerStore((state) => state.hsv)
1242
+
1243
+ const onValueChange = React.useCallback(
1244
+ (values: number[]) => {
1245
+ const alpha = (values[0] ?? 0) / 100
1246
+ const newColor = { ...color, a: alpha }
1247
+ const newHsv = { ...hsv, a: alpha }
1248
+ store.setColor(newColor)
1249
+ store.setHsv(newHsv)
1250
+ },
1251
+ [color, hsv, store]
1252
+ )
1253
+
1254
+ const gradientColor = `rgb(${color?.r ?? 0}, ${color?.g ?? 0}, ${color?.b ?? 0})`
1255
+
1256
+ return (
1257
+ <SliderPrimitive.Root
1258
+ data-slot="color-picker-alpha-slider"
1259
+ {...sliderProps}
1260
+ max={100}
1261
+ step={1}
1262
+ disabled={context.disabled}
1263
+ className={cn(
1264
+ "editor-slider-root",
1265
+ className
1266
+ )}
1267
+ value={[Math.round((color?.a ?? 1) * 100)]}
1268
+ onValueChange={onValueChange}
1269
+ >
1270
+ <SliderPrimitive.Track
1271
+ className="editor-slider-track"
1272
+ style={{
1273
+ background:
1274
+ "linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)",
1275
+ backgroundSize: "8px 8px",
1276
+ backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0px",
1277
+ }}
1278
+ >
1279
+ <div
1280
+ className="editor-absolute-full editor-rounded-full"
1281
+ style={{
1282
+ background: `linear-gradient(to right, transparent, ${gradientColor})`,
1283
+ }}
1284
+ />
1285
+ <SliderPrimitive.Range className="editor-absolute editor-h-full" />
1286
+ </SliderPrimitive.Track>
1287
+ <SliderPrimitive.Thumb className="editor-slider-thumb" />
1288
+ </SliderPrimitive.Root>
1289
+ )
1290
+ }
1291
+
1292
+ interface ColorPickerSwatchProps extends React.ComponentProps<"div"> {
1293
+ asChild?: boolean
1294
+ }
1295
+
1296
+ function ColorPickerSwatch(props: ColorPickerSwatchProps) {
1297
+ const { asChild, className, ...swatchProps } = props
1298
+ const context = useColorPickerContext("ColorPickerSwatch")
1299
+
1300
+ const color = useColorPickerStore((state) => state.color)
1301
+ const format = useColorPickerStore((state) => state.format)
1302
+
1303
+ const backgroundStyle = React.useMemo(() => {
1304
+ if (!color) {
1305
+ return {
1306
+ background:
1307
+ "linear-gradient(to bottom right, transparent calc(50% - 1px), hsl(var(--destructive)) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) no-repeat",
1308
+ }
1309
+ }
1310
+
1311
+ const colorString = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`
1312
+
1313
+ if (color.a < 1) {
1314
+ return {
1315
+ background: `linear-gradient(${colorString}, ${colorString}), repeating-conic-gradient(#ccc 0% 25%, #fff 0% 50%) 0% 50% / 8px 8px`,
1316
+ }
1317
+ }
1318
+
1319
+ return {
1320
+ backgroundColor: colorString,
1321
+ }
1322
+ }, [color])
1323
+
1324
+ const ariaLabel = !color
1325
+ ? "No color selected"
1326
+ : `Current color: ${colorToString(color, format)}`
1327
+
1328
+ const SwatchPrimitive = asChild ? Slot : "div"
1329
+
1330
+ return (
1331
+ <SwatchPrimitive
1332
+ role="img"
1333
+ aria-label={ariaLabel}
1334
+ data-slot="color-picker-swatch"
1335
+ {...swatchProps}
1336
+ className={cn(
1337
+ "editor-color-swatch",
1338
+ context.disabled && "editor-color-swatch--disabled",
1339
+ className
1340
+ )}
1341
+ style={{
1342
+ ...backgroundStyle,
1343
+ forcedColorAdjust: "none",
1344
+ }}
1345
+ />
1346
+ )
1347
+ }
1348
+
1349
+ // Type alias instead of empty interface to avoid lint error
1350
+ type ColorPickerEyeDropperProps = React.ComponentProps<typeof Button>
1351
+
1352
+ function ColorPickerEyeDropper(props: ColorPickerEyeDropperProps) {
1353
+ const { children, size, ...buttonProps } = props
1354
+ const context = useColorPickerContext("ColorPickerEyeDropper")
1355
+ const store = useColorPickerStoreContext("ColorPickerEyeDropper")
1356
+
1357
+ const color = useColorPickerStore((state) => state.color)
1358
+
1359
+ const onEyeDropper = React.useCallback(async () => {
1360
+ if (!window.EyeDropper) return
1361
+
1362
+ try {
1363
+ const eyeDropper = new window.EyeDropper()
1364
+ const result = await eyeDropper.open()
1365
+
1366
+ if (result.sRGBHex) {
1367
+ const currentAlpha = color?.a ?? 1
1368
+ const newColor = hexToRgb(result.sRGBHex, currentAlpha)
1369
+ const newHsv = rgbToHsv(newColor)
1370
+ store.setColor(newColor)
1371
+ store.setHsv(newHsv)
1372
+ }
1373
+ } catch (error) {
1374
+ logger.warn("EyeDropper error", { error })
1375
+ }
1376
+ }, [color, store])
1377
+
1378
+ const hasEyeDropper = typeof window !== "undefined" && !!window.EyeDropper
1379
+
1380
+ if (!hasEyeDropper) return null
1381
+
1382
+ const buttonSize = size ?? (children ? "default" : "icon")
1383
+
1384
+ return (
1385
+ <Button
1386
+ data-slot="color-picker-eye-dropper"
1387
+ {...buttonProps}
1388
+ variant="outline"
1389
+ size={buttonSize}
1390
+ onClick={onEyeDropper}
1391
+ disabled={context.disabled}
1392
+ >
1393
+ {children ?? <PipetteIcon />}
1394
+ </Button>
1395
+ )
1396
+ }
1397
+
1398
+ interface ColorPickerFormatSelectProps
1399
+ extends Omit<React.ComponentProps<typeof Select>, "value" | "onValueChange" | "children">,
1400
+ Pick<React.ComponentProps<typeof SelectTrigger>, "size" | "className"> {
1401
+ value?: ColorFormat
1402
+ }
1403
+
1404
+ function ColorPickerFormatSelect(props: ColorPickerFormatSelectProps) {
1405
+ const { size, className, ...selectProps } = props
1406
+ const context = useColorPickerContext("ColorPickerFormatSelector")
1407
+ const store = useColorPickerStoreContext("ColorPickerFormatSelector")
1408
+
1409
+ const format = useColorPickerStore((state) => state.format)
1410
+
1411
+ const onFormatChange = React.useCallback(
1412
+ (value: string) => {
1413
+ if (colorFormats.includes(value as ColorFormat)) {
1414
+ store.setFormat(value as ColorFormat)
1415
+ }
1416
+ },
1417
+ [store]
1418
+ )
1419
+
1420
+ return (
1421
+ <Select
1422
+ data-slot="color-picker-format-select"
1423
+ {...selectProps}
1424
+ value={format}
1425
+ onValueChange={onFormatChange}
1426
+ disabled={context.disabled}
1427
+ >
1428
+ <SelectTrigger
1429
+ data-slot="color-picker-format-select-trigger"
1430
+ size={size ?? "sm"}
1431
+ className={cn("editor-format-select-trigger", className)}
1432
+ >
1433
+ <SelectValue />
1434
+ </SelectTrigger>
1435
+ <SelectContent>
1436
+ {colorFormats.map((format) => (
1437
+ <SelectItem key={format} value={format}>
1438
+ {format.toUpperCase()}
1439
+ </SelectItem>
1440
+ ))}
1441
+ </SelectContent>
1442
+ </Select>
1443
+ )
1444
+ }
1445
+
1446
+ interface ColorPickerInputProps
1447
+ extends Omit<
1448
+ React.ComponentProps<typeof Input>,
1449
+ "value" | "onChange" | "color"
1450
+ > {
1451
+ withoutAlpha?: boolean
1452
+ }
1453
+
1454
+ function ColorPickerInput(props: ColorPickerInputProps) {
1455
+ const context = useColorPickerContext("ColorPickerInput")
1456
+ const store = useColorPickerStoreContext("ColorPickerInput")
1457
+
1458
+ const color = useColorPickerStore((state) => state.color)
1459
+ const format = useColorPickerStore((state) => state.format)
1460
+ const hsv = useColorPickerStore((state) => state.hsv)
1461
+
1462
+ const onColorChange = React.useCallback(
1463
+ (newColor: ColorValue) => {
1464
+ const newHsv = rgbToHsv(newColor)
1465
+ store.setColor(newColor)
1466
+ store.setHsv(newHsv)
1467
+ },
1468
+ [store]
1469
+ )
1470
+
1471
+ if (format === "hex") {
1472
+ return (
1473
+ <HexInput
1474
+ color={color}
1475
+ onColorChange={onColorChange}
1476
+ context={context}
1477
+ {...props}
1478
+ />
1479
+ )
1480
+ }
1481
+
1482
+ if (format === "rgb") {
1483
+ return (
1484
+ <RgbInput
1485
+ color={color}
1486
+ onColorChange={onColorChange}
1487
+ context={context}
1488
+ {...props}
1489
+ />
1490
+ )
1491
+ }
1492
+
1493
+ if (format === "hsl") {
1494
+ return (
1495
+ <HslInput
1496
+ color={color}
1497
+ onColorChange={onColorChange}
1498
+ context={context}
1499
+ {...props}
1500
+ />
1501
+ )
1502
+ }
1503
+
1504
+ if (format === "hsb") {
1505
+ return (
1506
+ <HsbInput
1507
+ hsv={hsv}
1508
+ onColorChange={onColorChange}
1509
+ context={context}
1510
+ {...props}
1511
+ />
1512
+ )
1513
+ }
1514
+ }
1515
+
1516
+ interface InputGroupItemProps
1517
+ extends React.ComponentProps<typeof Input> {
1518
+ position?: "first" | "middle" | "last" | "isolated"
1519
+ }
1520
+
1521
+ function InputGroupItem({
1522
+ className,
1523
+ position = "isolated",
1524
+ ...props
1525
+ }: InputGroupItemProps) {
1526
+ return (
1527
+ <Input
1528
+ data-slot="color-picker-input"
1529
+ className={cn(
1530
+ "editor-input-group-item",
1531
+ position !== "isolated" && `editor-input-group-item--${position}`,
1532
+ className
1533
+ )}
1534
+ {...props}
1535
+ />
1536
+ )
1537
+ }
1538
+
1539
+ interface FormatInputProps extends ColorPickerInputProps {
1540
+ color: ColorValue
1541
+ onColorChange: (color: ColorValue) => void
1542
+ context: ColorPickerContextValue
1543
+ }
1544
+
1545
+ function HexInput(props: FormatInputProps) {
1546
+ const {
1547
+ color,
1548
+ onColorChange,
1549
+ context,
1550
+ withoutAlpha,
1551
+ className,
1552
+ ...inputProps
1553
+ } = props
1554
+
1555
+ const hexValue = rgbToHex(color)
1556
+ const alphaValue = Math.round((color?.a ?? 1) * 100)
1557
+
1558
+ const onHexChange = React.useCallback(
1559
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1560
+ const value = event.target.value
1561
+ const parsedColor = parseColorString(value)
1562
+ if (parsedColor) {
1563
+ onColorChange({ ...parsedColor, a: color?.a ?? 1 })
1564
+ }
1565
+ },
1566
+ [color, onColorChange]
1567
+ )
1568
+
1569
+ const onAlphaChange = React.useCallback(
1570
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1571
+ const value = Number.parseInt(event.target.value, 10)
1572
+ if (!Number.isNaN(value) && value >= 0 && value <= 100) {
1573
+ onColorChange({ ...color, a: value / 100 })
1574
+ }
1575
+ },
1576
+ [color, onColorChange]
1577
+ )
1578
+
1579
+ if (withoutAlpha) {
1580
+ return (
1581
+ <InputGroupItem
1582
+ aria-label="Hex color value"
1583
+ position="isolated"
1584
+ {...inputProps}
1585
+ placeholder="#000000"
1586
+ className={cn("editor-font-mono", className)}
1587
+ value={hexValue}
1588
+ onChange={onHexChange}
1589
+ disabled={context.disabled}
1590
+ />
1591
+ )
1592
+ }
1593
+
1594
+ return (
1595
+ <div
1596
+ data-slot="color-picker-input-wrapper"
1597
+ className={cn("editor-input-wrapper", className)}
1598
+ >
1599
+ <InputGroupItem
1600
+ aria-label="Hex color value"
1601
+ position="first"
1602
+ {...inputProps}
1603
+ placeholder="#000000"
1604
+ className="editor-flex-grow editor-font-mono"
1605
+ value={hexValue}
1606
+ onChange={onHexChange}
1607
+ disabled={context.disabled}
1608
+ />
1609
+ <InputGroupItem
1610
+ aria-label="Alpha transparency percentage"
1611
+ position="last"
1612
+ {...inputProps}
1613
+ placeholder="100"
1614
+ inputMode="numeric"
1615
+ pattern="[0-9]*"
1616
+ min="0"
1617
+ max="100"
1618
+ className="editor-w-14"
1619
+ value={alphaValue}
1620
+ onChange={onAlphaChange}
1621
+ disabled={context.disabled}
1622
+ />
1623
+ </div>
1624
+ )
1625
+ }
1626
+
1627
+ function RgbInput(props: FormatInputProps) {
1628
+ const {
1629
+ color,
1630
+ onColorChange,
1631
+ context,
1632
+ withoutAlpha,
1633
+ className,
1634
+ ...inputProps
1635
+ } = props
1636
+
1637
+ const rValue = Math.round(color?.r ?? 0)
1638
+ const gValue = Math.round(color?.g ?? 0)
1639
+ const bValue = Math.round(color?.b ?? 0)
1640
+ const alphaValue = Math.round((color?.a ?? 1) * 100)
1641
+
1642
+ const onChannelChange = React.useCallback(
1643
+ (channel: "r" | "g" | "b" | "a", max: number, isAlpha = false) =>
1644
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1645
+ const value = Number.parseInt(event.target.value, 10)
1646
+ if (!Number.isNaN(value) && value >= 0 && value <= max) {
1647
+ const newValue = isAlpha ? value / 100 : value
1648
+ onColorChange({ ...color, [channel]: newValue })
1649
+ }
1650
+ },
1651
+ [color, onColorChange]
1652
+ )
1653
+
1654
+ return (
1655
+ <div
1656
+ data-slot="color-picker-input-wrapper"
1657
+ className={cn("editor-input-wrapper", className)}
1658
+ >
1659
+ <InputGroupItem
1660
+ aria-label="Red color component (0-255)"
1661
+ position="first"
1662
+ {...inputProps}
1663
+ placeholder="0"
1664
+ inputMode="numeric"
1665
+ pattern="[0-9]*"
1666
+ min="0"
1667
+ max="255"
1668
+ className="editor-w-14"
1669
+ value={rValue}
1670
+ onChange={onChannelChange("r", 255)}
1671
+ disabled={context.disabled}
1672
+ />
1673
+ <InputGroupItem
1674
+ aria-label="Green color component (0-255)"
1675
+ position="middle"
1676
+ {...inputProps}
1677
+ placeholder="0"
1678
+ inputMode="numeric"
1679
+ pattern="[0-9]*"
1680
+ min="0"
1681
+ max="255"
1682
+ className="editor-w-14"
1683
+ value={gValue}
1684
+ onChange={onChannelChange("g", 255)}
1685
+ disabled={context.disabled}
1686
+ />
1687
+ <InputGroupItem
1688
+ aria-label="Blue color component (0-255)"
1689
+ position={withoutAlpha ? "last" : "middle"}
1690
+ {...inputProps}
1691
+ placeholder="0"
1692
+ inputMode="numeric"
1693
+ pattern="[0-9]*"
1694
+ min="0"
1695
+ max="255"
1696
+ className="editor-w-14"
1697
+ value={bValue}
1698
+ onChange={onChannelChange("b", 255)}
1699
+ disabled={context.disabled}
1700
+ />
1701
+ {!withoutAlpha && (
1702
+ <InputGroupItem
1703
+ aria-label="Alpha transparency percentage"
1704
+ position="last"
1705
+ {...inputProps}
1706
+ placeholder="100"
1707
+ inputMode="numeric"
1708
+ pattern="[0-9]*"
1709
+ min="0"
1710
+ max="100"
1711
+ className="editor-w-14"
1712
+ value={alphaValue}
1713
+ onChange={onChannelChange("a", 100, true)}
1714
+ disabled={context.disabled}
1715
+ />
1716
+ )}
1717
+ </div>
1718
+ )
1719
+ }
1720
+
1721
+ function HslInput(props: FormatInputProps) {
1722
+ const {
1723
+ color,
1724
+ onColorChange,
1725
+ context,
1726
+ withoutAlpha,
1727
+ className,
1728
+ ...inputProps
1729
+ } = props
1730
+
1731
+ const hsl = React.useMemo(() => rgbToHsl(color), [color])
1732
+ const alphaValue = Math.round((color?.a ?? 1) * 100)
1733
+
1734
+ const onHslChannelChange = React.useCallback(
1735
+ (channel: "h" | "s" | "l", max: number) =>
1736
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1737
+ const value = Number.parseInt(event.target.value, 10)
1738
+ if (!Number.isNaN(value) && value >= 0 && value <= max) {
1739
+ const newHsl = { ...hsl, [channel]: value }
1740
+ const newColor = hslToRgb(newHsl, color?.a ?? 1)
1741
+ onColorChange(newColor)
1742
+ }
1743
+ },
1744
+ [hsl, color, onColorChange]
1745
+ )
1746
+
1747
+ const onAlphaChange = React.useCallback(
1748
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1749
+ const value = Number.parseInt(event.target.value, 10)
1750
+ if (!Number.isNaN(value) && value >= 0 && value <= 100) {
1751
+ onColorChange({ ...color, a: value / 100 })
1752
+ }
1753
+ },
1754
+ [color, onColorChange]
1755
+ )
1756
+
1757
+ return (
1758
+ <div
1759
+ data-slot="color-picker-input-wrapper"
1760
+ className={cn("editor-input-wrapper", className)}
1761
+ >
1762
+ <InputGroupItem
1763
+ aria-label="Hue degree (0-360)"
1764
+ position="first"
1765
+ {...inputProps}
1766
+ placeholder="0"
1767
+ inputMode="numeric"
1768
+ pattern="[0-9]*"
1769
+ min="0"
1770
+ max="360"
1771
+ className="editor-w-14"
1772
+ value={hsl.h}
1773
+ onChange={onHslChannelChange("h", 360)}
1774
+ disabled={context.disabled}
1775
+ />
1776
+ <InputGroupItem
1777
+ aria-label="Saturation percentage (0-100)"
1778
+ position="middle"
1779
+ {...inputProps}
1780
+ placeholder="0"
1781
+ inputMode="numeric"
1782
+ pattern="[0-9]*"
1783
+ min="0"
1784
+ max="100"
1785
+ className="editor-w-14"
1786
+ value={hsl.s}
1787
+ onChange={onHslChannelChange("s", 100)}
1788
+ disabled={context.disabled}
1789
+ />
1790
+ <InputGroupItem
1791
+ aria-label="Lightness percentage (0-100)"
1792
+ position={withoutAlpha ? "last" : "middle"}
1793
+ {...inputProps}
1794
+ placeholder="0"
1795
+ inputMode="numeric"
1796
+ pattern="[0-9]*"
1797
+ min="0"
1798
+ max="100"
1799
+ className="editor-w-14"
1800
+ value={hsl.l}
1801
+ onChange={onHslChannelChange("l", 100)}
1802
+ disabled={context.disabled}
1803
+ />
1804
+ {!withoutAlpha && (
1805
+ <InputGroupItem
1806
+ aria-label="Alpha transparency percentage"
1807
+ position="last"
1808
+ {...inputProps}
1809
+ placeholder="100"
1810
+ inputMode="numeric"
1811
+ pattern="[0-9]*"
1812
+ min="0"
1813
+ max="100"
1814
+ className="editor-w-14"
1815
+ value={alphaValue}
1816
+ onChange={onAlphaChange}
1817
+ disabled={context.disabled}
1818
+ />
1819
+ )}
1820
+ </div>
1821
+ )
1822
+ }
1823
+
1824
+ interface HsbInputProps extends Omit<FormatInputProps, "color"> {
1825
+ hsv: HSVColorValue
1826
+ }
1827
+
1828
+ function HsbInput(props: HsbInputProps) {
1829
+ const {
1830
+ hsv,
1831
+ onColorChange,
1832
+ context,
1833
+ withoutAlpha,
1834
+ className,
1835
+ ...inputProps
1836
+ } = props
1837
+
1838
+ const alphaValue = Math.round((hsv?.a ?? 1) * 100)
1839
+
1840
+ const onHsvChannelChange = React.useCallback(
1841
+ (channel: "h" | "s" | "v", max: number) =>
1842
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1843
+ const value = Number.parseInt(event.target.value, 10)
1844
+ if (!Number.isNaN(value) && value >= 0 && value <= max) {
1845
+ const newHsv = { ...hsv, [channel]: value }
1846
+ const newColor = hsvToRgb(newHsv)
1847
+ onColorChange(newColor)
1848
+ }
1849
+ },
1850
+ [hsv, onColorChange]
1851
+ )
1852
+
1853
+ const onAlphaChange = React.useCallback(
1854
+ (event: React.ChangeEvent<HTMLInputElement>) => {
1855
+ const value = Number.parseInt(event.target.value, 10)
1856
+ if (!Number.isNaN(value) && value >= 0 && value <= 100) {
1857
+ const currentColor = hsvToRgb(hsv)
1858
+ onColorChange({ ...currentColor, a: value / 100 })
1859
+ }
1860
+ },
1861
+ [hsv, onColorChange]
1862
+ )
1863
+
1864
+ return (
1865
+ <div
1866
+ data-slot="color-picker-input-wrapper"
1867
+ className={cn("editor-input-wrapper", className)}
1868
+ >
1869
+ <InputGroupItem
1870
+ aria-label="Hue degree (0-360)"
1871
+ position="first"
1872
+ {...inputProps}
1873
+ placeholder="0"
1874
+ inputMode="numeric"
1875
+ pattern="[0-9]*"
1876
+ min="0"
1877
+ max="360"
1878
+ className="editor-w-14"
1879
+ value={hsv?.h ?? 0}
1880
+ onChange={onHsvChannelChange("h", 360)}
1881
+ disabled={context.disabled}
1882
+ />
1883
+ <InputGroupItem
1884
+ aria-label="Saturation percentage (0-100)"
1885
+ position="middle"
1886
+ {...inputProps}
1887
+ placeholder="0"
1888
+ inputMode="numeric"
1889
+ pattern="[0-9]*"
1890
+ min="0"
1891
+ max="100"
1892
+ className="editor-w-14"
1893
+ value={hsv?.s ?? 0}
1894
+ onChange={onHsvChannelChange("s", 100)}
1895
+ disabled={context.disabled}
1896
+ />
1897
+ <InputGroupItem
1898
+ aria-label="Brightness percentage (0-100)"
1899
+ position={withoutAlpha ? "last" : "middle"}
1900
+ {...inputProps}
1901
+ placeholder="0"
1902
+ inputMode="numeric"
1903
+ pattern="[0-9]*"
1904
+ min="0"
1905
+ max="100"
1906
+ className="editor-w-14"
1907
+ value={hsv?.v ?? 0}
1908
+ onChange={onHsvChannelChange("v", 100)}
1909
+ disabled={context.disabled}
1910
+ />
1911
+ {!withoutAlpha && (
1912
+ <InputGroupItem
1913
+ aria-label="Alpha transparency percentage"
1914
+ position="last"
1915
+ {...inputProps}
1916
+ placeholder="100"
1917
+ inputMode="numeric"
1918
+ pattern="[0-9]*"
1919
+ min="0"
1920
+ max="100"
1921
+ className="editor-w-14"
1922
+ value={alphaValue}
1923
+ onChange={onAlphaChange}
1924
+ disabled={context.disabled}
1925
+ />
1926
+ )}
1927
+ </div>
1928
+ )
1929
+ }
1930
+
1931
+ interface ColorPickerPresetsProps extends React.ComponentProps<"div"> {
1932
+ asChild?: boolean
1933
+ }
1934
+
1935
+ function ColorPickerPresets(props: ColorPickerPresetsProps) {
1936
+ const { asChild, className, ...presetProps } = props
1937
+ const context = useColorPickerContext("ColorPickerPresets")
1938
+ const store = useColorPickerStoreContext("ColorPickerPresets")
1939
+ const color = useColorPickerStore((state) => state.color)
1940
+
1941
+ const onPresetClick = React.useCallback(
1942
+ (hex: string, alpha?: number) => {
1943
+ const nextAlpha = alpha ?? (color?.a ?? 1)
1944
+ const newColor = hexToRgb(hex, nextAlpha)
1945
+ store.setColor(newColor)
1946
+ store.setHsv(rgbToHsv(newColor))
1947
+ },
1948
+ [color, store]
1949
+ )
1950
+
1951
+ const PresetsPrimitive = asChild ? Slot : "div"
1952
+
1953
+ return (
1954
+ <PresetsPrimitive
1955
+ data-slot="color-picker-presets"
1956
+ {...presetProps}
1957
+ className={cn("editor-color-presets", className)}
1958
+ >
1959
+ {colorPresets.map((preset) => (
1960
+ <Button
1961
+ key={preset.value}
1962
+ type="button"
1963
+ variant="outline"
1964
+ size="sm"
1965
+ onClick={() =>
1966
+ onPresetClick(preset.value, "alpha" in preset ? preset.alpha : undefined)
1967
+ }
1968
+ disabled={context.disabled}
1969
+ className="editor-color-preset-item"
1970
+ title={preset.label}
1971
+ >
1972
+ <span
1973
+ aria-hidden="true"
1974
+ className="editor-color-preset-item__preview"
1975
+ style={{ backgroundColor: preset.value }}
1976
+ />
1977
+ <span className="editor-color-preset-item__label">{preset.value}</span>
1978
+ </Button>
1979
+ ))}
1980
+ </PresetsPrimitive>
1981
+ )
1982
+ }
1983
+
1984
+ export {
1985
+ ColorPickerRoot as ColorPicker,
1986
+ ColorPickerTrigger,
1987
+ ColorPickerContent,
1988
+ ColorPickerArea,
1989
+ ColorPickerHueSlider,
1990
+ ColorPickerAlphaSlider,
1991
+ ColorPickerSwatch,
1992
+ ColorPickerEyeDropper,
1993
+ ColorPickerFormatSelect,
1994
+ ColorPickerInput,
1995
+ ColorPickerPresets,
1996
+ //
1997
+ ColorPickerRoot as Root,
1998
+ ColorPickerTrigger as Trigger,
1999
+ ColorPickerContent as Content,
2000
+ ColorPickerArea as Area,
2001
+ ColorPickerHueSlider as HueSlider,
2002
+ ColorPickerAlphaSlider as AlphaSlider,
2003
+ ColorPickerSwatch as Swatch,
2004
+ ColorPickerEyeDropper as EyeDropper,
2005
+ ColorPickerFormatSelect as FormatSelect,
2006
+ ColorPickerInput as Input,
2007
+ ColorPickerPresets as Presets,
2008
+ //
2009
+ useColorPickerStore as useColorPicker,
2010
+ }