The marks get a tab of their own and the rulers read on one line
Four asks, one reading of the LIGHT column. EV's ruler sat under PROFILE, which
is the film simulation and nothing else — the range a picture is on is a knob of
the tonal range, not of the stock it was shot on. D.RANGE sat with the tone
controls for the same wrong reason: it is a hold on the whole frame, so it
belongs at the foot of the effects. The two Color Chromes had gone missing from
the eye, because the only place they were drawn was the effects slot and
DETAIL & EFFECTS now opens folded. And every ruler was two lines: a name with
its number on one, the track alone under them, where Lightroom reads a ruler as
one line. The four feature buttons answered a fifth ask that arrived with them:
AUTO, FIX, GRADIENT MASK and MONOCHROME do not name a look, they make a move on
the frame, and they now share a tab of their own.
TOOLS is the rail's ninth tab, between LIGHT and HSL, marked with the hammer and
pick (`⚒`) because the four features work ON the photo rather than pick a look
for it. The chips keep their keys, their amber-value logic and their pointer
behaviour — FIX and GRADIENT MASK still put down whatever tool was up before
they open their strip — and each carries its own mark at the head of its label
so the four read as one set: `✦ AUTO`, `✚ FIX`, `▚ GRADIENT MASK`,
`◑ MONOCHROME`. The marks were probed in the page's own stack before they were
spent (Inter, then the system sans): four-pointed star, thick cross, two
diagonal quadrants, half-filled circle — 26px, 47px, 77px and 15px of ink at
11px against the 36px a tofu box draws, so none of them is a missing glyph. The
strips still open in the last column, the same `stripChips` any other tab uses.
LIGHT keeps the one chip that is not a knob — TONE CURVE, `∿ TONE CURVE`, which
opens the graph on the photo rather than a ruler in the column, and stands over
the TONE panel whose shape it is — and gains the pair under it. COLOR CHROME and
CHROME BLUE are the same switch over two axes of one colour, so they are read
against each other rather than scanned in turn: one row, two chips, the table
layout the WB presets already wear (`.chip-row.grid`), 147px each side by side
at top 123 with nothing overflowing. Each names the strength it is on, and names
`OFF` when it is on `none` — the pair is compared, so the neutral value is one
of the two being read (`groupChip(g, nameAlways)`). Either one still opens its
own strip in the last column; picking STRONG lands on the chip and, at rest,
goes amber.
Every ruler is one line now: the knob's name at the head of the row, the track
taking the width they leave, the number it is on at the foot. The track moved
inside `MiniSlider`'s `.mini-head` and inside `SliderRow`'s `.head`, and both
rules became a flex row (`span` and `b` `flex: 0 0 auto`, the input
`flex: 1 1 40px; min-width: 0`). The eight TONE rows, the six of DETAIL &
EFFECTS and the mask column's eleven all read as one line, track included. The
HSL mixer's three knobs are the exception and keep the stacked form: three to a
narrow panel, a track between two labels is a sliver, so `.hsl-card-knobs`
wraps the readout onto its own line and lets the track take the second. The mask
column takes the ruler column's own width (`col-slider`, 232px) rather than a
strip's 168px, which is what the one-line form costs: eleven rows, and in a
strip's width the labels leave the tracks nothing.
PROFILE is the simulation strip alone, TONE leads with EV and then the finer
move on the same axis, and DETAIL & EFFECTS closes with the D.RANGE strip.
Verified: `npx tsc --noEmit` clean, `npm run build` clean
(`dist/assets/index-CJSnLD0u.js`). Off the running page, through a probe: the
rail reads `◉ PRESETS ★ FAVORITED ☀ LIGHT ⚒ TOOLS ◍ HSL ▣ FRAME ⤓ SAVE RECENT
✎ CREATE RECIPES`; LIGHT's non-grid row is `[{key: curve, label: "∿ TONE
CURVE"}]` and the grid row under it is `["grp-cx", "grp-cxb"]` — 147px + 147px
in a 300px column, both boxes at top 123, nothing overflowing, `COLOR CHROME
OFF` and `CHROME BLUE OFF` each on its chip; its strip is
`[hint-cx, cx:none, cx:weak, cx:strong]` and picking STRONG lands
`{value: "STRONG", amber: false, on: true}` and then `{value: "STRONG", amber:
true}` once the strip closes. The TONE rows come back `EV 15+229+36
one-line=true`, `EXPOSURE 58+198+24`, `CONTRAST 60+214+6`, `HIGHLIGHT
60+214+6`, `SHADOW 50+224+6`, `WHITE 36+238+6`, `BLACK 38+236+6` (label, track,
readout; one line on every one), and the mask column's eleven the same at 232px.
PROFILE holds `{ev: false, dr: false, sim: […11 sims]}` and DETAIL & EFFECTS
holds `{dr: [dr:auto, dr:100, dr:200, dr:400], chrome: false, last: "AUTO DR100
DR200 DR400"}`. The TOOLS row is `[✦ AUTO, ✚ FIX, ▚ GRADIENT MASK, ◑
MONOCHROME]` at 148px each, the marks drawn 26/47/77/15px against the 36px tofu
control, and FIX's and GRADIENT MASK's strips still open
(`[hint-fix, heal, mosaic]`, `[hint-gradient, linear, radial]`) with MONOCHROME
still switching `false → true`. No page errors. `studio-open-check` (updated
for the pair and the single LIGHT chip), `library-check` and `scan-nav-check`
all pass, and the column was viewed with TONE and DETAIL & EFFECTS open.
Co-authored-by: PenguinHarness <noreply@penguin.local>
This commit is contained in:
+61
-34
@@ -2352,20 +2352,32 @@ export function Workspace() {
|
||||
};
|
||||
|
||||
// A strip chip names its pick on the right edge, spelled the way the strip's
|
||||
// own option spells it. Still on its neutral value -> nothing to name.
|
||||
const groupChip = (g: Exclude<GroupKey, 'wm'>): ChipDef => {
|
||||
// own option spells it. Still on its neutral value -> nothing to name — unless
|
||||
// the caller says the chip always names one, which the two Color Chromes do:
|
||||
// the pair is read against each other, and OFF is one of the two values being
|
||||
// compared.
|
||||
const groupChip = (g: Exclude<GroupKey, 'wm'>, nameAlways = false): ChipDef => {
|
||||
const { label, value, off, options } = groupDefs[g];
|
||||
const picked = options.find((o) => o.v === value);
|
||||
return {
|
||||
key: `grp-${g}`,
|
||||
label,
|
||||
value: value !== off ? picked?.d ?? value : undefined,
|
||||
value: value !== off || nameAlways ? picked?.d ?? value : undefined,
|
||||
active: openGroup === g,
|
||||
amberValue: openGroup !== g && value !== off,
|
||||
onClick: () => toggleGroup(g),
|
||||
};
|
||||
};
|
||||
|
||||
// The two Color Chromes, side by side in LIGHT's own column: one row of two
|
||||
// chips, each naming the strength it is on. They are a pair, not a strip —
|
||||
// COLOR CHROME and CHROME BLUE are the same switch over two axes of the same
|
||||
// colour, so they are read against each other rather than scanned in turn, and
|
||||
// they wear the table layout the WB presets wear (see .chip-row.grid /
|
||||
// ChipRow's `grid`). Either one opens its own strip of values in the last
|
||||
// column, the same strip it has always been (see stripChips).
|
||||
const chromeChips: ChipDef[] = [groupChip('cx', true), groupChip('cxb', true)];
|
||||
|
||||
// The pair a chip carries when its option is a whole look: the photo takes the
|
||||
// look while the pointer rests on the chip, and gives it back when it leaves.
|
||||
const lookPreview = (look: Recipe): Pick<ChipDef, 'onHover' | 'onHoverEnd'> => ({
|
||||
@@ -2764,33 +2776,48 @@ export function Workspace() {
|
||||
// LIGHT is the one tab whose knobs are not chips: the sidebar
|
||||
// (DevelopPanels) puts every one of them on screen at once — WB's two,
|
||||
// the tonal range, presence, detail and effects — so what stays in this
|
||||
// row is what is NOT a knob. AUTO, which is an action (see autoTune);
|
||||
// TONE CURVE, which opens the graph on the photo (ToneCurvePanel) rather
|
||||
// than a ruler in the last column, and glows amber once the graph is off
|
||||
// the diagonal; and the three tools that take the pointer — FIX's
|
||||
// brushes, GRADIENT MASK's shapes, MONOCHROME. WB and FX are panels of
|
||||
// this tab now, so their rows live here: a chip that opened the same
|
||||
// record the panel already shows would be a second way to one knob.
|
||||
// row is what is NOT a knob. Only TONE CURVE is left: it opens the graph
|
||||
// on the photo (ToneCurvePanel) rather than a ruler in the last column,
|
||||
// and glows amber once the graph is off the diagonal. It stands over the
|
||||
// TONE panel, whose knobs it is the shape of. The four features that
|
||||
// write rather than name — AUTO, FIX, GRADIENT MASK, MONOCHROME — are
|
||||
// the TOOLS tab's now, and the two Color Chromes are the pair of chips
|
||||
// under this row (see chromeChips). WB and FX are panels of this tab
|
||||
// now, so their rows live here: a chip that opened the same record the
|
||||
// panel already shows would be a second way to one knob.
|
||||
//
|
||||
// GRAIN has no chip of its own for that reason: its two knobs are the
|
||||
// MONOCHROME GRAIN and GRAIN SIZE rows of DETAIL & EFFECTS below, and the
|
||||
// chip that opened a strip of the same two was a second way to them.
|
||||
return [
|
||||
{ key: 'auto', label: 'AUTO', onClick: () => void autoTune() },
|
||||
{
|
||||
key: 'curve',
|
||||
label: 'TONE CURVE',
|
||||
label: '∿ TONE CURVE',
|
||||
active: openParam === 'curve',
|
||||
amberValue: curveIsActive(recipe.adjustments.toneCurve),
|
||||
onClick: () => toggleParam('curve'),
|
||||
},
|
||||
];
|
||||
case 'tools':
|
||||
// TOOLS is the tab of the features that write rather than name: AUTO
|
||||
// reads the loaded photo and lands a whole set of numbers on it (see
|
||||
// autoTune), FIX and GRADIENT MASK take the pointer, MONOCHROME swaps the
|
||||
// base filter for the mono stock. They are one kind of thing — a move
|
||||
// made on the frame, not a look picked for it — which is why they share
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
return [
|
||||
{ key: 'auto', label: '✦ AUTO', onClick: () => void autoTune() },
|
||||
// FX's tools are not looks: each is a strip of its own, opened from the
|
||||
// row. FIX repairs a speck of dust, MOSAIC hides a patch, GRADIENT MASK
|
||||
// draws a shape; the strip's own chip carries the name and glows amber
|
||||
// while there is something inside it to clear.
|
||||
{
|
||||
key: 'fix',
|
||||
label: 'FIX',
|
||||
label: '✚ FIX',
|
||||
active: openGroup === 'fix',
|
||||
amberValue: healSpots.length > 0 || mosaicSpots.length > 0,
|
||||
onClick: () => {
|
||||
@@ -2802,7 +2829,7 @@ export function Workspace() {
|
||||
},
|
||||
{
|
||||
key: 'gradient-mask',
|
||||
label: 'GRADIENT MASK',
|
||||
label: '▚ GRADIENT MASK',
|
||||
active: openGroup === 'gradient',
|
||||
amberValue: masks.length > 0,
|
||||
onClick: () => {
|
||||
@@ -2811,7 +2838,7 @@ export function Workspace() {
|
||||
toggleGroup('gradient');
|
||||
},
|
||||
},
|
||||
{ key: 'mono', label: 'MONOCHROME', active: monoOn, onClick: toggleMono },
|
||||
{ key: 'mono', label: '◑ MONOCHROME', active: monoOn, onClick: toggleMono },
|
||||
];
|
||||
case 'hsl': {
|
||||
// The mixer is PRO: the picker, the eight bands and the three
|
||||
@@ -3022,18 +3049,14 @@ export function Workspace() {
|
||||
|
||||
// The studio sidebar's three slots (DevelopPanels): the picks that name a
|
||||
// whole look rather than a number, so they stay strips of their own inside the
|
||||
// panel that owns them — SIM and D.RANGE with the tone controls, the WB
|
||||
// presets with the two WB tracks, the two Color Chromes at the foot of the
|
||||
// effects column. Every one of them is the strip that already existed, so a
|
||||
// pick made here is the pick made on the tab it came from.
|
||||
// panel that owns them — SIM with the profile, the WB presets with the two WB
|
||||
// tracks, D.RANGE at the foot of the effects. The two Color Chromes are the one
|
||||
// pair that does not live in a panel: they are LIGHT's own row (see
|
||||
// chromeChips). Every one of them is the strip that already existed, so a pick
|
||||
// made here is the pick made on the tab it came from.
|
||||
const developSlots = {
|
||||
profile: (
|
||||
<>
|
||||
<ChipRow chips={choiceChips('sim', groupDefs.style.options, groupDefs.style.value, groupDefs.style.onPick, simPreview)} />
|
||||
<ChipRow
|
||||
chips={choiceChips('dr', DR_OPTIONS, String(recipe.adjustments.dynamicRange ?? 'auto'), groupDefs.dr.onPick)}
|
||||
/>
|
||||
</>
|
||||
<ChipRow chips={choiceChips('sim', groupDefs.style.options, groupDefs.style.value, groupDefs.style.onPick, simPreview)} />
|
||||
),
|
||||
// COLOR TEMP is left out: the ruler is the panel's own TEMPERATURE row, so a
|
||||
// chip that opened the same ruler would be a second way to the same knob.
|
||||
@@ -3066,11 +3089,11 @@ export function Workspace() {
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
// D.RANGE closes DETAIL & EFFECTS: it is a hold on the whole frame, so it
|
||||
// stands at the foot of the effects rather than with the tone knobs it is
|
||||
// read against.
|
||||
effects: (
|
||||
<>
|
||||
<ChipRow chips={choiceChips('cx', CHROME_OPTIONS, recipe.adjustments.colorChrome || 'none', groupDefs.cx.onPick)} />
|
||||
<ChipRow chips={choiceChips('cxb', CHROME_OPTIONS, recipe.adjustments.colorChromeBlue || 'none', groupDefs.cxb.onPick)} />
|
||||
</>
|
||||
<ChipRow chips={choiceChips('dr', DR_OPTIONS, String(recipe.adjustments.dynamicRange ?? 'auto'), groupDefs.dr.onPick)} />
|
||||
),
|
||||
};
|
||||
|
||||
@@ -3227,10 +3250,11 @@ export function Workspace() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* LIGHT's column is the sidebar itself: the two chips that are
|
||||
not knobs, then every knob of the tab in one stack, all of
|
||||
them open at once. */}
|
||||
{/* LIGHT's column is the sidebar itself: the chip that is not
|
||||
a knob, the pair of Color Chromes, then every knob of the tab
|
||||
in one stack, all of them open at once. */}
|
||||
{tab === 'light' ? <ChipRow chips={chips} /> : null}
|
||||
{tab === 'light' ? <ChipRow grid chips={chromeChips} /> : null}
|
||||
{tab === 'light' ? (
|
||||
<DevelopPanels
|
||||
adj={recipe.adjustments}
|
||||
@@ -3386,9 +3410,12 @@ export function Workspace() {
|
||||
feather it fades over, since that is the number its rim is made of.
|
||||
The column is the mask tool's, so putting that tool down — which is
|
||||
what another chip does — takes the column with it. It stands after
|
||||
the strip it came out of, the way a ruler stands after its own. */}
|
||||
the strip it came out of, the way a ruler stands after its own. Eleven
|
||||
rulers are eleven one-line rows, so it takes the ruler column's own
|
||||
width (col-slider): in a strip's 168px the tracks would be left with
|
||||
nothing but what the labels did not want. */}
|
||||
{maskTool && selMask ? (
|
||||
<div className="col col-sub strip" data-col="mask">
|
||||
<div className="col col-sub strip col-slider" data-col="mask">
|
||||
<ChipRow
|
||||
chips={[
|
||||
{ key: 'mask-kind', label: selMask.kind.toUpperCase(), active: true, onClick: () => {} },
|
||||
|
||||
@@ -58,6 +58,7 @@ export const en: Dict = {
|
||||
'tab.favorited': 'FAVORITED',
|
||||
'tab.photos': 'MY PHOTOS',
|
||||
'tab.light': 'LIGHT',
|
||||
'tab.tools': 'TOOLS',
|
||||
'tab.hsl': 'HSL',
|
||||
'hsl.hint': 'PRESS PICK, THEN SAMPLE A COLOUR ON THE PHOTO',
|
||||
'tab.frame': 'FRAME',
|
||||
|
||||
@@ -71,6 +71,7 @@ export const vi = {
|
||||
'tab.favorited': 'FAVORITED',
|
||||
'tab.photos': 'ẢNH CỦA TÔI',
|
||||
'tab.light': 'LIGHT',
|
||||
'tab.tools': 'TOOLS',
|
||||
'tab.hsl': 'HSL',
|
||||
'hsl.hint': 'BẤM PICK RỒI CHỌN MỘT MÀU TRÊN ẢNH',
|
||||
'tab.frame': 'FRAME',
|
||||
|
||||
@@ -219,8 +219,13 @@
|
||||
.slider-row { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 10px; display: flex; align-items: center; gap: 10px; }
|
||||
.col .slider-row { flex-direction: column; align-items: stretch; gap: 6px; }
|
||||
.slider-row .ruler { flex: 1 1 auto; min-width: 0; }
|
||||
.slider-row .head { display: flex; justify-content: space-between; font-size: 11px; letter-spacing: 0.1em; color: var(--text-dim); }
|
||||
.slider-row .head b { color: var(--text); font-family: var(--mono); letter-spacing: 0; }
|
||||
/* The same one-line ruler as the sidebar's rows: the knob's name, its track,
|
||||
then the number it is on — and a colour swatch under them where the knob
|
||||
drives a hue (WB's TEMPERATURE / TINT). */
|
||||
.slider-row .head { display: flex; align-items: center; gap: 8px; font-size: 11px; letter-spacing: 0.1em; color: var(--text-dim); }
|
||||
.slider-row .head span { flex: 0 0 auto; }
|
||||
.slider-row .head input[type="range"] { flex: 1 1 40px; min-width: 0; }
|
||||
.slider-row .head b { flex: 0 0 auto; color: var(--text); font-family: var(--mono); letter-spacing: 0; }
|
||||
.slider-row .head b.amber { color: var(--accent); }
|
||||
.slider-row .swatch {
|
||||
height: 26px;
|
||||
@@ -693,9 +698,20 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
|
||||
.hsl-card-close { border: 1px solid var(--border); background: var(--bg); color: var(--text-dim); border-radius: 999px; padding: 2px 7px; font-size: 11px; cursor: pointer; }
|
||||
.hsl-card-knobs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
|
||||
.mini-slider { min-width: 0; }
|
||||
.mini-head { display: flex; justify-content: space-between; font-size: 10px; letter-spacing: 0.08em; color: var(--text-dim); }
|
||||
.mini-head b { font-family: var(--mono); letter-spacing: 0; color: var(--text); }
|
||||
.mini-head { display: flex; align-items: center; gap: 8px; font-size: 10px; letter-spacing: 0.08em; color: var(--text-dim); }
|
||||
.mini-head span { flex: 0 0 auto; }
|
||||
/* The track takes the width the two labels leave, so the name of the knob opens
|
||||
the row and the number it is on closes it (see MiniSlider). */
|
||||
.mini-head input[type="range"] { flex: 1 1 40px; min-width: 0; }
|
||||
.mini-head b { flex: 0 0 auto; font-family: var(--mono); letter-spacing: 0; color: var(--text); }
|
||||
.mini-head b.amber { color: var(--accent); }
|
||||
/* The mixer's three knobs share one narrow panel, so they keep the stacked form
|
||||
the row above has just left: the name and its number on one line, the track
|
||||
under them. The readout is ordered ahead of the track, which takes the whole
|
||||
second line — in the markup the track sits between the two. */
|
||||
.hsl-card-knobs .mini-head { flex-wrap: wrap; justify-content: space-between; }
|
||||
.hsl-card-knobs .mini-head b { order: 1; }
|
||||
.hsl-card-knobs .mini-head input[type="range"] { order: 2; flex: 1 0 100%; }
|
||||
/* FRAME's STRAIGHTEN scale: a graduated ruler lying along the foot of the
|
||||
photo. The layer is transparent to the pointer so the photo still answers a
|
||||
drag outside the card; the card itself takes the drag. */
|
||||
|
||||
@@ -56,7 +56,8 @@ export interface SliderDef {
|
||||
}
|
||||
|
||||
// `grid` lays the row out as the table a set of same-kind values is read as
|
||||
// (WB's presets) instead of the wrapping strip the option rows use.
|
||||
// (WB's presets, LIGHT's two Color Chromes) instead of the wrapping strip the
|
||||
// option rows use.
|
||||
export function ChipRow({ chips, grid }: { chips: ChipDef[]; grid?: boolean }) {
|
||||
return (
|
||||
<div className={`chip-row${grid ? ' grid' : ''}`}>
|
||||
@@ -128,19 +129,23 @@ export function SliderRow({ slider }: { slider: SliderDef }) {
|
||||
<button type="button" className="back" onClick={slider.onBack} aria-label="back"><</button>
|
||||
) : null}
|
||||
<div className="ruler">
|
||||
{/* Label, track, readout: one line, the way a ruler is read — the name
|
||||
of the knob at the head of the row, the number it is on at the foot,
|
||||
and the travel between them taking the width that is left. */}
|
||||
<div className="head">
|
||||
<span>{slider.label}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
value={slider.value}
|
||||
aria-label={slider.label}
|
||||
onChange={(e) => slider.onChange(Number(e.target.value))}
|
||||
onDoubleClick={slider.onReset}
|
||||
/>
|
||||
<b className={slider.edited ? 'amber' : ''}>{slider.display}</b>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
value={slider.value}
|
||||
onChange={(e) => slider.onChange(Number(e.target.value))}
|
||||
onDoubleClick={slider.onReset}
|
||||
/>
|
||||
{slider.swatch ? (
|
||||
<div
|
||||
className="swatch"
|
||||
@@ -217,23 +222,28 @@ export function MiniSlider({
|
||||
}, [min, max, step]);
|
||||
return (
|
||||
<div className="mini-slider" ref={ref} data-key={dataKey ?? `${prefix}-${label}`}>
|
||||
{/* The studio's rows are one line each, the way a ruler is read: the
|
||||
knob's name, the track, then the number it is on. The mixer's three
|
||||
knobs are the exception — they sit three to a narrow panel, where a
|
||||
track between two labels would be a sliver, so they stack (see
|
||||
.hsl-card-knobs in app.css). */}
|
||||
<div className="mini-head">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
aria-label={label}
|
||||
className={track ? `track-${track}` : undefined}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
onDoubleClick={onReset}
|
||||
/>
|
||||
<b className={value !== defaultValue ? 'amber' : ''} data-key={`${prefix}-${label}-value`}>
|
||||
{format ? format(value) : value > 0 ? `+${value}` : String(value)}
|
||||
</b>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
aria-label={label}
|
||||
className={track ? `track-${track}` : undefined}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
onDoubleClick={onReset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,13 +23,18 @@ const PANELS: {
|
||||
rows: string[];
|
||||
// A slot rides inside its panel, where the parent's own select belongs in the
|
||||
// reading order — before the panel's knobs except in DETAIL & EFFECTS, whose
|
||||
// COLOR CHROME selects close the column.
|
||||
// D.RANGE selects close the column.
|
||||
slot?: 'profile' | 'wb' | 'effects';
|
||||
slotFirst?: boolean;
|
||||
}[] = [
|
||||
{ slug: 'profile', title: 'PROFILE', rows: ['ev'], slot: 'profile', slotFirst: true },
|
||||
// PROFILE is the film simulation and nothing else: EV's ruler is a knob of the
|
||||
// TONE panel's, the range the picture is on, so it sits with the rest of them
|
||||
// and not with the stock the frame is shot on.
|
||||
{ slug: 'profile', title: 'PROFILE', rows: [], slot: 'profile', slotFirst: true },
|
||||
{ slug: 'wb', title: 'WB', rows: ['temperature', 'tint'], slot: 'wb', slotFirst: true },
|
||||
{ slug: 'tone', title: 'TONE', rows: ['exposure', 'contrast', 'highlight', 'shadow', 'whites', 'blacks'] },
|
||||
// EV leads TONE: it is the one number the whole range hangs off, and the
|
||||
// exposure below it is the finer move on the same axis.
|
||||
{ slug: 'tone', title: 'TONE', rows: ['ev', 'exposure', 'contrast', 'highlight', 'shadow', 'whites', 'blacks'] },
|
||||
{ slug: 'presence', title: 'PRESENCE', rows: ['clarity', 'dehaze', 'vibrance', 'color'] },
|
||||
{
|
||||
slug: 'effects',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { MsgKey } from '../i18n/vi';
|
||||
import { useI18n } from '../i18n/I18nProvider';
|
||||
|
||||
// The app's eight tabs. LIGHT is the develop column and holds every knob of the
|
||||
// The app's nine tabs. LIGHT is the develop column and holds every knob of the
|
||||
// picture — white balance, the tonal range, presence and the effects — so WB and
|
||||
// FX, which used to be tabs of their own, are panels of it now. 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' | 'hsl' | 'frame' | 'save' | 'create';
|
||||
// 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
|
||||
// desktop and becomes the bottom bar under 860px (see styles/app.css).
|
||||
export type TabId = 'presets' | 'favorited' | 'photos' | 'light' | 'tools' | 'hsl' | 'frame' | 'save' | 'create';
|
||||
|
||||
// The build this bundle came from, put in by vite at build time and named by
|
||||
// whoever built the image (see vite.config.ts). Empty under a dev server.
|
||||
@@ -17,6 +19,9 @@ export const TABS: { id: TabId; label: MsgKey; glyph: string }[] = [
|
||||
{ id: 'favorited', label: 'tab.favorited', glyph: '★' },
|
||||
{ id: 'photos', label: 'tab.photos', glyph: '▤' },
|
||||
{ id: 'light', label: 'tab.light', glyph: '☀' },
|
||||
// The hammer and pick: the features that work ON the photo — AUTO's reading,
|
||||
// FIX's brushes, GRADIENT MASK's shapes, the MONOCHROME switch.
|
||||
{ id: 'tools', label: 'tab.tools', glyph: '⚒' },
|
||||
// The HSL mixer sits with the colour tabs: it edits one hue band at a time.
|
||||
{ id: 'hsl', label: 'tab.hsl', glyph: '◍' },
|
||||
{ id: 'frame', label: 'tab.frame', glyph: '▣' },
|
||||
|
||||
Reference in New Issue
Block a user