feat(studio): draw every slider as a line with a hollow node, and move the shapes into TOOLS

The default ring was a solid disc on a coloured bar, which read as a crude
block next to the rest of the rail; every range input is now a hairline
track with an outline node in the accent colour, and the temperature and
tint strips keep their gradients on the track itself.

CROP, ROTATE and WATERMARK reshape the frame rather than grade it, so they
belong with the tools that work on the whole image.
This commit is contained in:
2026-10-08 11:28:14 +07:00
parent af23f3a7c9
commit 5f78875549
3 changed files with 93 additions and 20 deletions
+22 -5
View File
@@ -2810,10 +2810,13 @@ export function Workspace() {
},
});
const framesRow = (): ChipDef[] => [
// The frame row is the frames alone. CROP, ROTATE and WATERMARK used to close
// it — a frame is the look, and cutting, levelling and signing the print are
// moves made on the frame, which is what TOOLS is (see `shapeChips`).
const framesRow = (): ChipDef[] =>
// NO FRAME is not a chip of its own: clicking the frame already on the
// photo takes it off again, so the row carries one entry per frame.
...FRAMES.filter((frame) => frame.id !== 'none').map((frame) => ({
FRAMES.filter((frame) => frame.id !== 'none').map((frame) => ({
key: `fr:${frame.id}`,
label: t(frame.label),
active: frameId === frame.id,
@@ -2828,7 +2831,12 @@ export function Workspace() {
remember();
setFrameId(frameId === frame.id ? 'none' : frame.id);
},
})),
}));
// The three moves that shape the print rather than tint it: the frame is cut
// to a shape, turned to the straight, and signed. Each opens its own strip in
// the last column, the same strip it opened from the frame row.
const shapeChips: ChipDef[] = [
groupChip('crop'),
// The chip names whichever of the two is off the straight — and both when
// both are — then the mirrors riding on top of them.
@@ -3022,6 +3030,11 @@ export function Workspace() {
// a tab instead of LIGHT's row. Each carries its own mark at the head of
// its label so the four read as one set.
//
// CROP, ROTATE and WATERMARK closed the frame row and are here for the
// same reason: cutting, levelling and signing the print are moves on the
// frame rather than a frame picked for it, and they take the same strips
// they always opened (see shapeChips).
//
// Nothing here is a knob, so the tab is this row alone: the strip a chip
// opens is the last column's (see stripChips), the same as anywhere else.
// AUTO and MONOCHROME come with any account — one lands a set of numbers
@@ -3069,6 +3082,10 @@ export function Workspace() {
}
: locked('gradient-mask', t('pro.gradientChip')),
{ key: 'mono', label: '◑ MONOCHROME', active: monoOn, onClick: toggleMono },
// The rule stands where the features that read or take the pointer
// end and the marks cut into the print begin.
{ key: 'tools-rule', divider: true, label: '', onClick: () => {} },
...shapeChips,
];
case 'hsl': {
// The mixer is PRO: the picker, the eight bands and the three
@@ -3720,11 +3737,11 @@ export function Workspace() {
</div>
</div>
{/* FRAME's WATERMARK chip opens the two collapses, one per
{/* TOOLS' WATERMARK chip opens the two collapses, one per
watermark type: the header chip, then that mark's own controls */}
{wmOpen ? (
<div className="col col-sub strip wm-extra" data-col="wm">
<ChipRow chips={[{ key: 'wm-back', label: '< FRAME', onClick: () => setOpenGroup(null) }]} />
<ChipRow chips={[{ key: 'wm-back', label: '< TOOLS', onClick: () => setOpenGroup(null) }]} />
{/* GPS WATERMARK — its colour, its face, its two rows, its place */}
<div className="wm-sec" data-key="wm-sec-gps">
+68 -14
View File
@@ -323,7 +323,60 @@
font-weight: 700;
cursor: pointer;
}
input[type="range"] { width: 100%; accent-color: var(--accent); }
/* A slider is a line and a node, and nothing else. The line is the theme's own
border ink at a hairline; the node is hollow — a ring drawn in the accent, no
fill of its own — so the control wears the theme's colour instead of the
browser's. `accent-color` goes with the old look: it painted a track and a
solid thumb of its own, and there is nothing of either left for it to paint.
The box is left taller than the line on purpose: the box is the drag target
(a finger is wider than a hairline), the line is what is read. */
input[type="range"] {
width: 100%;
height: 16px;
margin: 0;
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
}
/* One rule per engine: a grouped selector naming a pseudo-element the engine
does not know is dropped whole, and the track went with it (measured). */
input[type="range"]::-webkit-slider-runnable-track {
height: 2px;
border-radius: 999px;
background: var(--border);
}
input[type="range"]::-moz-range-track {
height: 2px;
border-radius: 999px;
background: var(--border);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px;
height: 10px;
margin-top: -4px; /* (10px node - 2px line) / 2 */
border: 1.5px solid var(--accent);
border-radius: 50%;
background: transparent;
}
input[type="range"]::-moz-range-thumb {
-webkit-appearance: none;
appearance: none;
box-sizing: border-box;
width: 10px;
height: 10px;
border: 1.5px solid var(--accent);
border-radius: 50%;
background: transparent;
}
/* The knob the pointer is on takes the theme's own hover ink, so the node says
it is the one being dragged. */
input[type="range"]:hover::-webkit-slider-thumb,
input[type="range"]:focus-visible::-webkit-slider-thumb { border-color: var(--accent-hover); }
input[type="range"]:hover::-moz-range-thumb,
input[type="range"]:focus-visible::-moz-range-thumb { border-color: var(--accent-hover); }
.hint { color: var(--text-dim); font-size: 12px; line-height: 1.5; }
.hint.ok { color: var(--accent); }
/* A hint that is also a control — the backup row's folder is the reader's to
@@ -1557,19 +1610,20 @@ button.hint:disabled { cursor: default; }
}
.dev-pro { font-size: 9px; letter-spacing: 0.08em; color: var(--accent); }
/* WB's two knobs: the track is the hue the value moves towards, the way
Lightroom's temperature and tint sliders are painted. Everything else keeps
the plain input's accent color. */
.track-temp, .track-tint {
height: 4px; border-radius: 999px;
appearance: none; -webkit-appearance: none;
}
.track-temp { background: linear-gradient(90deg, #3f6fd8, #b9c2cc 50%, #e8a33d); }
.track-tint { background: linear-gradient(90deg, #3f9d55, #b9c2cc 50%, #c04ec4); }
.track-temp::-webkit-slider-thumb, .track-tint::-webkit-slider-thumb {
appearance: none; -webkit-appearance: none;
width: 12px; height: 12px; border-radius: 50%;
background: var(--text); border: none; cursor: pointer;
}
Lightroom's temperature and tint sliders are painted — the same line as
everywhere else, only coloured, and a shade thicker so the cast it names can
be read. The node is the app's own ring and is left to the rule above: the
line says where the value is, the ring says where the knob is. The colour sits
on the track and not on the input (where it used to be drawn) because the
line is what is read — a background on the box would have shown through
around the hairline every other knob is drawn with. */
.track-temp::-webkit-slider-runnable-track { background: linear-gradient(90deg, #3f6fd8, #b9c2cc 50%, #e8a33d); height: 4px; }
.track-tint::-webkit-slider-runnable-track { background: linear-gradient(90deg, #3f9d55, #b9c2cc 50%, #c04ec4); height: 4px; }
.track-temp::-moz-range-track { background: linear-gradient(90deg, #3f6fd8, #b9c2cc 50%, #e8a33d); height: 4px; }
.track-tint::-moz-range-track { background: linear-gradient(90deg, #3f9d55, #b9c2cc 50%, #c04ec4); height: 4px; }
/* The node is centred on a track twice the hairline's height, so it shifts up
the one pixel the base rule does not account for. */
.track-temp::-webkit-slider-thumb, .track-tint::-webkit-slider-thumb { margin-top: -3px; }
/* --- responsive --------------------------------------------------------- */
@media (max-width: 860px) {
+3 -1
View File
@@ -6,7 +6,9 @@ import { useI18n } from '../i18n/I18nProvider';
// FX, which used to be tabs of their own, are panels of it now. TOOLS is the one
// tab of features rather than records: the chips that write a value or take the
// pointer (AUTO, FIX, GRADIENT MASK, MONOCHROME), which used to share LIGHT's
// row with the two chips that are not knobs. The rail is a single column on
// row with the two chips that are not knobs, and — since they act on the frame
// rather than pick a look for it — CROP, ROTATE and WATERMARK, which used to
// close the frame row. The rail is a single column on
// desktop and becomes the bottom bar under 860px (see styles/app.css).
export type TabId = 'presets' | 'favorited' | 'photos' | 'light' | 'tools' | 'hsl' | 'frame' | 'save' | 'create';