HSL: click a colour chip to open its panel, the way PICK does

The colour band chips only aimed the mixer before; now they open the same
panel the eyedropper opens, seeded with the colour the chip is named after
(hexToRgb turns that readout back into the RGB bytes the picker stores).
A pick already on the photo keeps its spot — only a mixer with nowhere to
hang takes the middle of the frame.

The HSL column itself is untouched: 8 chips, the rule, IMAGE,
HUE/SAT/LUM, the readout and RESET all stay where they were.
This commit is contained in:
2026-09-30 07:34:07 +07:00
parent f7031c1809
commit 35d8d57984
6 changed files with 44 additions and 16 deletions
+20 -4
View File
@@ -175,6 +175,13 @@ function hslToHex(h: number, s: number, l: number): string {
return `#${chan(0)}${chan(8)}${chan(4)}`;
}
// The way back: a band chip hands the eyedropper's readout the colour it is
// named after, and that readout is RGB bytes.
function hexToRgb(hex: string): { r: number; g: number; b: number } {
const n = parseInt(hex.slice(1), 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
// The knobs that drive a colour get a swatch under their ruler, so the hue is
// visible next to the number. TEMP is the only one left on this path: TINT's row
// lives in the sidebar's WB panel now, so its ±10 axis draws no ruler here.
@@ -2862,7 +2869,16 @@ export function Workspace() {
color: hslToHex(b.hue, 70, 50),
active: hslBand === b.id,
amberValue: hslBand !== b.id && moved,
onClick: () => setHslBand(b.id),
// A band chip aims the mixer AND opens its panel, the same panel
// the eyedropper opens: the colour it starts from is the colour
// the chip is named after, and this band's knobs push it from
// there. A pick already on the photo keeps its spot — only a
// mixer with nowhere to hang takes the middle of the frame.
onClick: () => {
setHslBand(b.id);
setSample(hexToRgb(hslToHex(b.hue, 70, 50)));
setPickedAt((at) => at ?? { fx: 0.5, fy: 0.5 });
},
};
}),
// The divider keeps the three image-wide sliders out of the colour
@@ -3249,7 +3265,7 @@ export function Workspace() {
aria-pressed={recipe.name === entry.name}
onClick={() => applyRecipe({ ...(entry.recipe as Recipe), name: entry.name })}
>
{entry.name}
<span className="chip-label">{entry.name}</span>
</button>
{/* Only this account can delete its own file — the API
scopes the delete by user, the button mirrors it. */}
@@ -3261,7 +3277,7 @@ export function Workspace() {
aria-label={t('save.delete')}
onClick={() => void deleteRecipeEntry(entry)}
>
✕
<span className="chip-label" aria-hidden="true">✕</span>
</button>
</div>
))}
@@ -3359,7 +3375,7 @@ export function Workspace() {
disabled={locating}
onClick={() => locateMe()}
>
{locating ? t('wm.locating') : t('wm.useLocation')}
<span className="chip-label">{locating ? t('wm.locating') : t('wm.useLocation')}</span>
</button>
</div>
<div className="chip-row">