feat: add compressor + fix ui for all sections
This commit is contained in:
@@ -12,32 +12,16 @@ type VerticalSliderProps = {
|
||||
|
||||
value: number;
|
||||
onChange?: (value: number) => Promise<boolean>;
|
||||
/**
|
||||
* Fires synchronously on every live value change — while dragging, on each
|
||||
* keypress — *before* the value is committed via onChange. Use this to
|
||||
* drive UI that should track the slider in real time (e.g. a preview
|
||||
* graph), without waiting for the (async) commit round-trip.
|
||||
*/
|
||||
onInput?: (value: number) => void;
|
||||
|
||||
unit?: string;
|
||||
|
||||
minAspectHeight?: number;
|
||||
maxAspectHeight?: number;
|
||||
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
// Design constants: at the original 70x240 ratio the track is 210 units tall
|
||||
// and holds 15 ticks (2 units tall) laid out with space-between, which works
|
||||
// out to a ~13 unit gap -> a ~15 unit center-to-center pitch. We reuse that
|
||||
// pitch to decide how many ticks fit in whatever track height we actually get.
|
||||
const DESIGN_TRACK_WIDTH_UNITS = 70;
|
||||
const TICK_HEIGHT_UNITS = 2;
|
||||
const TICK_PITCH_UNITS = 15;
|
||||
const MIN_TICKS = 3;
|
||||
const MAX_TICKS = 15;
|
||||
const MAX_TICK_SPACING_PX = 16;
|
||||
|
||||
function getDecimals(n: number) {
|
||||
const s = n.toString();
|
||||
@@ -55,8 +39,6 @@ export default function VerticalSlider({
|
||||
onChange,
|
||||
onInput,
|
||||
unit = '',
|
||||
minAspectHeight = 180,
|
||||
maxAspectHeight = 240,
|
||||
className,
|
||||
style,
|
||||
}: VerticalSliderProps) {
|
||||
@@ -70,7 +52,7 @@ export default function VerticalSlider({
|
||||
const dragging = useRef(false);
|
||||
const thumbRef = useRef<HTMLDivElement>(null);
|
||||
const keyboardEditing = useRef(false);
|
||||
const [tickCount, setTickCount] = useState(MAX_TICKS);
|
||||
const [tickCount, setTickCount] = useState(MIN_TICKS);
|
||||
|
||||
useEffect(() => {
|
||||
if (dragging.current || keyboardEditing.current || editingText !== null) {
|
||||
@@ -85,20 +67,13 @@ export default function VerticalSlider({
|
||||
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||
|
||||
const recompute = () => {
|
||||
const w = el.clientWidth;
|
||||
const h = el.clientHeight;
|
||||
if (!w || !h) return;
|
||||
if (!h) return;
|
||||
|
||||
// .gauge-track's width is always calc(70 * --s), so this recovers --s in px.
|
||||
const s = w / DESIGN_TRACK_WIDTH_UNITS;
|
||||
const heightUnits = h / s;
|
||||
|
||||
const raw = 1 + (heightUnits - TICK_HEIGHT_UNITS) / TICK_PITCH_UNITS;
|
||||
const n = Math.max(MIN_TICKS, Math.min(MAX_TICKS, Math.round(raw)));
|
||||
const n = Math.max(MIN_TICKS, Math.ceil(h / MAX_TICK_SPACING_PX) + 1);
|
||||
|
||||
setTickCount(n);
|
||||
};
|
||||
|
||||
recompute();
|
||||
|
||||
const observer = new ResizeObserver(recompute);
|
||||
@@ -122,17 +97,13 @@ export default function VerticalSlider({
|
||||
|
||||
function normalizeValue(input: number) {
|
||||
const clamped = Math.min(max, Math.max(min, input));
|
||||
|
||||
const currentStep = getStep(clamped);
|
||||
|
||||
const snapped = min + Math.round((clamped - min) / currentStep) * currentStep;
|
||||
|
||||
return Number(Math.min(max, Math.max(min, snapped)).toFixed(6));
|
||||
}
|
||||
|
||||
function updateValue(input: number) {
|
||||
const next = normalizeValue(input);
|
||||
|
||||
dragValue.current = next;
|
||||
setDisplayValue(next);
|
||||
onInput?.(next);
|
||||
@@ -144,11 +115,9 @@ export default function VerticalSlider({
|
||||
}
|
||||
|
||||
keyboardEditing.current = true;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const base = keyboardValue.current ?? value;
|
||||
|
||||
const currentStep = getStep(base);
|
||||
|
||||
let next = base;
|
||||
@@ -162,9 +131,7 @@ export default function VerticalSlider({
|
||||
}
|
||||
|
||||
next = normalizeValue(next);
|
||||
|
||||
keyboardValue.current = next;
|
||||
|
||||
updateValue(next);
|
||||
}
|
||||
|
||||
@@ -172,7 +139,6 @@ export default function VerticalSlider({
|
||||
if (keyboardValue.current == null) return;
|
||||
|
||||
const next = keyboardValue.current;
|
||||
|
||||
const success = await onChange?.(next);
|
||||
|
||||
keyboardEditing.current = false;
|
||||
@@ -209,7 +175,6 @@ export default function VerticalSlider({
|
||||
if (!dragging.current) return;
|
||||
|
||||
const finalValue = dragValue.current;
|
||||
|
||||
const success = await onChange?.(finalValue);
|
||||
|
||||
dragging.current = false;
|
||||
@@ -250,63 +215,56 @@ export default function VerticalSlider({
|
||||
|
||||
const percent = (displayValue - min) / (max - min);
|
||||
|
||||
const containerStyle = {
|
||||
'--min-height-units': minAspectHeight,
|
||||
'--max-height-units': maxAspectHeight,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return (
|
||||
<div className={['gauge-viewport', className].filter(Boolean).join(' ')} style={style}>
|
||||
<div className="gauge-container" style={containerStyle}>
|
||||
<div className="gauge-track" ref={trackRef}>
|
||||
<div className="ticks left">
|
||||
{Array.from({ length: tickCount }).map((_, i) => (
|
||||
<span key={i} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="slider"
|
||||
ref={sliderRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onPointerDown={startDrag}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={endDrag}
|
||||
onPointerCancel={endDrag}
|
||||
>
|
||||
<div className="track" />
|
||||
<div ref={thumbRef} className="thumb" style={{ bottom: `${percent * 100}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="ticks right">
|
||||
{Array.from({ length: tickCount }).map((_, i) => (
|
||||
<span key={i} />
|
||||
))}
|
||||
</div>
|
||||
<div className="gauge-track" ref={trackRef}>
|
||||
<div className="ticks left">
|
||||
{Array.from({ length: tickCount }).map((_, i) => (
|
||||
<span key={i} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="value">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={editingText ?? displayValue.toFixed(decimals)}
|
||||
style={{ width: '100%' }}
|
||||
onFocus={() => setEditingText(value.toFixed(decimals))}
|
||||
onChange={(e) => setEditingText(e.target.value)}
|
||||
onBlur={commitEditing}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') inputRef.current?.blur();
|
||||
if (e.key === 'Escape') {
|
||||
setEditingText(null);
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="value-unit">{unit}</span>
|
||||
<div
|
||||
className="slider"
|
||||
ref={sliderRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
onPointerDown={startDrag}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={endDrag}
|
||||
onPointerCancel={endDrag}
|
||||
>
|
||||
<div className="track" />
|
||||
<div ref={thumbRef} className="thumb" style={{ bottom: `${percent * 100}%` }} />
|
||||
</div>
|
||||
|
||||
<div className="ticks right">
|
||||
{Array.from({ length: tickCount }).map((_, i) => (
|
||||
<span key={i} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="value">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={editingText ?? displayValue.toFixed(decimals)}
|
||||
style={{ width: '100%' }}
|
||||
onFocus={() => setEditingText(value.toFixed(decimals))}
|
||||
onChange={(e) => setEditingText(e.target.value)}
|
||||
onBlur={commitEditing}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') inputRef.current?.blur();
|
||||
if (e.key === 'Escape') {
|
||||
setEditingText(null);
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="value-unit">{unit}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user