Update Vite configuration to deduplicate React and React-DOM; enhance DeviceContext with syncPeqCatalog method for improved PEQ state management; refactor LuxsinAPI to streamline PEQ filter updates; replace range input with ThumbOnlyRangeSlider in Home component for better user experience.

This commit is contained in:
yangy
2026-05-28 15:18:17 +08:00
parent e6b7dcc89c
commit ce55be295a
12 changed files with 1090 additions and 772 deletions
@@ -0,0 +1,145 @@
import { cn } from "@/lib/utils";
import { useRef, type CSSProperties } from "react";
/** Matches .cyan-slider thumb (~17.6px); generous hit area for touch. */
const THUMB_HIT_RADIUS_PX = 24;
function valueFromClientX(input: HTMLInputElement, clientX: number): number {
const rect = input.getBoundingClientRect();
const min = Number(input.min);
const max = Number(input.max);
if (rect.width <= 0) return Number(input.value);
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
return Math.round(min + ratio * (max - min));
}
function isPointerOnThumb(input: HTMLInputElement, clientX: number): boolean {
const rect = input.getBoundingClientRect();
const min = Number(input.min);
const max = Number(input.max);
const value = Number(input.value);
const span = max - min;
const ratio = span > 0 ? (value - min) / span : 0;
const thumbCenterX = rect.left + ratio * rect.width;
return Math.abs(clientX - thumbCenterX) <= THUMB_HIT_RADIUS_PX;
}
const RELEASE_KEYS = [
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Home",
"End",
"PageUp",
"PageDown",
] as const;
export interface ThumbOnlyRangeSliderProps {
value: number;
min: number;
max: number;
disabled?: boolean;
className?: string;
style?: CSSProperties;
onChange: (value: number) => void;
/** Fires when the user releases the thumb after a drag or keyboard adjust. */
onRelease?: (value: number) => void;
onDragStart?: () => void;
onDragEnd?: () => void;
}
/**
* Range slider that ignores track clicks/taps — only the thumb can be dragged.
* Prevents accidental jumps to high volume when missing the thumb.
*/
export default function ThumbOnlyRangeSlider({
value,
min,
max,
disabled = false,
className,
style,
onChange,
onRelease,
onDragStart,
onDragEnd,
}: ThumbOnlyRangeSliderProps) {
const draggingRef = useRef(false);
const emitRelease = (el: EventTarget | null) => {
if (disabled) return;
if (!(el instanceof HTMLInputElement)) return;
onRelease?.(Number(el.value));
onDragEnd?.();
};
return (
<input
type="range"
min={min}
max={max}
value={value}
disabled={disabled}
className={cn("cyan-slider relative z-10", className)}
style={{ touchAction: "none", ...style }}
onChange={(e) => {
if (disabled || draggingRef.current) return;
onChange(Number(e.target.value));
}}
onPointerDown={(e) => {
if (disabled) return;
const input = e.currentTarget;
if (!isPointerOnThumb(input, e.clientX)) {
e.preventDefault();
return;
}
e.preventDefault();
draggingRef.current = true;
onDragStart?.();
input.setPointerCapture(e.pointerId);
onChange(valueFromClientX(input, e.clientX));
}}
onPointerMove={(e) => {
if (disabled || !draggingRef.current) return;
onChange(valueFromClientX(e.currentTarget, e.clientX));
}}
onPointerUp={(e) => {
if (!draggingRef.current) return;
draggingRef.current = false;
try {
e.currentTarget.releasePointerCapture(e.pointerId);
} catch {
// ignore
}
emitRelease(e.currentTarget);
}}
onPointerCancel={(e) => {
if (!draggingRef.current) return;
draggingRef.current = false;
emitRelease(e.currentTarget);
}}
onLostPointerCapture={(e) => {
if (!draggingRef.current) return;
draggingRef.current = false;
emitRelease(e.currentTarget);
}}
onBlur={(e) => {
if (draggingRef.current) {
draggingRef.current = false;
emitRelease(e.currentTarget);
return;
}
if (!disabled) {
onRelease?.(Number(e.currentTarget.value));
}
}}
onKeyUp={(e) => {
if (disabled) return;
if (RELEASE_KEYS.includes(e.key as (typeof RELEASE_KEYS)[number])) {
emitRelease(e.currentTarget);
}
}}
/>
);
}