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">
+4 -4
View File
@@ -613,14 +613,14 @@ export function Landing() {
className={`chip${mode === 'light' ? ' on' : ''}`}
onClick={() => setMode('light')}
>
Light
<span className="chip-label">Light</span>
</button>
<button
type="button"
className={`chip${mode === 'dark' ? ' on' : ''}`}
onClick={() => setMode('dark')}
>
Dark
<span className="chip-label">Dark</span>
</button>
<button
type="button"
@@ -628,7 +628,7 @@ export function Landing() {
data-key="theme-auto"
onClick={() => setMode('auto')}
>
Auto
<span className="chip-label">Auto</span>
</button>
</div>
{/* The same colour groups the workspace offers; the page's own
@@ -642,7 +642,7 @@ export function Landing() {
className={`chip${accent === entry.id ? ' on' : ''}`}
onClick={() => setAccent(entry.id)}
>
{entry.label}
<span className="chip-label">{entry.label}</span>
</button>
))}
</div>
+1 -1
View File
@@ -684,7 +684,7 @@ export function Library() {
title={t('lib.subsHint')}
onClick={() => setDeep((prev) => !prev)}
>
{t(deep ? 'lib.subsHide' : 'lib.subsShow')}
<span className="chip-label">{t(deep ? 'lib.subsHide' : 'lib.subsShow')}</span>
</button>
</>
) : null}
+6 -1
View File
@@ -99,7 +99,12 @@ export function ChipRow({ chips, grid }: { chips: ChipDef[]; grid?: boolean }) {
}}
>
{chip.color ? <span className="chip-dot" data-color={chip.color} style={{ background: chip.color }} /> : null}
{chip.label}
{/* The name is a box, not the bare text node it used to be: a flex
container turns a loose text node into an anonymous item of its
own, and on iOS that item stops being painted until the column
it sits in is touched. Same fault, same shape as the panel head
(see `dev-panel-label` in DevelopPanels). No rule of its own. */}
<span className="chip-label">{chip.label}</span>
{chip.value ? <span className="val">{chip.value}</span> : null}
{chip.pro ? <span className="pro">PRO</span> : null}
</button>
+4 -1
View File
@@ -72,7 +72,10 @@ export function ToolRail({
}}
>
<span className="glyph" aria-hidden="true">{entry.glyph}</span>
{t(entry.label)}
{/* A box, not a bare text node: on a phone the glyph is display:none
and the name would be the button's only child — an anonymous flex
item, which is the one thing iOS stops painting until a touch. */}
<span className="rail-label">{t(entry.label)}</span>
</button>
))}
{/* The build this page came off, at the foot of the rail: an image on the
+9 -5
View File
@@ -155,9 +155,13 @@ export function TopBar({
{menu === 'theme' ? (
<div className="popover">
<div className="chip-row">
<button type="button" className={`chip${mode === 'light' ? ' on' : ''}`} onClick={() => setMode('light')}>Light</button>
<button type="button" className={`chip${mode === 'dark' ? ' on' : ''}`} onClick={() => setMode('dark')}>Dark</button>
<button type="button" className={`chip${mode === 'auto' ? ' on' : ''}`} data-key="theme-auto" onClick={() => setMode('auto')}>Auto</button>
{/* The name is a span, not a bare text node: a chip is a flex row,
and a loose text node in one is an anonymous item — the shape
iOS stops painting until the page is touched (same as the
panel heads and ChipRow's chips). */}
<button type="button" className={`chip${mode === 'light' ? ' on' : ''}`} onClick={() => setMode('light')}><span className="chip-label">Light</span></button>
<button type="button" className={`chip${mode === 'dark' ? ' on' : ''}`} onClick={() => setMode('dark')}><span className="chip-label">Dark</span></button>
<button type="button" className={`chip${mode === 'auto' ? ' on' : ''}`} data-key="theme-auto" onClick={() => setMode('auto')}><span className="chip-label">Auto</span></button>
</div>
<div className="chip-row" style={{ marginTop: 8 }}>
{ACCENTS.map((entry) => (
@@ -167,7 +171,7 @@ export function TopBar({
className={`chip${accent === entry.id ? ' on' : ''}`}
onClick={() => setAccent(entry.id)}
>
{entry.label}
<span className="chip-label">{entry.label}</span>
</button>
))}
</div>
@@ -204,7 +208,7 @@ export function TopBar({
setMenu(null);
}}
>
{entry.label}
<span className="chip-label">{entry.label}</span>
</button>
))}
</div>