11"use client" ;
22
33import type React from "react" ;
4-
5- import { useState , useEffect , useRef } from "react" ;
4+ import { useState , useEffect , useRef , useCallback } from "react" ;
65import { Input } from "@/components/ui/input" ;
76import { Button } from "@/components/ui/button" ;
87import {
@@ -16,73 +15,87 @@ interface ColorPickerProps {
1615 onChange : ( color : string ) => void ;
1716}
1817
18+ /** Discord-themed preset colors */
19+ const discordColors = [
20+ "#5865F2" , // Blurple
21+ "#57F287" , // Green
22+ "#FEE75C" , // Yellow
23+ "#EB459E" , // Fuchsia
24+ "#ED4245" , // Red
25+ "#000000" , // Black
26+ "#FFFFFF" , // White
27+ "#1E1F22" , // Dark
28+ "#2B2D31" , // Dark but not as dark
29+ "#313338" , // Even less dark
30+ "#F2F3F5" , // Light
31+ "#E3E5E8" , // Less light
32+ ] ;
33+
1934export function ColorPicker ( { color, onChange } : ColorPickerProps ) {
2035 const [ inputValue , setInputValue ] = useState ( color ) ;
21- const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
2236 const [ isOpen , setIsOpen ] = useState ( false ) ;
37+ const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
2338
24- // Discord colors
25- const discordColors = [
26- "#5865F2" , // Blurple
27- "#57F287" , // Green
28- "#FEE75C" , // Yellow
29- "#EB459E" , // Fuchsia
30- "#ED4245" , // Red
31- "#000000" , // Black
32- "#FFFFFF" , // White
33- "#1E1F22" , // Dark
34- "#2B2D31" , // Dark but not as dark
35- "#313338" , // Even less dark
36- "#F2F3F5" , // Light
37- "#E3E5E8" , // Less light
38- ] ;
39-
40- useEffect ( ( ) => {
41- setInputValue ( color ) ;
42- } , [ color ] ) ;
43-
44- useEffect ( ( ) => {
45- if ( isOpen ) {
46- setTimeout ( ( ) => {
47- drawColorGradient ( ) ;
48- } , 50 ) ;
49- }
50- } , [ isOpen ] ) ;
51-
52- const drawColorGradient = ( ) => {
39+ /* --------------------------------------------------------------------- *
40+ * Draw the HSV-style gradient on the canvas
41+ * --------------------------------------------------------------------- */
42+ const drawColorGradient = useCallback ( ( ) => {
5343 const canvas = canvasRef . current ;
5444 if ( ! canvas ) return ;
5545
5646 const ctx = canvas . getContext ( "2d" ) ;
5747 if ( ! ctx ) return ;
5848
59- // Clear canvas
49+ // Clear
6050 ctx . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
6151
62- // Draw color gradient (horizontal - hue)
63- const gradientH = ctx . createLinearGradient ( 0 , 0 , canvas . width , 0 ) ;
64- gradientH . addColorStop ( 0 , "#FF0000" ) ;
65- gradientH . addColorStop ( 1 / 6 , "#FFFF00" ) ;
66- gradientH . addColorStop ( 2 / 6 , "#00FF00" ) ;
67- gradientH . addColorStop ( 3 / 6 , "#00FFFF" ) ;
68- gradientH . addColorStop ( 4 / 6 , "#0000FF" ) ;
69- gradientH . addColorStop ( 5 / 6 , "#FF00FF" ) ;
70- gradientH . addColorStop ( 1 , "#FF0000" ) ;
71-
72- ctx . fillStyle = gradientH ;
52+ // Horizontal hue gradient
53+ const gradH = ctx . createLinearGradient ( 0 , 0 , canvas . width , 0 ) ;
54+ gradH . addColorStop ( 0 , "#FF0000" ) ;
55+ gradH . addColorStop ( 1 / 6 , "#FFFF00" ) ;
56+ gradH . addColorStop ( 2 / 6 , "#00FF00" ) ;
57+ gradH . addColorStop ( 3 / 6 , "#00FFFF" ) ;
58+ gradH . addColorStop ( 4 / 6 , "#0000FF" ) ;
59+ gradH . addColorStop ( 5 / 6 , "#FF00FF" ) ;
60+ gradH . addColorStop ( 1 , "#FF0000" ) ;
61+
62+ ctx . fillStyle = gradH ;
7363 ctx . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
7464
75- // Draw white to black gradient overlay (vertical - saturation/value)
76- const gradientV = ctx . createLinearGradient ( 0 , 0 , 0 , canvas . height ) ;
77- gradientV . addColorStop ( 0 , "rgba(255, 255, 255, 1)" ) ;
78- gradientV . addColorStop ( 0.5 , "rgba(255, 255, 255, 0)" ) ;
79- gradientV . addColorStop ( 0.5 , "rgba(0, 0, 0, 0)" ) ;
80- gradientV . addColorStop ( 1 , "rgba(0, 0, 0, 1)" ) ;
65+ // Vertical white→transparent→ black overlay (saturation/value)
66+ const gradV = ctx . createLinearGradient ( 0 , 0 , 0 , canvas . height ) ;
67+ gradV . addColorStop ( 0 , "rgba(255,255,255,1)" ) ;
68+ gradV . addColorStop ( 0.5 , "rgba(255,255,255,0)" ) ;
69+ gradV . addColorStop ( 0.5 , "rgba(0,0,0, 0)" ) ;
70+ gradV . addColorStop ( 1 , "rgba(0,0,0, 1)" ) ;
8171
82- ctx . fillStyle = gradientV ;
72+ ctx . fillStyle = gradV ;
8373 ctx . fillRect ( 0 , 0 , canvas . width , canvas . height ) ;
84- } ;
74+ } , [ ] ) ;
75+
76+ /* --------------------------------------------------------------------- *
77+ * Redraw when the popover opens (give the canvas time to mount)
78+ * --------------------------------------------------------------------- */
79+ useEffect ( ( ) => {
80+ if ( ! isOpen ) return ;
8581
82+ const timer = setTimeout ( ( ) => {
83+ drawColorGradient ( ) ;
84+ } , 50 ) ;
85+
86+ return ( ) => clearTimeout ( timer ) ;
87+ } , [ isOpen , drawColorGradient ] ) ;
88+
89+ /* --------------------------------------------------------------------- *
90+ * Keep the input in sync with the prop
91+ * --------------------------------------------------------------------- */
92+ useEffect ( ( ) => {
93+ setInputValue ( color ) ;
94+ } , [ color ] ) ;
95+
96+ /* --------------------------------------------------------------------- *
97+ * Click → sample color from canvas
98+ * --------------------------------------------------------------------- */
8699 const handleCanvasClick = ( e : React . MouseEvent < HTMLCanvasElement > ) => {
87100 const canvas = canvasRef . current ;
88101 if ( ! canvas ) return ;
@@ -94,19 +107,24 @@ export function ColorPicker({ color, onChange }: ColorPickerProps) {
94107 const ctx = canvas . getContext ( "2d" ) ;
95108 if ( ! ctx ) return ;
96109
97- const imageData = ctx . getImageData ( x , y , 1 , 1 ) . data ;
98- const color = `#${ [ imageData [ 0 ] , imageData [ 1 ] , imageData [ 2 ] ] . map ( ( x ) => x . toString ( 16 ) . padStart ( 2 , "0" ) ) . join ( "" ) } ` ;
110+ const [ r , g , b ] = ctx . getImageData ( x , y , 1 , 1 ) . data ;
111+ const hex = `#${ [ r , g , b ]
112+ . map ( ( c ) => c . toString ( 16 ) . padStart ( 2 , "0" ) )
113+ . join ( "" ) } `;
99114
100- onChange ( color ) ;
101- setInputValue ( color ) ;
115+ onChange ( hex ) ;
116+ setInputValue ( hex ) ;
102117 } ;
103118
119+ /* --------------------------------------------------------------------- *
120+ * Manual hex input
121+ * --------------------------------------------------------------------- */
104122 const handleInputChange = ( e : React . ChangeEvent < HTMLInputElement > ) => {
105- setInputValue ( e . target . value ) ;
123+ const val = e . target . value ;
124+ setInputValue ( val ) ;
106125
107- // Validate if it's a proper hex color
108- if ( / ^ # [ 0 - 9 A - F ] { 6 } $ / i. test ( e . target . value ) ) {
109- onChange ( e . target . value ) ;
126+ if ( / ^ # [ 0 - 9 A - F ] { 6 } $ / i. test ( val ) ) {
127+ onChange ( val ) ;
110128 }
111129 } ;
112130
@@ -122,8 +140,10 @@ export function ColorPicker({ color, onChange }: ColorPickerProps) {
122140 < span className = "sr-only" > Pick a color</ span >
123141 </ Button >
124142 </ PopoverTrigger >
143+
125144 < PopoverContent className = "w-64" >
126145 < div className = "space-y-4" >
146+ { /* Canvas picker */ }
127147 < div className = "relative w-full h-40 rounded-md overflow-hidden border border-input" >
128148 < canvas
129149 ref = { canvasRef }
@@ -134,26 +154,28 @@ export function ColorPicker({ color, onChange }: ColorPickerProps) {
134154 />
135155 </ div >
136156
157+ { /* Discord preset swatches */ }
137158 < div className = "grid grid-cols-6 gap-2" >
138- { discordColors . map ( ( discordColor , index ) => (
159+ { discordColors . map ( ( c ) => (
139160 < Button
140- key = { index }
161+ key = { c }
141162 variant = "outline"
142163 className = "w-8 h-8 p-0 rounded-md border"
143- style = { { backgroundColor : discordColor } }
164+ style = { { backgroundColor : c } }
144165 onClick = { ( ) => {
145- onChange ( discordColor ) ;
146- setInputValue ( discordColor ) ;
166+ onChange ( c ) ;
167+ setInputValue ( c ) ;
147168 } }
148169 >
149- < span className = "sr-only" > Select color { discordColor } </ span >
170+ < span className = "sr-only" > Select { c } </ span >
150171 </ Button >
151172 ) ) }
152173 </ div >
153174 </ div >
154175 </ PopoverContent >
155176 </ Popover >
156177
178+ { /* Hex input */ }
157179 < Input
158180 value = { inputValue }
159181 onChange = { handleInputChange }
0 commit comments