@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.
- package/dist/editor-x/editor.cjs +33121 -0
- package/dist/editor-x/editor.cjs.map +1 -0
- package/dist/editor-x/editor.css +2854 -0
- package/dist/editor-x/editor.css.map +1 -0
- package/dist/editor-x/editor.d.cts +12 -0
- package/dist/editor-x/editor.d.ts +12 -0
- package/dist/editor-x/editor.js +33095 -0
- package/dist/editor-x/editor.js.map +1 -0
- package/dist/index.cjs +33210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +2854 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +33183 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/src/components/lexical-editor.tsx +123 -0
- package/src/context/editor-container-context.tsx +29 -0
- package/src/context/priority-image-context.tsx +7 -0
- package/src/context/toolbar-context.tsx +60 -0
- package/src/context/uploads-context.tsx +53 -0
- package/src/editor-hooks/use-debounce.ts +80 -0
- package/src/editor-hooks/use-modal.tsx +64 -0
- package/src/editor-hooks/use-report.ts +57 -0
- package/src/editor-hooks/use-update-toolbar.ts +41 -0
- package/src/editor-ui/broken-image.tsx +18 -0
- package/src/editor-ui/caption-composer.tsx +45 -0
- package/src/editor-ui/code-button.tsx +75 -0
- package/src/editor-ui/color-picker.tsx +2010 -0
- package/src/editor-ui/content-editable.tsx +37 -0
- package/src/editor-ui/hooks/use-image-caption-controls.ts +118 -0
- package/src/editor-ui/hooks/use-image-node-interactions.ts +245 -0
- package/src/editor-ui/hooks/use-responsive-image-dimensions.ts +202 -0
- package/src/editor-ui/image-component.tsx +321 -0
- package/src/editor-ui/image-placeholder.tsx +57 -0
- package/src/editor-ui/image-resizer.tsx +499 -0
- package/src/editor-ui/image-sizing.ts +120 -0
- package/src/editor-ui/lazy-image.tsx +136 -0
- package/src/editor-x/editor.tsx +117 -0
- package/src/editor-x/nodes.ts +79 -0
- package/src/editor-x/plugins.tsx +380 -0
- package/src/hooks/use-click-outside.ts +27 -0
- package/src/hooks/use-element-size.ts +54 -0
- package/src/hooks/use-header-height.ts +95 -0
- package/src/hooks/use-isomorphic-layout-effect.ts +4 -0
- package/src/index.ts +4 -0
- package/src/lib/logger.ts +6 -0
- package/src/lib/utils.ts +19 -0
- package/src/nodes/autocomplete-node.tsx +94 -0
- package/src/nodes/embeds/tweet-node.tsx +224 -0
- package/src/nodes/embeds/youtube-node.tsx +519 -0
- package/src/nodes/emoji-node.tsx +83 -0
- package/src/nodes/image-node.tsx +328 -0
- package/src/nodes/keyword-node.tsx +58 -0
- package/src/nodes/layout-container-node.tsx +128 -0
- package/src/nodes/layout-item-node.tsx +118 -0
- package/src/nodes/list-with-color-node.tsx +160 -0
- package/src/nodes/mention-node.ts +122 -0
- package/src/plugins/actions/actions-plugin.tsx +3 -0
- package/src/plugins/actions/character-limit-plugin.tsx +27 -0
- package/src/plugins/actions/clear-editor-plugin.tsx +70 -0
- package/src/plugins/actions/counter-character-plugin.tsx +80 -0
- package/src/plugins/actions/edit-mode-toggle-plugin.tsx +49 -0
- package/src/plugins/actions/import-export-plugin.tsx +61 -0
- package/src/plugins/actions/markdown-toggle-plugin.tsx +78 -0
- package/src/plugins/actions/max-length-plugin.tsx +59 -0
- package/src/plugins/actions/share-content-plugin.tsx +72 -0
- package/src/plugins/actions/speech-to-text-plugin.tsx +159 -0
- package/src/plugins/actions/tree-view-plugin.tsx +63 -0
- package/src/plugins/align-plugin.tsx +86 -0
- package/src/plugins/auto-link-plugin.tsx +34 -0
- package/src/plugins/autocomplete-plugin.tsx +2574 -0
- package/src/plugins/code-action-menu-plugin.tsx +240 -0
- package/src/plugins/code-highlight-plugin.tsx +22 -0
- package/src/plugins/component-picker-menu-plugin.tsx +427 -0
- package/src/plugins/context-menu-plugin.tsx +311 -0
- package/src/plugins/drag-drop-paste-plugin.tsx +52 -0
- package/src/plugins/draggable-block-plugin.tsx +50 -0
- package/src/plugins/embeds/auto-embed-plugin.tsx +324 -0
- package/src/plugins/embeds/twitter-plugin.tsx +45 -0
- package/src/plugins/embeds/youtube-plugin.tsx +84 -0
- package/src/plugins/emoji-picker-plugin.tsx +206 -0
- package/src/plugins/emojis-plugin.tsx +84 -0
- package/src/plugins/floating-link-editor-plugin.tsx +791 -0
- package/src/plugins/floating-text-format-plugin.tsx +710 -0
- package/src/plugins/images-plugin.tsx +671 -0
- package/src/plugins/keywords-plugin.tsx +59 -0
- package/src/plugins/layout-plugin.tsx +658 -0
- package/src/plugins/link-plugin.tsx +18 -0
- package/src/plugins/list-color-plugin.tsx +178 -0
- package/src/plugins/list-max-indent-level-plugin.tsx +85 -0
- package/src/plugins/mentions-plugin.tsx +714 -0
- package/src/plugins/picker/alignment-picker-plugin.tsx +40 -0
- package/src/plugins/picker/bulleted-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/check-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/code-picker-plugin.tsx +30 -0
- package/src/plugins/picker/columns-layout-picker-plugin.tsx +16 -0
- package/src/plugins/picker/component-picker-option.tsx +47 -0
- package/src/plugins/picker/divider-picker-plugin.tsx +14 -0
- package/src/plugins/picker/embeds-picker-plugin.tsx +24 -0
- package/src/plugins/picker/heading-picker-plugin.tsx +32 -0
- package/src/plugins/picker/image-picker-plugin.tsx +16 -0
- package/src/plugins/picker/numbered-list-picker-plugin.tsx +14 -0
- package/src/plugins/picker/paragraph-picker-plugin.tsx +20 -0
- package/src/plugins/picker/quote-picker-plugin.tsx +21 -0
- package/src/plugins/picker/table-picker-plugin.tsx +56 -0
- package/src/plugins/tab-focus-plugin.tsx +66 -0
- package/src/plugins/table-column-resizer-plugin.tsx +309 -0
- package/src/plugins/table-plugin.tsx +299 -0
- package/src/plugins/toolbar/block-format/block-format-data.tsx +69 -0
- package/src/plugins/toolbar/block-format/format-bulleted-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-check-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-code-block.tsx +45 -0
- package/src/plugins/toolbar/block-format/format-heading.tsx +34 -0
- package/src/plugins/toolbar/block-format/format-list-with-marker.tsx +74 -0
- package/src/plugins/toolbar/block-format/format-numbered-list.tsx +40 -0
- package/src/plugins/toolbar/block-format/format-paragraph.tsx +31 -0
- package/src/plugins/toolbar/block-format/format-quote.tsx +32 -0
- package/src/plugins/toolbar/block-format-toolbar-plugin.tsx +117 -0
- package/src/plugins/toolbar/block-insert/insert-columns-layout.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-embeds.tsx +31 -0
- package/src/plugins/toolbar/block-insert/insert-horizontal-rule.tsx +30 -0
- package/src/plugins/toolbar/block-insert/insert-image.tsx +32 -0
- package/src/plugins/toolbar/block-insert/insert-table.tsx +32 -0
- package/src/plugins/toolbar/block-insert-plugin.tsx +30 -0
- package/src/plugins/toolbar/clear-formatting-toolbar-plugin.tsx +92 -0
- package/src/plugins/toolbar/code-language-toolbar-plugin.tsx +121 -0
- package/src/plugins/toolbar/element-format-toolbar-plugin.tsx +251 -0
- package/src/plugins/toolbar/font-background-toolbar-plugin.tsx +179 -0
- package/src/plugins/toolbar/font-color-toolbar-plugin.tsx +101 -0
- package/src/plugins/toolbar/font-family-toolbar-plugin.tsx +91 -0
- package/src/plugins/toolbar/font-format-toolbar-plugin.tsx +85 -0
- package/src/plugins/toolbar/font-size-toolbar-plugin.tsx +177 -0
- package/src/plugins/toolbar/history-toolbar-plugin.tsx +87 -0
- package/src/plugins/toolbar/link-toolbar-plugin.tsx +90 -0
- package/src/plugins/toolbar/subsuper-toolbar-plugin.tsx +69 -0
- package/src/plugins/toolbar/toolbar-plugin.tsx +66 -0
- package/src/plugins/typing-pref-plugin.tsx +118 -0
- package/src/shared/can-use-dom.ts +4 -0
- package/src/shared/environment.ts +47 -0
- package/src/shared/invariant.ts +16 -0
- package/src/shared/use-layout-effect.ts +12 -0
- package/src/themes/_mixins.scss +107 -0
- package/src/themes/_variables.scss +33 -0
- package/src/themes/editor-theme.scss +622 -0
- package/src/themes/editor-theme.ts +118 -0
- package/src/themes/plugins.scss +1180 -0
- package/src/themes/ui-components.scss +936 -0
- package/src/transformers/markdown-emoji-transformer.ts +20 -0
- package/src/transformers/markdown-hr-transformer.ts +28 -0
- package/src/transformers/markdown-image-transformer.ts +31 -0
- package/src/transformers/markdown-list-transformer.ts +51 -0
- package/src/transformers/markdown-table-transformer.ts +200 -0
- package/src/transformers/markdown-tweet-transformer.ts +26 -0
- package/src/ui/button-group.tsx +10 -0
- package/src/ui/button.tsx +29 -0
- package/src/ui/collapsible.tsx +67 -0
- package/src/ui/command.tsx +48 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/flex.tsx +38 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/popover.tsx +128 -0
- package/src/ui/scroll-area.tsx +17 -0
- package/src/ui/select.tsx +171 -0
- package/src/ui/separator.tsx +20 -0
- package/src/ui/slider.tsx +14 -0
- package/src/ui/slot.tsx +3 -0
- package/src/ui/tabs.tsx +87 -0
- package/src/ui/toggle-group.tsx +109 -0
- package/src/ui/toggle.tsx +28 -0
- package/src/ui/tooltip.tsx +28 -0
- package/src/ui/typography.tsx +44 -0
- package/src/utils/doc-serialization.ts +68 -0
- package/src/utils/emoji-list.ts +16604 -0
- package/src/utils/get-dom-range-rect.ts +20 -0
- package/src/utils/get-selected-node.ts +20 -0
- package/src/utils/is-mobile-width.ts +0 -0
- package/src/utils/set-floating-elem-position-for-link-editor.ts +39 -0
- package/src/utils/set-floating-elem-position.ts +44 -0
- package/src/utils/swipe.ts +119 -0
- 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
|
+
}
|