mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
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 
This commit is contained in:
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";
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./Slider";
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user