fix: matrix choice selector
This commit is contained in:
@@ -28,17 +28,13 @@ function ChoiceSelect<T extends string>({
|
|||||||
|
|
||||||
if (currentIndex === -1 || options.length === 0) return;
|
if (currentIndex === -1 || options.length === 0) return;
|
||||||
|
|
||||||
const nextIndex =
|
const nextIndex = (currentIndex + direction + options.length) % options.length;
|
||||||
(currentIndex + direction + options.length) % options.length;
|
|
||||||
|
|
||||||
onChange(options[nextIndex]);
|
onChange(options[nextIndex]);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`choice-select ${className}`} data-disabled={disabled || undefined}>
|
||||||
className={`choice-select ${className}`}
|
|
||||||
data-disabled={disabled || undefined}
|
|
||||||
>
|
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
className="choice-select__input"
|
className="choice-select__input"
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
.choice-stepper__arrows {
|
.choice-stepper__arrows {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { JSX, useEffect, useState } from 'react';
|
import { JSX, useEffect, useRef, useState } from 'react';
|
||||||
import Button from '../Button/Button';
|
import Button from '../Button/Button';
|
||||||
import { TriangleUpIcon, TriangleDownIcon } from '../../assets/icons/Triangle';
|
import { TriangleUpIcon, TriangleDownIcon } from '../../assets/icons/Triangle';
|
||||||
import './ChoiceStepper.css';
|
import './ChoiceStepper.css';
|
||||||
|
|
||||||
|
const WHEEL_COMMIT_DELAY = 250;
|
||||||
|
// Safety net: if the parent's `value` prop never comes back exactly equal
|
||||||
|
// to what we committed (rounding, an intermediate transform, etc.), stop
|
||||||
|
// waiting after this long so the stepper doesn't get stuck ignoring
|
||||||
|
// external updates.
|
||||||
|
const PENDING_FALLBACK_MS = 1000;
|
||||||
|
|
||||||
|
type CommitResult = Promise<boolean | void> | boolean | void;
|
||||||
|
|
||||||
function StepperArrows({
|
function StepperArrows({
|
||||||
onUp,
|
onUp,
|
||||||
onDown,
|
onDown,
|
||||||
@@ -44,7 +53,7 @@ type ChoiceStepperListProps<T> = {
|
|||||||
value: T;
|
value: T;
|
||||||
values: readonly T[];
|
values: readonly T[];
|
||||||
formatLabel: (value: T) => string;
|
formatLabel: (value: T) => string;
|
||||||
onChange: (value: T) => void;
|
onChange: (value: T) => CommitResult;
|
||||||
id?: string;
|
id?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -59,38 +68,117 @@ function ChoiceStepperList<T>({
|
|||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
}: ChoiceStepperListProps<T>) {
|
}: ChoiceStepperListProps<T>) {
|
||||||
const index = values.indexOf(value);
|
const [localValue, setLocalValue] = useState(value);
|
||||||
|
const [text, setText] = useState(formatLabel(value));
|
||||||
|
|
||||||
|
// True from the moment a commit starts until the prop `value` we get from
|
||||||
|
// the parent actually reflects it (not just until onChange resolves —
|
||||||
|
// those are two different moments and the gap between them is what was
|
||||||
|
// causing the old-value blink).
|
||||||
|
const interacting = useRef(false);
|
||||||
|
// The value we're waiting to see echoed back via the `value` prop.
|
||||||
|
const pendingValue = useRef<T | null>(null);
|
||||||
|
const wheelTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const fallbackTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (interacting.current) {
|
||||||
|
// Only release control once the prop actually catches up to what we
|
||||||
|
// committed — otherwise keep showing localValue as-is (no flash back
|
||||||
|
// to the stale old value in between).
|
||||||
|
if (pendingValue.current !== null && Object.is(value, pendingValue.current)) {
|
||||||
|
interacting.current = false;
|
||||||
|
pendingValue.current = null;
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLocalValue(value);
|
||||||
|
setText(formatLabel(value));
|
||||||
|
}, [value, formatLabel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (wheelTimeout.current) clearTimeout(wheelTimeout.current);
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const index = values.indexOf(localValue);
|
||||||
const canUp = index === -1 || index < values.length - 1;
|
const canUp = index === -1 || index < values.length - 1;
|
||||||
const canDown = index > 0;
|
const canDown = index > 0;
|
||||||
|
|
||||||
const [text, setText] = useState(formatLabel(value));
|
const stepLocal = (direction: 1 | -1): T | null => {
|
||||||
useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
|
const currentIndex = values.indexOf(localValue);
|
||||||
|
if (currentIndex === -1) return null;
|
||||||
const step = (direction: 1 | -1) => {
|
|
||||||
const currentIndex = values.indexOf(value);
|
|
||||||
|
|
||||||
if (currentIndex === -1) return;
|
|
||||||
|
|
||||||
const nextIndex = currentIndex + direction;
|
const nextIndex = currentIndex + direction;
|
||||||
|
if (nextIndex < 0 || nextIndex >= values.length) return null;
|
||||||
|
|
||||||
if (nextIndex >= 0 && nextIndex < values.length) {
|
const next = values[nextIndex];
|
||||||
onChange(values[nextIndex]);
|
setLocalValue(next);
|
||||||
|
setText(formatLabel(next));
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function commit(next: T) {
|
||||||
|
interacting.current = true;
|
||||||
|
pendingValue.current = next;
|
||||||
|
|
||||||
|
const result = await onChange(next);
|
||||||
|
|
||||||
|
if (result === false) {
|
||||||
|
interacting.current = false;
|
||||||
|
pendingValue.current = null;
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
setLocalValue(value);
|
||||||
|
setText(formatLabel(value));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accepted: don't clear `interacting` here. Wait for the prop-sync
|
||||||
|
// effect above to see `value === next` and release it, so we never
|
||||||
|
// hand control back to a still-stale prop. Guard with a timeout in
|
||||||
|
// case it never matches exactly.
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
fallbackTimeout.current = setTimeout(() => {
|
||||||
|
interacting.current = false;
|
||||||
|
pendingValue.current = null;
|
||||||
|
}, PENDING_FALLBACK_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepAndCommit = (direction: 1 | -1) => {
|
||||||
|
const next = stepLocal(direction);
|
||||||
|
if (next !== null) void commit(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepWheel = (direction: 1 | -1) => {
|
||||||
|
const next = stepLocal(direction);
|
||||||
|
if (next === null) return;
|
||||||
|
|
||||||
|
interacting.current = true;
|
||||||
|
if (wheelTimeout.current) clearTimeout(wheelTimeout.current);
|
||||||
|
wheelTimeout.current = setTimeout(() => {
|
||||||
|
wheelTimeout.current = null;
|
||||||
|
void commit(next);
|
||||||
|
}, WHEEL_COMMIT_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const commitText = () => {
|
const commitText = () => {
|
||||||
const match = values.find(
|
const match = values.find(
|
||||||
(v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase()
|
(v) => formatLabel(v).trim().toLowerCase() === text.trim().toLowerCase()
|
||||||
);
|
);
|
||||||
if (match !== undefined) onChange(match);
|
if (match !== undefined) {
|
||||||
else setText(formatLabel(value));
|
setLocalValue(match);
|
||||||
|
void commit(match);
|
||||||
|
} else {
|
||||||
|
setText(formatLabel(value));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
|
||||||
className={`choice-stepper ${className ?? ''}`}
|
|
||||||
data-disabled={disabled || undefined}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
className="choice-stepper__input"
|
className="choice-stepper__input"
|
||||||
@@ -102,25 +190,32 @@ function ChoiceStepperList<T>({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (e.deltaY < 0 && canUp) {
|
if (e.deltaY < 0 && canUp) {
|
||||||
step(1);
|
stepWheel(1);
|
||||||
} else if (e.deltaY > 0 && canDown) {
|
} else if (e.deltaY > 0 && canDown) {
|
||||||
step(-1);
|
stepWheel(-1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') e.currentTarget.blur();
|
if (e.key === 'Enter') {
|
||||||
else if (e.key === 'ArrowUp') {
|
e.currentTarget.blur();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
step(1);
|
interacting.current = true;
|
||||||
|
stepLocal(1);
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
step(-1);
|
interacting.current = true;
|
||||||
|
stepLocal(-1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||||
|
void commit(localValue);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<StepperArrows
|
<StepperArrows
|
||||||
onUp={() => step(1)}
|
onUp={() => stepAndCommit(1)}
|
||||||
onDown={() => step(-1)}
|
onDown={() => stepAndCommit(-1)}
|
||||||
canUp={canUp}
|
canUp={canUp}
|
||||||
canDown={canDown}
|
canDown={canDown}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -137,7 +232,7 @@ type ChoiceStepperRangeProps = {
|
|||||||
max: number;
|
max: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
formatLabel: (value: number) => string;
|
formatLabel: (value: number) => string;
|
||||||
onChange: (value: number) => void;
|
onChange: (value: number) => CommitResult;
|
||||||
id?: string;
|
id?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -154,30 +249,100 @@ function ChoiceStepperRange({
|
|||||||
disabled,
|
disabled,
|
||||||
className,
|
className,
|
||||||
}: ChoiceStepperRangeProps) {
|
}: ChoiceStepperRangeProps) {
|
||||||
|
const [localValue, setLocalValue] = useState(value);
|
||||||
const [text, setText] = useState(formatLabel(value));
|
const [text, setText] = useState(formatLabel(value));
|
||||||
useEffect(() => setText(formatLabel(value)), [value, formatLabel]);
|
|
||||||
|
const interacting = useRef(false);
|
||||||
|
const pendingValue = useRef<number | null>(null);
|
||||||
|
const wheelTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const fallbackTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (interacting.current) {
|
||||||
|
if (pendingValue.current !== null && Object.is(value, pendingValue.current)) {
|
||||||
|
interacting.current = false;
|
||||||
|
pendingValue.current = null;
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLocalValue(value);
|
||||||
|
setText(formatLabel(value));
|
||||||
|
}, [value, formatLabel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (wheelTimeout.current) clearTimeout(wheelTimeout.current);
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clamp = (n: number) => Math.min(max, Math.max(min, n));
|
const clamp = (n: number) => Math.min(max, Math.max(min, n));
|
||||||
|
|
||||||
const canUp = value < max;
|
const canUp = localValue < max;
|
||||||
const canDown = value > min;
|
const canDown = localValue > min;
|
||||||
|
|
||||||
const step = (direction: 1 | -1) => {
|
const stepLocal = (direction: 1 | -1): number | null => {
|
||||||
const next = clamp(value + direction * stepSize);
|
const next = clamp(localValue + direction * stepSize);
|
||||||
if (next !== value) onChange(next);
|
if (next === localValue) return null;
|
||||||
|
setLocalValue(next);
|
||||||
|
setText(formatLabel(next));
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function commit(next: number) {
|
||||||
|
interacting.current = true;
|
||||||
|
pendingValue.current = next;
|
||||||
|
|
||||||
|
const result = await onChange(next);
|
||||||
|
|
||||||
|
if (result === false) {
|
||||||
|
interacting.current = false;
|
||||||
|
pendingValue.current = null;
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
setLocalValue(value);
|
||||||
|
setText(formatLabel(value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current);
|
||||||
|
fallbackTimeout.current = setTimeout(() => {
|
||||||
|
interacting.current = false;
|
||||||
|
pendingValue.current = null;
|
||||||
|
}, PENDING_FALLBACK_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepAndCommit = (direction: 1 | -1) => {
|
||||||
|
const next = stepLocal(direction);
|
||||||
|
if (next !== null) void commit(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepWheel = (direction: 1 | -1) => {
|
||||||
|
const next = stepLocal(direction);
|
||||||
|
if (next === null) return;
|
||||||
|
|
||||||
|
interacting.current = true;
|
||||||
|
if (wheelTimeout.current) clearTimeout(wheelTimeout.current);
|
||||||
|
wheelTimeout.current = setTimeout(() => {
|
||||||
|
wheelTimeout.current = null;
|
||||||
|
void commit(next);
|
||||||
|
}, WHEEL_COMMIT_DELAY);
|
||||||
};
|
};
|
||||||
|
|
||||||
const commitText = () => {
|
const commitText = () => {
|
||||||
const parsed = Number(text);
|
const parsed = Number(text);
|
||||||
if (Number.isFinite(parsed)) onChange(clamp(parsed));
|
if (Number.isFinite(parsed)) {
|
||||||
else setText(formatLabel(value));
|
const next = clamp(parsed);
|
||||||
|
setLocalValue(next);
|
||||||
|
void commit(next);
|
||||||
|
} else {
|
||||||
|
setText(formatLabel(value));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`choice-stepper ${className ?? ''}`} data-disabled={disabled || undefined}>
|
||||||
className={`choice-stepper ${className ?? ''}`}
|
|
||||||
data-disabled={disabled || undefined}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
className="choice-stepper__input"
|
className="choice-stepper__input"
|
||||||
@@ -189,9 +354,9 @@ function ChoiceStepperRange({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (e.deltaY < 0 && canUp) {
|
if (e.deltaY < 0 && canUp) {
|
||||||
step(1);
|
stepWheel(1);
|
||||||
} else if (e.deltaY > 0 && canDown) {
|
} else if (e.deltaY > 0 && canDown) {
|
||||||
step(-1);
|
stepWheel(-1);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -199,16 +364,26 @@ function ChoiceStepperRange({
|
|||||||
e.currentTarget.blur();
|
e.currentTarget.blur();
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (canUp) step(1);
|
if (canUp) {
|
||||||
|
interacting.current = true;
|
||||||
|
stepLocal(1);
|
||||||
|
}
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (canDown) step(-1);
|
if (canDown) {
|
||||||
|
interacting.current = true;
|
||||||
|
stepLocal(-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||||
|
void commit(localValue);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<StepperArrows
|
<StepperArrows
|
||||||
onUp={() => step(1)}
|
onUp={() => stepAndCommit(1)}
|
||||||
onDown={() => step(-1)}
|
onDown={() => stepAndCommit(-1)}
|
||||||
canUp={canUp}
|
canUp={canUp}
|
||||||
canDown={canDown}
|
canDown={canDown}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -81,29 +81,29 @@ function CompressorPanel({
|
|||||||
<div className="compressor-panel__cell compressor-panel__cell--steppers">
|
<div className="compressor-panel__cell compressor-panel__cell--steppers">
|
||||||
<div className="compressor-panel__stepper-item">
|
<div className="compressor-panel__stepper-item">
|
||||||
<ChoiceStepper
|
<ChoiceStepper
|
||||||
className="compressor-panel__stepper compressor-panel__stepper--centered"
|
className="compressor-panel__stepper compressor-panel__stepper--centered"
|
||||||
value={comp.ratio}
|
value={comp.ratio}
|
||||||
values={RATIO_ORDER}
|
values={RATIO_ORDER}
|
||||||
formatLabel={(v) => RATIO_LABEL[v]}
|
formatLabel={(v) => RATIO_LABEL[v]}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onLiveValue(channel, 'ratio', value);
|
onLiveValue(channel, 'ratio', value);
|
||||||
commit({ ratio: value });
|
commit({ ratio: value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="compressor-panel__cell-label">Ratio</div>
|
<div className="compressor-panel__cell-label">Ratio</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="compressor-panel__stepper-item">
|
<div className="compressor-panel__stepper-item">
|
||||||
<ChoiceStepper
|
<ChoiceStepper
|
||||||
className="compressor-panel__stepper compressor-panel__stepper--centered"
|
className="compressor-panel__stepper compressor-panel__stepper--centered"
|
||||||
value={comp.knee}
|
value={comp.knee}
|
||||||
values={KNEE_VALUES}
|
values={KNEE_VALUES}
|
||||||
formatLabel={(v) => `${v} dB`}
|
formatLabel={(v) => `${v} dB`}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
onLiveValue(channel, 'knee', value);
|
onLiveValue(channel, 'knee', value);
|
||||||
commit({ knee: value });
|
commit({ knee: value });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="compressor-panel__cell-label">Knee</div>
|
<div className="compressor-panel__cell-label">Knee</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -206,10 +206,13 @@ function LimiterSection({ dsp, set_limiter }: { dsp: DSP; set_limiter: SetLimite
|
|||||||
<div className="limiter-graph__meters">
|
<div className="limiter-graph__meters">
|
||||||
{outputs.map((output) => {
|
{outputs.map((output) => {
|
||||||
const outputState = dsp.state.current_config.output_states[output];
|
const outputState = dsp.state.current_config.output_states[output];
|
||||||
const idx = OUTPUT_METER_INDEX[output];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MeterBar key={output} label={outputState.name} meter={dsp.meters.channels[idx]} />
|
<MeterBar
|
||||||
|
key={output}
|
||||||
|
label={outputState.name}
|
||||||
|
meter={dsp.output_meters.meters[output]}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
.matrix-panel__row .choice-stepper {
|
.matrix-panel__row .choice-stepper {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
height: auto;
|
aspect-ratio: 3 / 1;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,4 +109,11 @@
|
|||||||
|
|
||||||
.matrix-gain-stepper .choice-stepper__input {
|
.matrix-gain-stepper .choice-stepper__input {
|
||||||
font-size: 2.8cqh;
|
font-size: 2.8cqh;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.matrix-gain-stepper .choice-stepper__arrows {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
width: 20%;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user