diff --git a/src/assets/icons/AudioIcons.tsx b/src/assets/icons/AudioIcons.tsx index 8d15471..6fad76c 100644 --- a/src/assets/icons/AudioIcons.tsx +++ b/src/assets/icons/AudioIcons.tsx @@ -113,25 +113,14 @@ export function BoltIcon(props: React.SVGProps) { strokeLinecap="round" strokeLinejoin="round" /> - + ); } export function GainIcon(props: React.SVGProps) { return ( - + ) { export function GateIcon(props: React.SVGProps) { return ( - + ) { export function CompressorIcon(props: React.SVGProps) { return ( - + ) { export function LimiterIcon(props: React.SVGProps) { return ( - + ) { export function DelayIcon(props: React.SVGProps) { return ( - + ) { export function MatrixIcon(props: React.SVGProps) { return ( - + diff --git a/src/components/ChoiceSelect/ChoiceSelect.css b/src/components/ChoiceSelect/ChoiceSelect.css index cd2e8dc..6e373ae 100644 --- a/src/components/ChoiceSelect/ChoiceSelect.css +++ b/src/components/ChoiceSelect/ChoiceSelect.css @@ -28,7 +28,7 @@ font-family: var(--font-ui); font-size: 90%; - + color: inherit; cursor: pointer; @@ -122,4 +122,4 @@ .choice-select__listbox--portal { position: fixed; z-index: 1000; /* clear of any local stacking contexts in the panel */ -} \ No newline at end of file +} diff --git a/src/components/ChoiceSelect/ChoiceSelect.tsx b/src/components/ChoiceSelect/ChoiceSelect.tsx index 374ffea..2f56e92 100644 --- a/src/components/ChoiceSelect/ChoiceSelect.tsx +++ b/src/components/ChoiceSelect/ChoiceSelect.tsx @@ -40,35 +40,35 @@ function ChoiceSelect({ const listboxRef = useRef(null); const suppressNextClick = useRef(false); - useEffect(() => { - function handleMouseDown(e: MouseEvent) { - const target = e.target as Node; - const clickedOutside = - rootRef.current && - !rootRef.current.contains(target) && - !listboxRef.current?.contains(target); + useEffect(() => { + function handleMouseDown(e: MouseEvent) { + const target = e.target as Node; + const clickedOutside = + rootRef.current && + !rootRef.current.contains(target) && + !listboxRef.current?.contains(target); - if (clickedOutside && open) { - setOpen(false); - suppressNextClick.current = true; + if (clickedOutside && open) { + setOpen(false); + suppressNextClick.current = true; + } } - } - function handleClickCapture(e: MouseEvent) { - if (suppressNextClick.current) { - e.preventDefault(); - e.stopPropagation(); - suppressNextClick.current = false; + function handleClickCapture(e: MouseEvent) { + if (suppressNextClick.current) { + e.preventDefault(); + e.stopPropagation(); + suppressNextClick.current = false; + } } - } - document.addEventListener('mousedown', handleMouseDown); - document.addEventListener('click', handleClickCapture, true); // capture phase - return () => { - document.removeEventListener('mousedown', handleMouseDown); - document.removeEventListener('click', handleClickCapture, true); - }; -}, [open]); + document.addEventListener('mousedown', handleMouseDown); + document.addEventListener('click', handleClickCapture, true); // capture phase + return () => { + document.removeEventListener('mousedown', handleMouseDown); + document.removeEventListener('click', handleClickCapture, true); + }; + }, [open]); // Re-check placement/position on scroll/resize while open — the trigger's // position can change (e.g. inside a scrollable panel), and since the @@ -93,27 +93,27 @@ function ChoiceSelect({ }; function updatePlacement() { - const el = triggerRef.current; - if (!el) return; + const el = triggerRef.current; + if (!el) return; - const rect = el.getBoundingClientRect(); - const spaceBelow = window.innerHeight - rect.bottom; - const spaceAbove = rect.top; - const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow; - setDropUp(needsFlip); + const rect = el.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow; + setDropUp(needsFlip); - // Capture the trigger's resolved font-size (its cqh has already resolved - // correctly here, since it's still inside the real container tree) and - // carry it as a plain px value into the portal, where cqh would break. - const fontSize = parseFloat(getComputedStyle(el).fontSize); + // Capture the trigger's resolved font-size (its cqh has already resolved + // correctly here, since it's still inside the real container tree) and + // carry it as a plain px value into the portal, where cqh would break. + const fontSize = parseFloat(getComputedStyle(el).fontSize); - setCoords({ - top: needsFlip ? rect.top - LISTBOX_GAP : rect.bottom + LISTBOX_GAP, - left: rect.left, - width: rect.width, - fontSize, - }); -} + setCoords({ + top: needsFlip ? rect.top - LISTBOX_GAP : rect.bottom + LISTBOX_GAP, + left: rect.left, + width: rect.width, + fontSize, + }); + } function openList() { if (disabled) return; @@ -229,4 +229,4 @@ function ChoiceSelect({ ); } -export default ChoiceSelect; \ No newline at end of file +export default ChoiceSelect; diff --git a/src/components/ChoiceStepper/ChoiceStepper.css b/src/components/ChoiceStepper/ChoiceStepper.css index e99fb68..70bf94f 100644 --- a/src/components/ChoiceStepper/ChoiceStepper.css +++ b/src/components/ChoiceStepper/ChoiceStepper.css @@ -78,9 +78,15 @@ } .choice-stepper[data-disabled] { - opacity: 0.5; + opacity: 0.45; + filter: grayscale(1); } .choice-stepper__input:disabled { cursor: not-allowed; } + +.choice-stepper[data-disabled] .choice-stepper__input, +.choice-stepper[data-disabled] .choice-stepper__arrow-btn { + color: var(--border-hairline); +} diff --git a/src/components/VerticalSlider/VerticalSlider.css b/src/components/VerticalSlider/VerticalSlider.css index 9fcb04b..91e64af 100644 --- a/src/components/VerticalSlider/VerticalSlider.css +++ b/src/components/VerticalSlider/VerticalSlider.css @@ -146,3 +146,26 @@ font-family: inherit; color: inherit; } + +.gauge-viewport.is-disabled { + opacity: 0.45; + filter: grayscale(1); +} + +.gauge-viewport.is-disabled .slider { + cursor: not-allowed; +} + +.gauge-viewport.is-disabled .thumb { + border-color: var(--border-hairline); + box-shadow: none; +} + +.gauge-viewport.is-disabled .value { + color: var(--border-hairline); + box-shadow: none; +} + +.value input:disabled { + cursor: not-allowed; +} diff --git a/src/components/VerticalSlider/VerticalSlider.tsx b/src/components/VerticalSlider/VerticalSlider.tsx index 893518c..20ef333 100644 --- a/src/components/VerticalSlider/VerticalSlider.tsx +++ b/src/components/VerticalSlider/VerticalSlider.tsx @@ -38,6 +38,9 @@ type VerticalSliderProps = { className?: string; style?: React.CSSProperties; + + /** Greys out the slider and blocks all interaction. */ + disabled?: boolean; }; const defaultStep: StepFn = () => 0.1; @@ -174,6 +177,7 @@ export default function VerticalSlider({ formatValue, showTicks = true, scale = 'linear', + disabled = false, className, style, }: VerticalSliderProps) { @@ -240,6 +244,26 @@ export default function VerticalSlider({ return () => observer.disconnect(); }, []); + useEffect(() => { + if (!disabled) return; + + dragging.current = false; + keyboardEditing.current = false; + keyboardValue.current = null; + wheelEditing.current = false; + wheelValue.current = null; + if (wheelCommitTimeout.current) { + clearTimeout(wheelCommitTimeout.current); + wheelCommitTimeout.current = null; + } + + setEditingText(null); + setDisplayValue(value); + dragValue.current = value; + onInput?.(value); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [disabled]); + const decimals = useMemo(() => { if (listValues) return Math.min(3, getDecimals(displayValue)); return Math.min(3, getDecimals(step(displayValue))); @@ -273,6 +297,8 @@ export default function VerticalSlider({ } function handleKeyDown(e: React.KeyboardEvent) { + if (disabled) return; + if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { return; } @@ -307,6 +333,8 @@ export default function VerticalSlider({ } async function handleKeyUp() { + if (disabled) return; + if (keyboardValue.current == null) return; const next = keyboardValue.current; @@ -323,6 +351,7 @@ export default function VerticalSlider({ } function handleWheel(e: WheelEvent) { + if (disabled) return; e.preventDefault(); wheelEditing.current = true; @@ -393,6 +422,8 @@ export default function VerticalSlider({ } function startDrag(e: React.PointerEvent) { + if (disabled) return; + const target = e.currentTarget; dragging.current = true; target.setPointerCapture(e.pointerId); @@ -427,6 +458,11 @@ export default function VerticalSlider({ } async function commitEditing() { + if (disabled) { + setEditingText(null); + return; + } + if (editingText !== null && editingText !== '' && editingText !== '-') { const parsed = Number(editingText); @@ -452,7 +488,10 @@ export default function VerticalSlider({ const percent = valueToPercent(displayValue, min, max, scale, listValues); return ( -
+
setEditingText(value.toFixed(decimals))} // plain number while editing diff --git a/src/features/dsp408/components/ChannelSection/ChannelSection.tsx b/src/features/dsp408/components/ChannelSection/ChannelSection.tsx index 6e0225c..1ed5ce5 100644 --- a/src/features/dsp408/components/ChannelSection/ChannelSection.tsx +++ b/src/features/dsp408/components/ChannelSection/ChannelSection.tsx @@ -11,6 +11,8 @@ import { PEQ_FILTERS_LABEL, PEQ_Q_RANGES, PEAK_Q_VALUES, + PEQ_Q_UNUSED, + PEQ_GAIN_USED, } from '../../constants'; import { Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types'; import { DSP408 as DSP } from '../../../../types'; @@ -128,6 +130,8 @@ function PeqPanelSliders({ onLiveValue: (value: PEQ) => void; }) { const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type]; + const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type); + const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type); const commit = (overrides: Partial) => { const next = { ...peq, ...overrides }; @@ -168,6 +172,7 @@ function PeqPanelSliders({ value={peq.q} onChange={(value) => commit({ q: value })} onInput={(value) => onLiveValue({ ...peq, q: value })} + disabled={qDisabled} style={{ width: '100%', flex: '1 1 auto' }} /> @@ -184,6 +189,7 @@ function PeqPanelSliders({ formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))} onChange={(value) => commit({ gain: value })} onInput={(value) => onLiveValue({ ...peq, gain: value })} + disabled={gainDisabled} style={{ width: '100%', flex: '1 1 auto' }} /> @@ -335,6 +341,9 @@ function PeqBandList({ const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type]; const qValues = PEAK_Q_VALUES.filter((q) => q >= qMin && q <= qMax); + const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type); + const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type); + return (
v.toFixed(2)} + disabled={qDisabled} onChange={(value) => commit(band, { q: value })} /> @@ -380,6 +390,7 @@ function PeqBandList({ max={12} step={0.1} formatLabel={(v) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`} + disabled={gainDisabled} onChange={(value) => commit(band, { gain: value })} /> @@ -715,8 +726,8 @@ function ChannelSection({
e.stopPropagation()}>

Reset {activeChannelState.name} PEQ?

- This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB, - Peak). This action cannot be undone. + This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB, Peak). + This action cannot be undone.

diff --git a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx index 98bcd8d..5f0948b 100644 --- a/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx +++ b/src/features/dsp408/components/DSP408Panel/DSP408Panel.tsx @@ -1,6 +1,17 @@ import { useState } from 'react'; import { Sidebar, SidebarItem } from '../../../../components/Sidebar/Sidebar'; -import { BoltIcon, CompressorIcon, DelayIcon, GainIcon, GateIcon, InfoIcon, LimiterIcon, MatrixIcon, SlidersVerticalIcon, TextIcon } from '../../../../assets/icons/AudioIcons'; +import { + BoltIcon, + CompressorIcon, + DelayIcon, + GainIcon, + GateIcon, + InfoIcon, + LimiterIcon, + MatrixIcon, + SlidersVerticalIcon, + TextIcon, +} from '../../../../assets/icons/AudioIcons'; import ConnectionPanel from '../ConnectionSection/ConnectionSection'; import GainSection from '../GainSection/GainSection'; import CompressorSection from '../CompressorSection/CompressorSection'; diff --git a/src/features/dsp408/components/GainSection/GainSection.tsx b/src/features/dsp408/components/GainSection/GainSection.tsx index 5d3c913..e971708 100644 --- a/src/features/dsp408/components/GainSection/GainSection.tsx +++ b/src/features/dsp408/components/GainSection/GainSection.tsx @@ -97,9 +97,7 @@ function getInputChannelCurves( ): ChannelCurves { const gain = liveGain ?? state.gain; - const geqValues = GEQ_BANDS_LABEL.map( - ({ frequency }) => state.geq.gains[frequency] ?? 0 - ); + const geqValues = GEQ_BANDS_LABEL.map(({ frequency }) => state.geq.gains[frequency] ?? 0); const geq = state.geq.bypass ? [] @@ -109,17 +107,9 @@ function getInputChannelCurves( q: GEQ_CURVE_Q, }); - const peqCrossover = getPeqCrossoverPoints( - freqs, - state.peq_chain, - state.crossover - ); + const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover); - const total = sumCurvePoints([ - geq, - peqCrossover, - getGainCurvePoints(freqs, gain), - ]); + const total = sumCurvePoints([geq, peqCrossover, getGainCurvePoints(freqs, gain)]); return { geq, peqCrossover, total }; } diff --git a/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx b/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx index b60b42a..6cdd069 100644 --- a/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx +++ b/src/features/dsp408/components/MiscellaneousSection/MiscellaneousSection.tsx @@ -112,7 +112,7 @@ function MiscellaneousPanel({ value={inputSource.type} options={INPUT_TYPES} onChange={handleInputTypeChange} - style={{color: 'var(--font-primary)'}} + style={{ color: 'var(--font-primary)' }} />
@@ -132,7 +132,7 @@ function MiscellaneousPanel({ return `${frequency.slice(1)} Hz`; }} onChange={handleFrequencyChange} - style={{color: 'var(--font-primary)'}} + style={{ color: 'var(--font-primary)' }} />
)} diff --git a/src/features/dsp408/constants.ts b/src/features/dsp408/constants.ts index 5c109e1..82ce90e 100644 --- a/src/features/dsp408/constants.ts +++ b/src/features/dsp408/constants.ts @@ -225,6 +225,20 @@ export const PEQ_FILTERS_LABEL: { value: PEQFilter; label: string }[] = [ { value: PEQFilter.AllPass2, label: 'All Pass 2' }, ]; +/** Filter types that ignore Q (first-order filters). */ +export const PEQ_Q_UNUSED = new Set([ + PEQFilter.Lp6Db, + PEQFilter.Hp6Db, + PEQFilter.AllPass1, +]); + +/** Filter types that ignore gain (everything except Peak and the shelves). */ +export const PEQ_GAIN_USED = new Set([ + PEQFilter.Peak, + PEQFilter.LowShelf, + PEQFilter.HighShelf, +]); + export const FREQUENCY_MIN = 19.7; export const FREQUENCY_MAX = 20160; export const FREQUENCY_QUANTIZATION = 300; diff --git a/src/features/dsp408/dsp408.ts b/src/features/dsp408/dsp408.ts index 58bd80d..a6aa8a9 100644 --- a/src/features/dsp408/dsp408.ts +++ b/src/features/dsp408/dsp408.ts @@ -773,7 +773,7 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) { await updateDSPState(); - notify('Channels copied successfully', 'success', dsp.name) + notify('Channels copied successfully', 'success', dsp.name); return true; } catch (err) {