Skip to content

Commit 9255c14

Browse files
perf(web): the theme color picker compiles with the React Compiler
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent e32718d commit 9255c14

2 files changed

Lines changed: 33 additions & 34 deletions

File tree

‎apps/web/reactCompiledFiles.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,5 @@ export const REACT_COMPILED_FILES = [
9292
"apps/web/src/components/settings/SnapShotSettings.tsx",
9393
"apps/web/src/components/settings/ConnectionsSettings.tsx",
9494
"apps/web/src/hooks/useResizeDrag.ts",
95+
"apps/web/src/components/settings/ThemeColorPicker.tsx",
9596
] as const;

‎apps/web/src/components/settings/ThemeColorPicker.tsx‎

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { memo, useCallback, useEffect, useRef, useState } from "react";
1+
import { memo, useEffect, useEffectEvent, useRef, useState } from "react";
2+
import { useStableCallback } from "../../hooks/useStableCallback";
23
import { isThemeColor, themeColorToHex, type ThemeColorRole } from "../../themePalette";
34
import { cn } from "../../lib/utils";
45
import { 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

Comments
 (0)