feat(ui): compact Color Wheels & Match panel with MiniSlider rulers and 4-column color bands

This commit is contained in:
2026-10-16 08:56:49 +07:00
parent a841c7f5a0
commit 7a3b81b6ea
3 changed files with 139 additions and 78 deletions
+112 -77
View File
@@ -4028,7 +4028,7 @@ export function Workspace() {
<div>
<span className="dev-box-title" style={{ marginBottom: '6px', display: 'block' }}>COLOR BANDS</span>
<ChipRow
grid
grid4
chips={HSL_BANDS.map((b) => {
const band = recipe.adjustments.hslBands?.[b.id];
const moved = !!band && (band[0] !== 0 || band[1] !== 0 || band[2] !== 0);
@@ -4050,7 +4050,7 @@ export function Workspace() {
{hslBand && (
<div className="dev-subgroup" style={{ marginTop: '4px' }}>
<div className="dev-subgroup-title" style={{ display: 'flex', justifyContent: 'space-between' }}>
<div className="dev-subgroup-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>BAND: {HSL_BANDS.find((b) => b.id === hslBand)?.label}</span>
{(recipe.adjustments.hslBands?.[hslBand]?.[0] || recipe.adjustments.hslBands?.[hslBand]?.[1] || recipe.adjustments.hslBands?.[hslBand]?.[2]) ? (
<button
@@ -4082,39 +4082,45 @@ export function Workspace() {
};
return (
<>
<div className="dev-row">
<span className="dev-row-label">Hue</span>
<input
type="range"
min={-30}
max={30}
value={bandVals[0]}
onChange={(e) => updateBand(0, Number(e.target.value))}
/>
<span className="dev-row-value">{bandVals[0] > 0 ? `+${bandVals[0]}` : bandVals[0]}</span>
</div>
<div className="dev-row">
<span className="dev-row-label">Saturation</span>
<input
type="range"
min={-100}
max={100}
value={bandVals[1]}
onChange={(e) => updateBand(1, Number(e.target.value))}
/>
<span className="dev-row-value">{bandVals[1] > 0 ? `+${bandVals[1]}` : bandVals[1]}</span>
</div>
<div className="dev-row">
<span className="dev-row-label">Luminance</span>
<input
type="range"
min={-100}
max={100}
value={bandVals[2]}
onChange={(e) => updateBand(2, Number(e.target.value))}
/>
<span className="dev-row-value">{bandVals[2] > 0 ? `+${bandVals[2]}` : bandVals[2]}</span>
</div>
<MiniSlider
label="Hue"
value={bandVals[0]}
min={-10}
max={10}
step={1}
defaultValue={0}
format={(v) => v.toFixed(1)}
prefix={`dev-hsl-${hslBand}`}
dataKey={`dev-hsl-${hslBand}-hue`}
onChange={(v) => updateBand(0, v)}
onReset={() => updateBand(0, 0)}
/>
<MiniSlider
label="Saturation"
value={bandVals[1]}
min={-10}
max={10}
step={1}
defaultValue={0}
format={(v) => v.toFixed(1)}
prefix={`dev-hsl-${hslBand}`}
dataKey={`dev-hsl-${hslBand}-sat`}
onChange={(v) => updateBand(1, v)}
onReset={() => updateBand(1, 0)}
/>
<MiniSlider
label="Luminance"
value={bandVals[2]}
min={-10}
max={10}
step={1}
defaultValue={0}
format={(v) => v.toFixed(1)}
prefix={`dev-hsl-${hslBand}`}
dataKey={`dev-hsl-${hslBand}-lum`}
onChange={(v) => updateBand(2, v)}
onReset={() => updateBand(2, 0)}
/>
</>
);
})()}
@@ -4122,49 +4128,78 @@ export function Workspace() {
)}
<div className="dev-subgroup" style={{ marginTop: '4px' }}>
<div className="dev-subgroup-title">IMAGE MASTER</div>
<div className="dev-row">
<span className="dev-row-label">Hue</span>
<input
type="range"
min={-180}
max={180}
value={recipe.adjustments.hslHue ?? 0}
onChange={(e) => {
remember();
setAdjustment({ hslHue: Number(e.target.value) });
}}
/>
<span className="dev-row-value">{(recipe.adjustments.hslHue ?? 0) > 0 ? `+${recipe.adjustments.hslHue}` : recipe.adjustments.hslHue ?? 0}</span>
</div>
<div className="dev-row">
<span className="dev-row-label">Saturation</span>
<input
type="range"
min={-100}
max={100}
value={recipe.adjustments.hslSat ?? 0}
onChange={(e) => {
remember();
setAdjustment({ hslSat: Number(e.target.value) });
}}
/>
<span className="dev-row-value">{(recipe.adjustments.hslSat ?? 0) > 0 ? `+${recipe.adjustments.hslSat}` : recipe.adjustments.hslSat ?? 0}</span>
</div>
<div className="dev-row">
<span className="dev-row-label">Luminance</span>
<input
type="range"
min={-100}
max={100}
value={recipe.adjustments.hslLum ?? 0}
onChange={(e) => {
remember();
setAdjustment({ hslLum: Number(e.target.value) });
}}
/>
<span className="dev-row-value">{(recipe.adjustments.hslLum ?? 0) > 0 ? `+${recipe.adjustments.hslLum}` : recipe.adjustments.hslLum ?? 0}</span>
<div className="dev-subgroup-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>IMAGE MASTER</span>
{((recipe.adjustments.hslHue ?? 0) !== 0 || (recipe.adjustments.hslSat ?? 0) !== 0 || (recipe.adjustments.hslLum ?? 0) !== 0) ? (
<button
type="button"
style={{ background: 'none', border: 'none', color: 'var(--accent)', cursor: 'pointer', fontSize: '10px' }}
onClick={() => {
remember();
setAdjustment({ hslHue: 0, hslSat: 0, hslLum: 0 });
}}
>
RESET
</button>
) : null}
</div>
<MiniSlider
label="Hue"
value={recipe.adjustments.hslHue ?? 0}
min={-10}
max={10}
step={1}
defaultValue={0}
format={(v) => v.toFixed(1)}
prefix="dev-hsl-master"
dataKey="dev-hsl-master-hue"
onChange={(v) => {
remember();
setAdjustment({ hslHue: v });
}}
onReset={() => {
remember();
setAdjustment({ hslHue: 0 });
}}
/>
<MiniSlider
label="Saturation"
value={recipe.adjustments.hslSat ?? 0}
min={-10}
max={10}
step={1}
defaultValue={0}
format={(v) => v.toFixed(1)}
prefix="dev-hsl-master"
dataKey="dev-hsl-master-sat"
onChange={(v) => {
remember();
setAdjustment({ hslSat: v });
}}
onReset={() => {
remember();
setAdjustment({ hslSat: 0 });
}}
/>
<MiniSlider
label="Luminance"
value={recipe.adjustments.hslLum ?? 0}
min={-10}
max={10}
step={1}
defaultValue={0}
format={(v) => v.toFixed(1)}
prefix="dev-hsl-master"
dataKey="dev-hsl-master-lum"
onChange={(v) => {
remember();
setAdjustment({ hslLum: v });
}}
onReset={() => {
remember();
setAdjustment({ hslLum: 0 });
}}
/>
</div>
</div>
),
+24
View File
@@ -201,6 +201,30 @@
font-weight: 400;
white-space: nowrap;
}
.chip-row.grid-4 {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px;
width: 100%;
}
.chip-row.grid-4 .chip {
justify-content: center;
text-align: center;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 4px 2px;
font-size: 10px;
font-weight: 400;
white-space: nowrap;
}
.chip-row.grid-4 .chip .chip-label {
font-size: 9.5px;
}
.chip-row.grid-4 .chip .chip-dot {
width: 8px;
height: 8px;
}
.chip-label {
font-weight: 400;
letter-spacing: 0.02em;
+3 -1
View File
@@ -72,19 +72,21 @@ export function ChipRow({
chips,
grid,
grid3,
grid4,
list,
glyph,
}: {
chips: ChipDef[];
grid?: boolean;
grid3?: boolean;
grid4?: boolean;
list?: boolean;
// The mark drawn at the head of every row of a `list` (a frame, a look). It
// is decoration and nothing else — the name beside it is the row.
glyph?: string;
}) {
return (
<div className={`chip-row${list ? ' list' : ''}${grid3 ? ' grid-3' : grid ? ' grid' : ''}`}>
<div className={`chip-row${list ? ' list' : ''}${grid4 ? ' grid-4' : grid3 ? ' grid-3' : grid ? ' grid' : ''}`}>
{chips.map((chip) =>
chip.divider ? (
<span key={chip.key} className="chip-rule" role="separator" aria-orientation="horizontal" />