feat: add compressor + fix ui for all sections

This commit is contained in:
2026-08-06 14:13:04 +02:00
parent e110a28f80
commit 78ea04453e
23 changed files with 1721 additions and 665 deletions
+49 -91
View File
@@ -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>
);