1- import { memo , useCallback , useEffect , useRef , useState } from "react" ;
1+ import { memo , useEffect , useEffectEvent , useRef , useState } from "react" ;
2+ import { useStableCallback } from "../../hooks/useStableCallback" ;
23import { isThemeColor , themeColorToHex , type ThemeColorRole } from "../../themePalette" ;
34import { cn } from "../../lib/utils" ;
45import { hexToHsv , hsvToHex , type HsvColor } from "../../lib/color" ;
@@ -80,66 +81,63 @@ function ThemeColorPickerPanel({
8081 const [ hsv , setHsv ] = useState ( ( ) => hexToHsv ( normalizedValue ) ) ;
8182 const [ hexDraft , setHexDraft ] = useState ( normalizedValue ) ;
8283 const [ rgbDraft , setRgbDraft ] = useState ( ( ) => themeRgbValue ( normalizedValue ) ) ;
83- const isEditingTextRef = useRef ( false ) ;
84+ const [ isEditingText , setIsEditingText ] = useState ( false ) ;
85+ const [ syncedValue , setSyncedValue ] = useState ( normalizedValue ) ;
8486 const currentColor = hsvToHex ( hsv . h , hsv . s , hsv . v ) ;
8587 const currentRgb = themeRgbValue ( currentColor ) ;
8688
87- useEffect ( ( ) => {
89+ if ( syncedValue !== normalizedValue ) {
90+ setSyncedValue ( normalizedValue ) ;
8891 // While a text field is focused, the incoming value may be the guided
8992 // editor's readability-adjusted echo of what is being typed; rewriting the
9093 // draft would fight the keystrokes. The swatch still tracks via hsv.
91- if ( ! isEditingTextRef . current ) {
94+ if ( ! isEditingText ) {
9295 setHexDraft ( normalizedValue ) ;
9396 setRgbDraft ( themeRgbValue ( normalizedValue ) ) ;
9497 }
9598 // Keep the current hue/saturation when the incoming value is just our own
9699 // change echoed back; hex → HSV is lossy for greys, white, and black.
97- setHsv ( ( current ) =>
98- hsvToHex ( current . h , current . s , current . v ) === normalizedValue
99- ? current
100- : hexToHsv ( normalizedValue ) ,
101- ) ;
102- } , [ normalizedValue ] ) ;
100+ if ( currentColor !== normalizedValue ) setHsv ( hexToHsv ( normalizedValue ) ) ;
101+ }
103102
104103 // Local state updates immediately for a smooth thumb; the parent commit
105104 // (which can regenerate a whole guided palette) is batched to one call per
106105 // animation frame.
107- const onChangeRef = useRef ( onChange ) ;
108- onChangeRef . current = onChange ;
106+ const commitColor = useStableCallback ( onChange ) ;
109107 const pendingCommitRef = useRef < string | null > ( null ) ;
110108 const commitFrameRef = useRef < number | null > ( null ) ;
111109 // The final drag frame must not be lost when the popover closes or the
112110 // pointer lifts before the animation frame fires.
113- const flushPendingCommit = useCallback ( ( ) => {
111+ const flushPendingCommit = ( ) => {
114112 if ( commitFrameRef . current !== null ) {
115113 cancelAnimationFrame ( commitFrameRef . current ) ;
116114 commitFrameRef . current = null ;
117115 }
118116 const pending = pendingCommitRef . current ;
119117 pendingCommitRef . current = null ;
120- if ( pending !== null ) onChangeRef . current ( pending ) ;
121- } , [ ] ) ;
122- useEffect ( ( ) => ( ) => flushPendingCommit ( ) , [ flushPendingCommit ] ) ;
123- const scheduleCommit = useCallback ( ( color : string ) => {
118+ if ( pending !== null ) commitColor ( pending ) ;
119+ } ;
120+ // Flush only on unmount; rerunning this cleanup mid-drag would commit early.
121+ const flushOnUnmount = useEffectEvent ( ( ) => flushPendingCommit ( ) ) ;
122+ useEffect ( ( ) => ( ) => flushOnUnmount ( ) , [ ] ) ;
123+ const scheduleCommit = ( color : string ) => {
124124 pendingCommitRef . current = color ;
125- commitFrameRef . current ??= requestAnimationFrame ( ( ) => {
125+ if ( commitFrameRef . current !== null ) return ;
126+ commitFrameRef . current = requestAnimationFrame ( ( ) => {
126127 commitFrameRef . current = null ;
127128 const pending = pendingCommitRef . current ;
128129 pendingCommitRef . current = null ;
129- if ( pending !== null ) onChangeRef . current ( pending ) ;
130+ if ( pending !== null ) commitColor ( pending ) ;
130131 } ) ;
131- } , [ ] ) ;
132+ } ;
132133
133- const commitHsv = useCallback (
134- ( nextHsv : HsvColor ) => {
135- setHsv ( nextHsv ) ;
136- const nextColor = hsvToHex ( nextHsv . h , nextHsv . s , nextHsv . v ) ;
137- setHexDraft ( nextColor ) ;
138- setRgbDraft ( themeRgbValue ( nextColor ) ) ;
139- scheduleCommit ( nextColor + alphaSuffix ) ;
140- } ,
141- [ alphaSuffix , scheduleCommit ] ,
142- ) ;
134+ const commitHsv = ( nextHsv : HsvColor ) => {
135+ setHsv ( nextHsv ) ;
136+ const nextColor = hsvToHex ( nextHsv . h , nextHsv . s , nextHsv . v ) ;
137+ setHexDraft ( nextColor ) ;
138+ setRgbDraft ( themeRgbValue ( nextColor ) ) ;
139+ scheduleCommit ( nextColor + alphaSuffix ) ;
140+ } ;
143141
144142 const handleHexChange = ( nextValue : string ) => {
145143 setHexDraft ( nextValue ) ;
@@ -198,13 +196,13 @@ function ThemeColorPickerPanel({
198196 aria-label = { `${ label } picker hex value` }
199197 className = "h-8 min-w-0 flex-1 bg-transparent font-mono text-xs text-foreground outline-none"
200198 onBlur = { ( ) => {
201- isEditingTextRef . current = false ;
199+ setIsEditingText ( false ) ;
202200 setHexDraft ( currentColor ) ;
203201 setRgbDraft ( currentRgb ) ;
204202 } }
205203 onChange = { ( event ) => handleHexChange ( event . currentTarget . value ) }
206204 onFocus = { ( ) => {
207- isEditingTextRef . current = true ;
205+ setIsEditingText ( true ) ;
208206 } }
209207 spellCheck = { false }
210208 value = { hexDraft }
@@ -220,13 +218,13 @@ function ThemeColorPickerPanel({
220218 aria-label = { `${ label } picker RGB value` }
221219 className = "h-8 min-w-0 flex-1 bg-transparent font-mono text-xs text-foreground outline-none"
222220 onBlur = { ( ) => {
223- isEditingTextRef . current = false ;
221+ setIsEditingText ( false ) ;
224222 setHexDraft ( currentColor ) ;
225223 setRgbDraft ( currentRgb ) ;
226224 } }
227225 onChange = { ( event ) => handleRgbChange ( event . currentTarget . value ) }
228226 onFocus = { ( ) => {
229- isEditingTextRef . current = true ;
227+ setIsEditingText ( true ) ;
230228 } }
231229 spellCheck = { false }
232230 value = { rgbDraft }
0 commit comments