diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-06-05 20:52:54 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-06-05 20:52:54 +0000 |
| commit | 09f964451d7d92e9891430ec4595c1276d486aab (patch) | |
| tree | 28da4483f5c28924a8c47fceb648b1baebe88224 /src/components/UI | |
| download | yace-master.tar.gz yace-master.zip | |
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'src/components/UI')
| -rw-r--r-- | src/components/UI/Input.tsx | 23 | ||||
| -rw-r--r-- | src/components/UI/Slider.tsx | 74 | ||||
| -rw-r--r-- | src/components/UI/Toggle.tsx | 25 |
3 files changed, 122 insertions, 0 deletions
diff --git a/src/components/UI/Input.tsx b/src/components/UI/Input.tsx new file mode 100644 index 0000000..e6a84c0 --- /dev/null +++ b/src/components/UI/Input.tsx @@ -0,0 +1,23 @@ +interface InputProps { + value: string | number + onChange: (value: string) => void + placeholder?: string + type?: string + min?: number + max?: number +} + +export default function Input({ value, onChange, placeholder, type = 'text', min, max }: InputProps) { + return ( + <input + type={type} + value={value} + placeholder={placeholder} + min={min} + max={max} + onChange={(e) => onChange(e.target.value)} + className="w-full h-7 px-2 rounded-md text-ui-sm text-text-primary bg-editor-surface border transition-all duration-150 outline-none placeholder:text-text-muted focus:border-accent" + style={{ borderColor: 'var(--color-panel-border)' }} + /> + ) +} diff --git a/src/components/UI/Slider.tsx b/src/components/UI/Slider.tsx new file mode 100644 index 0000000..22a65ee --- /dev/null +++ b/src/components/UI/Slider.tsx @@ -0,0 +1,74 @@ +import { useRef, useCallback, useEffect, useState } from 'react' + +interface SliderProps { + value: number + min: number + max: number + step?: number + onChange: (value: number) => void +} + +export default function Slider({ value, min, max, step = 1, onChange }: SliderProps) { + const trackRef = useRef<HTMLDivElement>(null) + const [dragging, setDragging] = useState(false) + + const pct = ((value - min) / (max - min)) * 100 + + const updateFromClient = useCallback((clientX: number) => { + const rect = trackRef.current?.getBoundingClientRect() + if (!rect) return + const raw = (clientX - rect.left) / rect.width + const clamped = Math.max(0, Math.min(1, raw)) + const stepped = Math.round((min + clamped * (max - min)) / step) * step + onChange(Math.max(min, Math.min(max, stepped))) + }, [min, max, step, onChange]) + + const handleMouseDown = useCallback((e: React.MouseEvent) => { + e.preventDefault() + setDragging(true) + updateFromClient(e.clientX) + }, [updateFromClient]) + + useEffect(() => { + if (!dragging) return + const handleMove = (e: MouseEvent) => { e.preventDefault(); updateFromClient(e.clientX) } + const handleUp = () => setDragging(false) + window.addEventListener('mousemove', handleMove) + window.addEventListener('mouseup', handleUp) + return () => { window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleUp) } + }, [dragging, updateFromClient]) + + return ( + <div + ref={trackRef} + className="relative h-4 w-full cursor-pointer flex items-center" + onMouseDown={handleMouseDown} + role="slider" + aria-valuemin={min} + aria-valuemax={max} + aria-valuenow={value} + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); onChange(Math.min(max, value + step)) } + if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); onChange(Math.max(min, value - step)) } + }} + > + <div className="absolute h-[3px] w-full rounded-full" style={{ backgroundColor: 'var(--color-panel-border)' }}> + <div + className="absolute h-full rounded-full transition-[width] duration-75" + style={{ width: `${pct}%`, backgroundColor: 'var(--color-accent)' }} + /> + </div> + <div + className="absolute w-3.5 h-3.5 rounded-full shadow-sm border-2 transition-transform duration-75" + style={{ + left: `${pct}%`, + marginLeft: '-7px', + backgroundColor: dragging ? 'var(--color-accent)' : 'var(--color-panel-bg)', + borderColor: 'var(--color-accent)', + transform: dragging ? 'scale(1.15)' : 'scale(1)', + }} + /> + </div> + ) +} diff --git a/src/components/UI/Toggle.tsx b/src/components/UI/Toggle.tsx new file mode 100644 index 0000000..17b98e2 --- /dev/null +++ b/src/components/UI/Toggle.tsx @@ -0,0 +1,25 @@ +interface ToggleProps { + checked: boolean + onChange: (checked: boolean) => void + disabled?: boolean +} + +export default function Toggle({ checked, onChange, disabled }: ToggleProps) { + return ( + <button + role="switch" + aria-checked={checked} + disabled={disabled} + className="relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors duration-200 disabled:opacity-30 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-transparent" + style={{ + backgroundColor: checked ? 'var(--color-accent)' : 'var(--color-panel-border)', + }} + onClick={() => { if (!disabled) onChange(!checked) }} + > + <span + className="inline-block h-[16px] w-[16px] rounded-full bg-white shadow-sm transition-transform duration-200" + style={{ transform: checked ? 'translateX(18px)' : 'translateX(3px)', marginTop: '2px' }} + /> + </button> + ) +} |