new percentage strength slider update with number input (#540)

- new custom reusable components: number input and slider
- better looking slider UI
- strength slider now shows percentage visually, but it actually returns
value 0.0-1.0
- users can input numbers from 0-100, arrow buttons increment/decrement
the value


![image](https://github.com/storytold/GottaGoFast/assets/46063141/43b915f4-1f75-4784-86cd-fc60a5bf8c0d)
This commit is contained in:
BFlat authored and GitHub committed 2024-06-26 16:43:17 -04:00
1 parent 1ffe6a62f9
commit 057c8885d6
6 files changed
+244 -26

No files matched your search

@@ -0,0 +1,86 @@
import React, { useEffect, useState } from "react";
import { Input } from "../Input";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown, faChevronUp } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
interface NumberInputProps {
value: number;
onChange: (value: number) => void;
}
export const NumberInput: React.FC<NumberInputProps> = ({
value,
onChange,
}) => {
const [inputValue, setInputValue] = useState<string>(value.toString());
useEffect(() => {
setInputValue(Math.round(value * 100) / 100 + "");
}, [value]);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
if (
newValue === "" ||
(!isNaN(Number(newValue)) && Number(newValue) <= 100)
) {
setInputValue(newValue);
const numericValue = parseFloat(newValue);
if (newValue !== "") {
onChange(Math.round(numericValue * 100) / 100);
}
}
};
const handleBlur = () => {
if (inputValue === "") {
setInputValue(Math.round(value * 100) / 100 + "");
}
};
const handleIncrement = () => {
const newValue = Math.min(Number(inputValue) + 1, 100);
setInputValue(Math.round(newValue * 100) / 100 + "");
onChange(Math.round(newValue * 100) / 100);
};
const handleDecrement = () => {
const newValue = Math.max(Number(inputValue) - 1, 0);
setInputValue(Math.round(newValue * 100) / 100 + "");
onChange(Math.round(newValue * 100) / 100);
};
const incrementButtonStyle =
"flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-ui-controls-button outline-none focus:outline-none transition-all active:outline-none";
return (
<div className="relative">
<Input
type="text"
value={inputValue}
onChange={handleChange}
onBlur={handleBlur}
inputClassName="w-[70px] h-[30px] rounded-md text-sm"
/>
<div className="absolute right-0 top-0 flex h-[30px] flex-col rounded-r-md border-l border-white/15 bg-gray-800">
<button
className={twMerge(
incrementButtonStyle,
"rounded-tr-md border-b border-white/10",
)}
onClick={handleIncrement}
>
<FontAwesomeIcon icon={faChevronUp} />
</button>
<button
className={twMerge(incrementButtonStyle, "rounded-br-md")}
onClick={handleDecrement}
>
<FontAwesomeIcon icon={faChevronDown} />
</button>
</div>
</div>
);
};
@@ -0,0 +1 @@
export * from "./NumberInput";
+130
View File
@@ -0,0 +1,130 @@
import { Transition } from "@headlessui/react";
import React, { useState, useRef, useEffect, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
interface SliderProps {
min: number;
max: number;
value: number;
onChange: (value: number) => void;
step: number;
className?: string;
showTooltip?: boolean;
tooltipContent?: ReactNode;
suffix?: string;
}
export const Slider: React.FC<SliderProps> = ({
min,
max,
value,
onChange,
step,
className,
showTooltip,
tooltipContent,
suffix,
}) => {
const [localValue, setLocalValue] = useState(value);
const sliderRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [showHandleTooltip, setShowHandleTooltip] = useState(false);
useEffect(() => {
setLocalValue(value);
}, [value]);
const updatePosition = (x: number) => {
if (!sliderRef.current) return;
const rect = sliderRef.current.getBoundingClientRect();
const fraction = (x - rect.left) / rect.width;
const newValue = fraction * (max - min) + min;
const roundedValue = Math.round(newValue / step) * step;
const clampedValue = Math.min(Math.max(roundedValue, min), max);
setLocalValue(clampedValue);
onChange(clampedValue);
};
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
setShowHandleTooltip(true);
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
updatePosition(e.clientX);
};
const handleMouseMove = (e: MouseEvent) => {
e.preventDefault();
updatePosition(e.clientX);
};
const handleMouseUp = (e: MouseEvent) => {
e.preventDefault();
setIsDragging(false);
setShowHandleTooltip(false);
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
const handleMouseEnter = () => {
if (!isDragging) {
setShowHandleTooltip(true);
}
};
const handleMouseLeave = () => {
if (!isDragging) {
setShowHandleTooltip(false);
}
};
const percentage = ((localValue - min) / (max - min)) * 100;
const displayValue =
localValue.toFixed(step.toString().split(".")[1]?.length || 0) +
(suffix || "");
return (
<div
ref={sliderRef}
className={twMerge(
"relative h-2.5 w-full cursor-pointer rounded-full bg-gray-500",
className,
)}
onMouseDown={handleMouseDown}
>
<div
className="absolute h-2.5 rounded-full bg-brand-primary"
style={{ width: `${percentage}%` }}
></div>
<div
className={twMerge(
"absolute -top-1 h-[18px] w-[18px] -translate-x-1/2 transform rounded-full bg-gray-200 transition-colors",
isDragging ? "bg-white" : "bg-gray-300 hover:bg-white",
)}
style={{ left: `${percentage}%` }}
onMouseDown={handleMouseDown}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<Transition
show={showHandleTooltip}
enter="transition ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0"
>
{showTooltip && showHandleTooltip && (
<div
className="absolute -top-8 z-10 rounded-md bg-ui-controls px-2 py-1 text-xs text-white shadow-lg"
style={{ left: `${percentage}%`, transform: "translateX(-50%)" }}
>
{tooltipContent ? tooltipContent : displayValue}
</div>
)}
</Transition>
</div>
);
};
+1
View File
@@ -0,0 +1 @@
export * from "./Slider";
+2
View File
@@ -14,9 +14,11 @@ export * from "./Input"; //need to decouple hotkey signal
export * from "./InputVector"; //need to decouple hotkey signal
export * from "./ListDropdown";
export * from "./LoadingBar";
export * from "./NumberInput";
export * from "./Pagination";
export * from "./Pill";
export * from "./Select"; //Select's onChange type should be generic
export * from "./Slider";
export * from "./Tabs";
export * from "./Textarea";
export * from "./Tooltip";
@@ -1,4 +1,4 @@
import { Button, Label, PremiumLock } from "~/components";
import { Button, Label, NumberInput, PremiumLock, Slider } from "~/components";
import {
faArrowsRotate,
faChevronLeft,
@@ -10,6 +10,8 @@ import Queue from "~/pages/PageEnigma/Queue/Queue";
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
import { Switch } from "@headlessui/react";
// import Slider from "rc-slider";
// import "rc-slider/assets/index.css";
import {
faceDetail,
@@ -22,8 +24,12 @@ import {
export function StyleButtons() {
useSignals();
const sliderChanged = (event: React.ChangeEvent<HTMLInputElement>) => {
styleStrength.value = parseFloat(event.target.value);
const sliderChanged = (value: number | number[]) => {
styleStrength.value = (value as number) / 100;
};
const handleNumberInputChange = (value: number) => {
styleStrength.value = value / 100;
};
const switchPreview = async () => {
@@ -202,35 +208,27 @@ export function StyleButtons() {
<div>
<Label>
<div className="mb-1 leading-tight">Set the Style Strength</div>
<div className="mb-1 leading-tight">Set the Style Strength (%)</div>
</Label>
<div className="mb-2 text-xs text-white/70">
<div className="mb-2.5 text-xs text-white/70">
(The higher the value the more the style will be applied, the lower
the value the closer to source.)
</div>
<div className="flex gap-2">
<div className="rounde w-8 py-0.5 text-center font-medium">
{styleStrength.value}
</div>
<input
className="
active:accent-red-700
w-full
cursor-pointer
accent-brand-primary
outline-none
transition-all
duration-150
hover:accent-brand-primary-400
focus:outline-none
"
type="range"
value={styleStrength.value}
min="0"
max="1.0"
step="0.1"
<div className="mb-2 flex items-center gap-3.5">
<NumberInput
value={styleStrength.value * 100}
onChange={handleNumberInputChange}
/>
<Slider
value={styleStrength.value * 100}
min={0}
max={100}
step={1}
onChange={sliderChanged}
showTooltip={true}
suffix="%"
className="mr-1"
/>
</div>
</div>