221 lines
8.7 KiB
TypeScript
221 lines
8.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { Dices } from 'lucide-react'
|
|
import { Slider } from './Slider'
|
|
import { Switch } from './Switch'
|
|
import { Dropdown } from './Dropdown'
|
|
import { EFFECTS } from '../config/effects'
|
|
import type { EffectValues } from '../types/effects'
|
|
|
|
interface EffectsBarProps {
|
|
values: EffectValues
|
|
onChange: (parameterId: string, value: number | boolean | string) => void
|
|
onMapClick?: (paramId: string, lfoIndex: number) => void
|
|
getMappedLFOs?: (paramId: string) => number[]
|
|
}
|
|
|
|
export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: EffectsBarProps) {
|
|
const [activeTab, setActiveTab] = useState<string>(EFFECTS[0].id)
|
|
const [isCollapsed, setIsCollapsed] = useState(true)
|
|
const randomizeEffect = (effect: typeof EFFECTS[number]) => {
|
|
effect.parameters.forEach(param => {
|
|
if (param.id.endsWith('Enable')) return
|
|
|
|
if (param.options) {
|
|
const randomOption = param.options[Math.floor(Math.random() * param.options.length)]
|
|
onChange(param.id, randomOption.value)
|
|
} else {
|
|
const range = param.max - param.min
|
|
const steps = Math.floor(range / param.step)
|
|
const randomStep = Math.floor(Math.random() * (steps + 1))
|
|
const randomValue = param.min + (randomStep * param.step)
|
|
onChange(param.id, randomValue)
|
|
}
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="bg-black border-t-2 border-white px-2 lg:px-6 py-3 lg:py-4">
|
|
{/* Desktop: Grid layout */}
|
|
<div className="hidden lg:grid lg:grid-cols-4 lg:gap-4">
|
|
{EFFECTS.map(effect => {
|
|
return (
|
|
<div key={effect.id} className="border-2 border-white p-3">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-mono text-[10px] tracking-[0.2em] text-white">
|
|
{effect.name.toUpperCase()}
|
|
</h3>
|
|
<button
|
|
onClick={() => randomizeEffect(effect)}
|
|
className="p-1 text-white hover:bg-white hover:text-black transition-colors"
|
|
>
|
|
<Dices size={12} strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
{effect.bypassable && (
|
|
<Switch
|
|
checked={!values[`${effect.id}Bypass`]}
|
|
onChange={(checked) => onChange(`${effect.id}Bypass`, !checked)}
|
|
label={values[`${effect.id}Bypass`] ? 'OFF' : 'ON'}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
{effect.parameters.map(param => {
|
|
if (param.options) {
|
|
return (
|
|
<Dropdown
|
|
key={param.id}
|
|
label={param.label}
|
|
value={values[param.id] as string ?? param.default as string}
|
|
options={param.options}
|
|
onChange={(value) => onChange(param.id, value)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
|
|
|
|
if (isSwitch) {
|
|
return (
|
|
<div key={param.id} className="flex flex-col gap-1 mt-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
|
|
{param.label.toUpperCase()}
|
|
</span>
|
|
<Switch
|
|
checked={Boolean(values[param.id])}
|
|
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
|
|
label={values[param.id] ? 'ON' : 'OFF'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Slider
|
|
key={param.id}
|
|
label={param.label}
|
|
value={values[param.id] as number ?? param.default as number}
|
|
min={param.min}
|
|
max={param.max}
|
|
step={param.step}
|
|
unit={param.unit}
|
|
onChange={(value) => onChange(param.id, value)}
|
|
valueId={param.id}
|
|
paramId={param.id}
|
|
onMapClick={onMapClick}
|
|
mappedLFOs={getMappedLFOs ? getMappedLFOs(param.id) : []}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Mobile: Tabbed layout */}
|
|
<div className="lg:hidden flex flex-col">
|
|
<div className="flex border-2 border-white">
|
|
{EFFECTS.map(effect => (
|
|
<button
|
|
key={effect.id}
|
|
onClick={() => {
|
|
if (activeTab === effect.id && !isCollapsed) {
|
|
setIsCollapsed(true)
|
|
} else {
|
|
setActiveTab(effect.id)
|
|
setIsCollapsed(false)
|
|
}
|
|
}}
|
|
className={`flex-1 p-2 font-mono text-[9px] tracking-[0.15em] transition-colors ${
|
|
activeTab === effect.id && !isCollapsed
|
|
? 'bg-white text-black'
|
|
: 'bg-black text-white hover:bg-white/10'
|
|
}`}
|
|
>
|
|
{effect.name.toUpperCase()}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{!isCollapsed && EFFECTS.map(effect => {
|
|
if (activeTab !== effect.id) return null
|
|
return (
|
|
<div key={effect.id} className="border-2 border-t-0 border-white p-3">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => randomizeEffect(effect)}
|
|
className="p-1 text-white hover:bg-white hover:text-black transition-colors"
|
|
>
|
|
<Dices size={14} strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
{effect.bypassable && (
|
|
<Switch
|
|
checked={!values[`${effect.id}Bypass`]}
|
|
onChange={(checked) => onChange(`${effect.id}Bypass`, !checked)}
|
|
label={values[`${effect.id}Bypass`] ? 'OFF' : 'ON'}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col gap-3">
|
|
{effect.parameters.map(param => {
|
|
if (param.options) {
|
|
return (
|
|
<Dropdown
|
|
key={param.id}
|
|
label={param.label}
|
|
value={values[param.id] as string ?? param.default as string}
|
|
options={param.options}
|
|
onChange={(value) => onChange(param.id, value)}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
|
|
|
|
if (isSwitch) {
|
|
return (
|
|
<div key={param.id} className="flex flex-col gap-1 mt-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
|
|
{param.label.toUpperCase()}
|
|
</span>
|
|
<Switch
|
|
checked={Boolean(values[param.id])}
|
|
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
|
|
label={values[param.id] ? 'ON' : 'OFF'}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Slider
|
|
key={param.id}
|
|
label={param.label}
|
|
value={values[param.id] as number ?? param.default as number}
|
|
min={param.min}
|
|
max={param.max}
|
|
step={param.step}
|
|
unit={param.unit}
|
|
onChange={(value) => onChange(param.id, value)}
|
|
valueId={param.id}
|
|
paramId={param.id}
|
|
onMapClick={onMapClick}
|
|
mappedLFOs={getMappedLFOs ? getMappedLFOs(param.id) : []}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
} |