feat: gate sectioon
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import './VerticalSlider.css';
|
||||
|
||||
@@ -12,13 +12,33 @@ 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;
|
||||
|
||||
function getDecimals(n: number) {
|
||||
const s = n.toString();
|
||||
const i = s.indexOf('.');
|
||||
@@ -33,19 +53,59 @@ export default function VerticalSlider({
|
||||
switchStep,
|
||||
value,
|
||||
onChange,
|
||||
onInput,
|
||||
unit = '',
|
||||
minAspectHeight = 180,
|
||||
maxAspectHeight = 240,
|
||||
className,
|
||||
style,
|
||||
}: VerticalSliderProps) {
|
||||
const [editingText, setEditingText] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const keyboardValue = useRef<number | null>(null);
|
||||
const [displayValue, setDisplayValue] = useState(value);
|
||||
const dragValue = useRef(value);
|
||||
const dragging = useRef(false);
|
||||
const thumbRef = useRef<HTMLDivElement>(null);
|
||||
const keyboardEditing = useRef(false);
|
||||
const [tickCount, setTickCount] = useState(MAX_TICKS);
|
||||
|
||||
useEffect(() => {
|
||||
if (dragging.current || keyboardEditing.current || editingText !== null) {
|
||||
return;
|
||||
}
|
||||
setDisplayValue(value);
|
||||
dragValue.current = value;
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = trackRef.current;
|
||||
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||
|
||||
const recompute = () => {
|
||||
const w = el.clientWidth;
|
||||
const h = el.clientHeight;
|
||||
if (!w || !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)));
|
||||
|
||||
setTickCount(n);
|
||||
};
|
||||
|
||||
recompute();
|
||||
|
||||
const observer = new ResizeObserver(recompute);
|
||||
observer.observe(el);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
function getStep(value: number) {
|
||||
if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) {
|
||||
@@ -75,6 +135,7 @@ export default function VerticalSlider({
|
||||
|
||||
dragValue.current = next;
|
||||
setDisplayValue(next);
|
||||
onInput?.(next);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
@@ -120,6 +181,7 @@ export default function VerticalSlider({
|
||||
if (success === false) {
|
||||
setDisplayValue(value);
|
||||
dragValue.current = value;
|
||||
onInput?.(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +217,7 @@ export default function VerticalSlider({
|
||||
if (success === false) {
|
||||
setDisplayValue(value);
|
||||
dragValue.current = value;
|
||||
onInput?.(value);
|
||||
}
|
||||
|
||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||
@@ -170,12 +233,14 @@ export default function VerticalSlider({
|
||||
const next = normalizeValue(parsed);
|
||||
|
||||
setDisplayValue(next);
|
||||
onInput?.(next);
|
||||
|
||||
const success = await onChange?.(next);
|
||||
|
||||
if (success === false) {
|
||||
setDisplayValue(value);
|
||||
dragValue.current = value;
|
||||
onInput?.(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,17 +250,17 @@ 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"
|
||||
>
|
||||
<div className="gauge-track">
|
||||
<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: 15 }).map((_, i) => (
|
||||
{Array.from({ length: tickCount }).map((_, i) => (
|
||||
<span key={i} />
|
||||
))}
|
||||
</div>
|
||||
@@ -216,7 +281,7 @@ export default function VerticalSlider({
|
||||
</div>
|
||||
|
||||
<div className="ticks right">
|
||||
{Array.from({ length: 15 }).map((_, i) => (
|
||||
{Array.from({ length: tickCount }).map((_, i) => (
|
||||
<span key={i} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user