web: fold the create form's groups and add the white/black point rows
The CREATE RECIPES form had grown into one long scroll. The six categorical groups (SIMULATION, DYNAMIC RANGE, GRAIN EFFECT, COLOR CHROME EFFECT, COLOR CHROME EFFECT BLUE, WHITE BALANCE) are now native <details> folds — the browser keeps the open flag, so no state and no library. The two tone-curve ENDS join the numeric grid: EXPOSURE (the matrix gain the IQ tab already drives), EV (the old EXPOSURE COMP. row, renamed to the phone's word), WHITE and BLACK. WHITE/BLACK are new ColorAdjustments fields, applied in TONE_SKSL as cubic end-weights rather than another smoothstep knee — the HL/SH knees already spend the slope budget, and the cubic keeps the curve monotonic for every combination (derivative >= 0.46), so a brighter input can still never come out darker. Both are optional, so stored recipes keep working.
This commit is contained in:
@@ -80,6 +80,8 @@ export interface ColorAdjustments {
|
|||||||
wbBlue?: number; // Fuji WB fine-tune, Blue axis (-9..9) — extra blue-channel gain
|
wbBlue?: number; // Fuji WB fine-tune, Blue axis (-9..9) — extra blue-channel gain
|
||||||
highlight: number; // -10 to +10 (- pulls highlights down / roll, + brightens)
|
highlight: number; // -10 to +10 (- pulls highlights down / roll, + brightens)
|
||||||
shadow: number; // -10 to +10 (+ lifts shadows, - deepens)
|
shadow: number; // -10 to +10 (+ lifts shadows, - deepens)
|
||||||
|
whites?: number; // -10 to +10 (white point: the very top of the tone curve)
|
||||||
|
blacks?: number; // -10 to +10 (black point: the very toe of the tone curve)
|
||||||
denoise: number; // -10 to +10 (+ = blur sigma; - = add film grain back)
|
denoise: number; // -10 to +10 (+ = blur sigma; - = add film grain back)
|
||||||
clarity: number; // -10 to +10 (mapped to matrix convolution / bloom)
|
clarity: number; // -10 to +10 (mapped to matrix convolution / bloom)
|
||||||
grain: number; // 0 to 10 (mapped to noise turbulence opacity/scale)
|
grain: number; // 0 to 10 (mapped to noise turbulence opacity/scale)
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import { BaseFilter, ColorAdjustments } from '../types';
|
|||||||
// (Fuji extended DR); 0/auto/DR100 = no extra curve.
|
// (Fuji extended DR); 0/auto/DR100 = no extra curve.
|
||||||
// hl - highlight -1..1: + lifts toward white, - rolls the bright side down.
|
// hl - highlight -1..1: + lifts toward white, - rolls the bright side down.
|
||||||
// sh - shadow -1..1: + lifts the dark side, - deepens it.
|
// sh - shadow -1..1: + lifts the dark side, - deepens it.
|
||||||
|
// wh - white point -1..1: + pushes the top of the ramp toward white, - rolls
|
||||||
|
// it down. Cubic end-weight, so it never fights the HL knee.
|
||||||
|
// bl - black point -1..1: + lifts the toe (faded black), - crushes it.
|
||||||
// vib - vibrance -1..1: chroma-masked saturation. It rides along in this shader
|
// vib - vibrance -1..1: chroma-masked saturation. It rides along in this shader
|
||||||
// (rather than the colour matrix) because it needs per-pixel chroma:
|
// (rather than the colour matrix) because it needs per-pixel chroma:
|
||||||
// already-vivid pixels move least, so skins/skies deepen without the neon
|
// already-vivid pixels move least, so skins/skies deepen without the neon
|
||||||
@@ -41,6 +44,8 @@ uniform shader src;
|
|||||||
uniform float dr;
|
uniform float dr;
|
||||||
uniform float hl;
|
uniform float hl;
|
||||||
uniform float sh;
|
uniform float sh;
|
||||||
|
uniform float wh;
|
||||||
|
uniform float bl;
|
||||||
uniform float vib;
|
uniform float vib;
|
||||||
uniform float shTr;
|
uniform float shTr;
|
||||||
uniform float shTg;
|
uniform float shTg;
|
||||||
@@ -61,6 +66,14 @@ vec4 main(vec2 xy) {
|
|||||||
// Dynamic range: gentle shadow lift + highlight roll (protect brights).
|
// Dynamic range: gentle shadow lift + highlight roll (protect brights).
|
||||||
o += dr * 0.12 * shMask * (1.0 - t);
|
o += dr * 0.12 * shMask * (1.0 - t);
|
||||||
o -= dr * 0.18 * hlMask * t;
|
o -= dr * 0.18 * hlMask * t;
|
||||||
|
// White/black point. The HL/SH knees above already spend the slope budget
|
||||||
|
// (each just under 1.0), so these two use cubic end-weights instead of
|
||||||
|
// another smoothstep: only the last/first stretch of the ramp moves, and the
|
||||||
|
// derivative stays >= 1 - 3*0.18 = 0.46 for every combination, so the curve
|
||||||
|
// can never invert. + lifts that end (faded blacks / brighter whites),
|
||||||
|
// - digs it in (true black / rolled white).
|
||||||
|
o += bl * 0.18 * (1.0 - t) * (1.0 - t) * (1.0 - t);
|
||||||
|
o += wh * 0.18 * t * t * t;
|
||||||
o = clamp(o, 0.0, 1.0);
|
o = clamp(o, 0.0, 1.0);
|
||||||
// Lightness takes the curve, hue stays: the colour difference is gained
|
// Lightness takes the curve, hue stays: the colour difference is gained
|
||||||
// only part-way so darkening cannot collapse a colour to black and lifting
|
// only part-way so darkening cannot collapse a colour to black and lifting
|
||||||
@@ -176,6 +189,8 @@ export interface ToneUniforms {
|
|||||||
dr: number; // 0..1
|
dr: number; // 0..1
|
||||||
hl: number; // -1..1 (adjustments.highlight / 10)
|
hl: number; // -1..1 (adjustments.highlight / 10)
|
||||||
sh: number; // -1..1 (adjustments.shadow / 10)
|
sh: number; // -1..1 (adjustments.shadow / 10)
|
||||||
|
wh: number; // -1..1 (adjustments.whites / 10 — white point)
|
||||||
|
bl: number; // -1..1 (adjustments.blacks / 10 — black point)
|
||||||
vib: number; // -1..1 (adjustments.vibrance / 10)
|
vib: number; // -1..1 (adjustments.vibrance / 10)
|
||||||
shT: [number, number, number]; // shadow split-tone RGB bias, -1..1
|
shT: [number, number, number]; // shadow split-tone RGB bias, -1..1
|
||||||
hlT: [number, number, number]; // highlight split-tone RGB bias, -1..1
|
hlT: [number, number, number]; // highlight split-tone RGB bias, -1..1
|
||||||
@@ -208,6 +223,8 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
|
|||||||
const dr = drRaw === 'auto' || drRaw === 100 ? 0 : (drRaw - 100) / 300;
|
const dr = drRaw === 'auto' || drRaw === 100 ? 0 : (drRaw - 100) / 300;
|
||||||
const hl = Math.max(-1, Math.min(1, (adj.highlight ?? 0) / 10));
|
const hl = Math.max(-1, Math.min(1, (adj.highlight ?? 0) / 10));
|
||||||
const sh = Math.max(-1, Math.min(1, (adj.shadow ?? 0) / 10));
|
const sh = Math.max(-1, Math.min(1, (adj.shadow ?? 0) / 10));
|
||||||
|
const wh = Math.max(-1, Math.min(1, (adj.whites ?? 0) / 10));
|
||||||
|
const bl = Math.max(-1, Math.min(1, (adj.blacks ?? 0) / 10));
|
||||||
const vib = Math.max(-1, Math.min(1, (adj.vibrance ?? 0) / 10));
|
const vib = Math.max(-1, Math.min(1, (adj.vibrance ?? 0) / 10));
|
||||||
const film = (baseFilter && FILM_TONE[baseFilter]) || {};
|
const film = (baseFilter && FILM_TONE[baseFilter]) || {};
|
||||||
const shT: [number, number, number] = film.shT ?? [0, 0, 0];
|
const shT: [number, number, number] = film.shT ?? [0, 0, 0];
|
||||||
@@ -224,6 +241,8 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
|
|||||||
dr,
|
dr,
|
||||||
hl: hl + (film.hl ?? 0),
|
hl: hl + (film.hl ?? 0),
|
||||||
sh: sh + (film.sh ?? 0),
|
sh: sh + (film.sh ?? 0),
|
||||||
|
wh,
|
||||||
|
bl,
|
||||||
vib,
|
vib,
|
||||||
shT,
|
shT,
|
||||||
hlT,
|
hlT,
|
||||||
@@ -235,7 +254,7 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
|
|||||||
// Flat uniform buffer for `makeShaderWithChildren` / `<Shader uniforms>` — the
|
// Flat uniform buffer for `makeShaderWithChildren` / `<Shader uniforms>` — the
|
||||||
// order must match TONE_SKSL's declarations.
|
// order must match TONE_SKSL's declarations.
|
||||||
export function toneUniformArray(u: ToneUniforms): number[] {
|
export function toneUniformArray(u: ToneUniforms): number[] {
|
||||||
return [u.dr, u.hl, u.sh, u.vib, u.shT[0], u.shT[1], u.shT[2], u.hlT[0], u.hlT[1], u.hlT[2], u.cc, u.ccb];
|
return [u.dr, u.hl, u.sh, u.wh, u.bl, u.vib, u.shT[0], u.shT[1], u.shT[2], u.hlT[0], u.hlT[1], u.hlT[2], u.cc, u.ccb];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toneIsActive(u: ToneUniforms): boolean {
|
export function toneIsActive(u: ToneUniforms): boolean {
|
||||||
@@ -243,6 +262,8 @@ export function toneIsActive(u: ToneUniforms): boolean {
|
|||||||
u.dr !== 0 ||
|
u.dr !== 0 ||
|
||||||
u.hl !== 0 ||
|
u.hl !== 0 ||
|
||||||
u.sh !== 0 ||
|
u.sh !== 0 ||
|
||||||
|
u.wh !== 0 ||
|
||||||
|
u.bl !== 0 ||
|
||||||
u.vib !== 0 ||
|
u.vib !== 0 ||
|
||||||
u.shT[0] !== 0 ||
|
u.shT[0] !== 0 ||
|
||||||
u.shT[1] !== 0 ||
|
u.shT[1] !== 0 ||
|
||||||
|
|||||||
@@ -193,6 +193,12 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.create-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
.create-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||||||
|
/* Collapsible groups: native <details>, so only the caret is ours. */
|
||||||
|
.create-sec { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.create-sec > summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; user-select: none; }
|
||||||
|
.create-sec > summary::-webkit-details-marker { display: none; }
|
||||||
|
.create-sec > summary::before { content: '▸'; font-size: 9px; transition: transform 0.15s ease; }
|
||||||
|
.create-sec[open] > summary::before { transform: rotate(90deg); }
|
||||||
.create-num { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
.create-num { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||||
.create-num span { font-size: 10px; letter-spacing: 0.12em; color: var(--text-dim); font-weight: 600; }
|
.create-num span { font-size: 10px; letter-spacing: 0.12em; color: var(--text-dim); font-weight: 600; }
|
||||||
.create-num.wide { grid-column: 1 / -1; }
|
.create-num.wide { grid-column: 1 / -1; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||||
import { ChipRow, type ChipDef } from './ChipColumn';
|
import { ChipRow, type ChipDef } from './ChipColumn';
|
||||||
import { useI18n } from '../i18n/I18nProvider';
|
import { useI18n } from '../i18n/I18nProvider';
|
||||||
import { DEFAULT_ADJUSTMENTS, FILM_SIMS } from '../../shared/utils/defaultRecipes';
|
import { DEFAULT_ADJUSTMENTS, FILM_SIMS } from '../../shared/utils/defaultRecipes';
|
||||||
@@ -51,6 +51,17 @@ const wbKeyOf = (s: ColorAdjustments) =>
|
|||||||
return p.temperature === s.temperature && p.tint === s.tint;
|
return p.temperature === s.temperature && p.tint === s.tint;
|
||||||
}) ?? 'custom';
|
}) ?? 'custom';
|
||||||
|
|
||||||
|
// One collapsible group. A native <details> — no state, no library, and the
|
||||||
|
// browser keeps the open/closed flag itself (the `open` prop only seeds it).
|
||||||
|
function Fold({ label, children }: { label: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<details className="create-sec" open>
|
||||||
|
<summary className="create-label">{label}</summary>
|
||||||
|
{children}
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function RecipeCreatePanel({
|
export function RecipeCreatePanel({
|
||||||
currentBase,
|
currentBase,
|
||||||
current,
|
current,
|
||||||
@@ -89,6 +100,9 @@ export function RecipeCreatePanel({
|
|||||||
wbKey: wbKeyOf(s),
|
wbKey: wbKeyOf(s),
|
||||||
highlight: String(s.highlight),
|
highlight: String(s.highlight),
|
||||||
shadow: String(s.shadow),
|
shadow: String(s.shadow),
|
||||||
|
whites: String(s.whites ?? 0),
|
||||||
|
blacks: String(s.blacks ?? 0),
|
||||||
|
exposure: String(s.exposure),
|
||||||
color: String(s.saturation),
|
color: String(s.saturation),
|
||||||
nr: String(s.denoise),
|
nr: String(s.denoise),
|
||||||
sharp: String(s.sharpening),
|
sharp: String(s.sharpening),
|
||||||
@@ -108,6 +122,9 @@ export function RecipeCreatePanel({
|
|||||||
const [wbBlue, setWbBlue] = useState(String(seedAdj.wbBlue ?? 0));
|
const [wbBlue, setWbBlue] = useState(String(seedAdj.wbBlue ?? 0));
|
||||||
const [fHigh, setFHigh] = useState(String(seedAdj.highlight));
|
const [fHigh, setFHigh] = useState(String(seedAdj.highlight));
|
||||||
const [fShadow, setFShadow] = useState(String(seedAdj.shadow));
|
const [fShadow, setFShadow] = useState(String(seedAdj.shadow));
|
||||||
|
const [fWhites, setFWhites] = useState(String(seedAdj.whites ?? 0));
|
||||||
|
const [fBlacks, setFBlacks] = useState(String(seedAdj.blacks ?? 0));
|
||||||
|
const [fExposure, setFExposure] = useState(String(seedAdj.exposure));
|
||||||
const [fColor, setFColor] = useState(String(seedAdj.saturation));
|
const [fColor, setFColor] = useState(String(seedAdj.saturation));
|
||||||
const [fNr, setFNr] = useState(String(seedAdj.denoise));
|
const [fNr, setFNr] = useState(String(seedAdj.denoise));
|
||||||
const [fSharp, setFSharp] = useState(String(seedAdj.sharpening));
|
const [fSharp, setFSharp] = useState(String(seedAdj.sharpening));
|
||||||
@@ -125,6 +142,9 @@ export function RecipeCreatePanel({
|
|||||||
setWbKey(v.wbKey);
|
setWbKey(v.wbKey);
|
||||||
setFHigh(v.highlight);
|
setFHigh(v.highlight);
|
||||||
setFShadow(v.shadow);
|
setFShadow(v.shadow);
|
||||||
|
setFWhites(v.whites);
|
||||||
|
setFBlacks(v.blacks);
|
||||||
|
setFExposure(v.exposure);
|
||||||
setFColor(v.color);
|
setFColor(v.color);
|
||||||
setFNr(v.nr);
|
setFNr(v.nr);
|
||||||
setFSharp(v.sharp);
|
setFSharp(v.sharp);
|
||||||
@@ -146,6 +166,9 @@ export function RecipeCreatePanel({
|
|||||||
wbBlue: int(wbBlue, 0, -9, 9),
|
wbBlue: int(wbBlue, 0, -9, 9),
|
||||||
highlight: int(fHigh, simAdj.highlight, -10, 10),
|
highlight: int(fHigh, simAdj.highlight, -10, 10),
|
||||||
shadow: int(fShadow, simAdj.shadow, -10, 10),
|
shadow: int(fShadow, simAdj.shadow, -10, 10),
|
||||||
|
whites: int(fWhites, simAdj.whites ?? 0, -10, 10),
|
||||||
|
blacks: int(fBlacks, simAdj.blacks ?? 0, -10, 10),
|
||||||
|
exposure: int(fExposure, simAdj.exposure, -10, 10),
|
||||||
saturation: int(fColor, simAdj.saturation, -10, 10),
|
saturation: int(fColor, simAdj.saturation, -10, 10),
|
||||||
denoise: int(fNr, simAdj.denoise, -10, 10),
|
denoise: int(fNr, simAdj.denoise, -10, 10),
|
||||||
sharpening: int(fSharp, simAdj.sharpening, -10, 10),
|
sharpening: int(fSharp, simAdj.sharpening, -10, 10),
|
||||||
@@ -182,7 +205,7 @@ export function RecipeCreatePanel({
|
|||||||
const { baseFilter, adjustments } = build();
|
const { baseFilter, adjustments } = build();
|
||||||
onPreview({ baseFilter, adjustments });
|
onPreview({ baseFilter, adjustments });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [sim, dr, grain, cc, ccb, wbKey, wbRed, wbBlue, fHigh, fShadow, fColor, fNr, fSharp, fClarity, fEv]);
|
}, [sim, dr, grain, cc, ccb, wbKey, wbRed, wbBlue, fHigh, fShadow, fWhites, fBlacks, fColor, fNr, fSharp, fClarity, fExposure, fEv]);
|
||||||
|
|
||||||
const opts = (
|
const opts = (
|
||||||
prefix: string,
|
prefix: string,
|
||||||
@@ -232,71 +255,80 @@ export function RecipeCreatePanel({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<span className="create-label">SIMULATION</span>
|
<Fold label="SIMULATION">
|
||||||
<ChipRow
|
<ChipRow
|
||||||
chips={FILM_SIMS.map((s) => ({
|
chips={FILM_SIMS.map((s) => ({
|
||||||
key: s.id,
|
key: s.id,
|
||||||
label: s.name,
|
label: s.name,
|
||||||
active: sim === s.baseFilter,
|
active: sim === s.baseFilter,
|
||||||
onClick: () => selectSim(s.baseFilter),
|
onClick: () => selectSim(s.baseFilter),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
</Fold>
|
||||||
|
|
||||||
<span className="create-label">DYNAMIC RANGE</span>
|
<Fold label="DYNAMIC RANGE">
|
||||||
<ChipRow chips={opts('dr', DR_OPTS, dr, setDr)} />
|
<ChipRow chips={opts('dr', DR_OPTS, dr, setDr)} />
|
||||||
|
</Fold>
|
||||||
|
|
||||||
<div className="create-grid">
|
<div className="create-grid">
|
||||||
{[
|
{[
|
||||||
{ label: 'HIGHLIGHT', value: fHigh, set: setFHigh },
|
{ label: 'HIGHLIGHT', value: fHigh, set: setFHigh },
|
||||||
{ label: 'SHADOW', value: fShadow, set: setFShadow },
|
{ label: 'SHADOW', value: fShadow, set: setFShadow },
|
||||||
|
{ label: 'WHITE', value: fWhites, set: setFWhites },
|
||||||
|
{ label: 'BLACK', value: fBlacks, set: setFBlacks },
|
||||||
{ label: 'COLOR', value: fColor, set: setFColor },
|
{ label: 'COLOR', value: fColor, set: setFColor },
|
||||||
{ label: 'NOISE RED.', value: fNr, set: setFNr, ph: '-10..10' },
|
{ label: 'NOISE RED.', value: fNr, set: setFNr, ph: '-10..10' },
|
||||||
{ label: 'SHARPENING', value: fSharp, set: setFSharp, ph: '-10..10' },
|
{ label: 'SHARPENING', value: fSharp, set: setFSharp, ph: '-10..10' },
|
||||||
{ label: 'CLARITY', value: fClarity, set: setFClarity },
|
{ label: 'CLARITY', value: fClarity, set: setFClarity },
|
||||||
{ label: 'EXPOSURE COMP.', value: fEv, set: setFEv, ph: 'EV' },
|
{ label: 'EXPOSURE', value: fExposure, set: setFExposure },
|
||||||
|
{ label: 'EV', value: fEv, set: setFEv, ph: '±3 EV' },
|
||||||
].map((f) => num(f.label, f.value, f.set, f.ph))}
|
].map((f) => num(f.label, f.value, f.set, f.ph))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="create-label">GRAIN EFFECT</span>
|
<Fold label="GRAIN EFFECT">
|
||||||
<ChipRow chips={opts('grain', GRAIN_OPTS, grain, setGrain)} />
|
<ChipRow chips={opts('grain', GRAIN_OPTS, grain, setGrain)} />
|
||||||
|
</Fold>
|
||||||
|
|
||||||
<span className="create-label">COLOR CHROME EFFECT</span>
|
<Fold label="COLOR CHROME EFFECT">
|
||||||
<ChipRow chips={opts('cc', CC_OPTS, cc, setCc)} />
|
<ChipRow chips={opts('cc', CC_OPTS, cc, setCc)} />
|
||||||
|
</Fold>
|
||||||
|
|
||||||
<span className="create-label">COLOR CHROME EFFECT BLUE</span>
|
<Fold label="COLOR CHROME EFFECT BLUE">
|
||||||
<ChipRow chips={opts('ccb', CC_OPTS, ccb, setCcb)} />
|
<ChipRow chips={opts('ccb', CC_OPTS, ccb, setCcb)} />
|
||||||
|
</Fold>
|
||||||
|
|
||||||
<span className="create-label">WHITE BALANCE</span>
|
<Fold label="WHITE BALANCE">
|
||||||
<ChipRow
|
<ChipRow
|
||||||
chips={[
|
chips={[
|
||||||
...opts('wb', WB_PRESETS.map((w) => ({ v: w.key, d: w.label })), wbKey, setWbKey),
|
...opts('wb', WB_PRESETS.map((w) => ({ v: w.key, d: w.label })), wbKey, setWbKey),
|
||||||
...(wbKey === 'custom'
|
...(wbKey === 'custom'
|
||||||
? [{ key: 'wb:custom', label: `${seedAdj.temperature}K`, active: true, onClick: () => setWbKey('custom') }]
|
? [{ key: 'wb:custom', label: `${seedAdj.temperature}K`, active: true, onClick: () => setWbKey('custom') }]
|
||||||
: []),
|
: []),
|
||||||
]}
|
]}
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="create-shift">
|
|
||||||
<span className="create-label">WB SHIFT</span>
|
|
||||||
<span className="create-label">RED</span>
|
|
||||||
<input
|
|
||||||
className="field"
|
|
||||||
data-key="create-wbRed"
|
|
||||||
value={wbRed}
|
|
||||||
placeholder="-9..9"
|
|
||||||
inputMode="numeric"
|
|
||||||
onChange={(e) => setWbRed(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
<span className="create-label">BLUE</span>
|
|
||||||
<input
|
<div className="create-shift">
|
||||||
className="field"
|
<span className="create-label">WB SHIFT</span>
|
||||||
data-key="create-wbBlue"
|
<span className="create-label">RED</span>
|
||||||
value={wbBlue}
|
<input
|
||||||
placeholder="-9..9"
|
className="field"
|
||||||
inputMode="numeric"
|
data-key="create-wbRed"
|
||||||
onChange={(e) => setWbBlue(e.target.value)}
|
value={wbRed}
|
||||||
/>
|
placeholder="-9..9"
|
||||||
</div>
|
inputMode="numeric"
|
||||||
|
onChange={(e) => setWbRed(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="create-label">BLUE</span>
|
||||||
|
<input
|
||||||
|
className="field"
|
||||||
|
data-key="create-wbBlue"
|
||||||
|
value={wbBlue}
|
||||||
|
placeholder="-9..9"
|
||||||
|
inputMode="numeric"
|
||||||
|
onChange={(e) => setWbBlue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fold>
|
||||||
|
|
||||||
<div className="create-shift">
|
<div className="create-shift">
|
||||||
<span className="create-label">ISO</span>
|
<span className="create-label">ISO</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user