aboutsummaryrefslogtreecommitdiff
path: root/src/components/UI
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/UI')
-rw-r--r--src/components/UI/Input.tsx23
-rw-r--r--src/components/UI/Slider.tsx74
-rw-r--r--src/components/UI/Toggle.tsx25
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>
+ )
+}