- Shipped
- August 23, 2026 at 3:04 AM UTC
- Author
- kamo
- Commit
- 7229b37
ModernColorGroup was controlled and uncontrolled at the same time. It kept local state for every slot, mirrored the `value` prop into that state in one effect, and called `onChange` from another with a freshly built object whenever any slot moved. The parent stores what it is handed, so that new object identity re-triggered the mirror, and a setTimeout(..., 0) guard flag kept the cycle from ever converging. Two consequences. Mounted and untouched, the group never went quiet: it re-rendered the whole 3,400-line settings page for as long as the Branding editor was open. And every tick pushed the parent's copy back over the local state, so anything typed into the hex field was racing the mirror. It also derived the four shades from whatever the base field held mid-keystroke — "#c81" is a fine argument to parseInt, so no error surfaced and the shades were simply computed from a colour nobody had finished typing. Derive from `value`, call `onChange` only from real events, and refuse to derive shades from an incomplete hex. The state machine moves to colorGroupState.ts so it can be tested directly; the component keeps its markup. Three of the four new component tests fail against the old version, including the one that shows "#c81" publishing lighter: #1e2a9f.