feat: improve gain section

This commit is contained in:
2026-07-28 18:41:40 +02:00
parent eab62e151e
commit 255b1f217e
9 changed files with 129 additions and 204 deletions

View File

@ -36,3 +36,34 @@
border-color: var(--border-hairline); border-color: var(--border-hairline);
color: var(--text-muted); color: var(--text-muted);
} }
.btn-ghost:hover:not(:disabled),
.btn-toggle:hover:not(:disabled),
.btn-outline:hover:not(:disabled) {
background: var(--bg-raised);
color: var(--text-primary);
}
/* Active toggle states */
.btn-toggle.active-danger {
background: var(--accent-down);
border-color: var(--accent-down);
color: var(--bg-void);
}
.btn-toggle.active-warning {
background: var(--accent-connecting);
border-color: var(--accent-connecting);
color: var(--bg-void);
}
/* Optional: active state hover */
.btn-toggle.active-danger:hover:not(:disabled) {
background: var(--accent-down);
border-color: var(--accent-down);
}
.btn-toggle.active-warning:hover:not(:disabled) {
background: var(--accent-connecting);
border-color: var(--accent-connecting);
}

View File

@ -1,16 +1,15 @@
.channel-group { .channel-group {
/* container-type: inline-size; */
position: relative; position: relative;
height: calc(100% - var(--space-1) - var(--space-1)); flex: 1 1 0;
flex: 0 0 auto; min-height: 0;
display: flex; display: flex;
align-items: flex-end;
border: 1px solid var(--border-hairline); border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-md); border-radius: var(--radius-md);
/* enough top padding to fully clear the label's own height, not just its border-overlap */ padding: 4cqh var(--space-3) 0;
padding: calc(1.2vh + var(--space-3)) var(--space-3) 0;
margin-bottom: var(--space-1); margin-bottom: var(--space-1);
margin-top: var(--space-1); margin-top: var(--space-1);
box-sizing: border-box; box-sizing: border-box;
@ -19,14 +18,14 @@
.channel-group__label { .channel-group__label {
position: absolute; position: absolute;
top: var(--space-2); /* sits inside the padded space, not straddling the border */ top: 1cqh;
left: 50%; left: 50%;
transform: translateX(-50%); /* only horizontal centering now — no vertical overlap */ transform: translateX(-50%);
padding: 0 var(--space-2); padding: 0 var(--space-2);
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: var(--tab-close-font); font-size: 2cqh;
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
color: var(--text-muted); color: var(--text-muted);
@ -39,5 +38,5 @@
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
height: 100%; height: 100%;
gap: var(--space-2); gap: var(--space-1);
} }

View File

@ -1,38 +1,3 @@
.gain-section {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: var(--dsp-control-heigth);
display: flex;
align-items: flex-end;
justify-content: center;
box-sizing: border-box;
padding: 0 var(--space-4);
background: var(--bg-panel);
z-index: 20;
border-top: var(--border-width) solid var(--border-hairline);
}
.gain-panels {
height: 100%;
display: flex;
gap: var(--space-4);
overflow-x: auto;
}
.gain-panel-frame {
flex: 0 0 auto;
height: calc(100% - 2 * var(--space-1));
margin: var(--space-1) 0;
}
.gain-panel-viewport { .gain-panel-viewport {
position: relative; position: relative;
aspect-ratio: 90 / 400; aspect-ratio: 90 / 400;
@ -56,10 +21,11 @@
box-sizing: border-box; box-sizing: border-box;
gap: var(--space-2); gap: var(--space-2);
background: var(--bg-void); background: var(--bg-void);
padding-bottom: var(--space-2);
} }
.gain-panel__title { .gain-panel__title {
flex: 0 0 auto; /* NEW: never let this shrink */ flex: 0 0 auto;
width: 100%; width: 100%;
text-align: center; text-align: center;
font-family: var(--font-ui); font-family: var(--font-ui);
@ -70,8 +36,34 @@
border-bottom: var(--border-width) solid var(--border-hairline); border-bottom: var(--border-width) solid var(--border-hairline);
} }
.channel-group__label { /* Container */
font-size: calc(var(--tab-close-font) * var(--panel-scale, 1));
.gain-section {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: var(--dsp-control-heigth);
display: flex;
align-items: flex-end;
justify-content: center;
box-sizing: border-box;
background: var(--bg-panel);
z-index: 20;
border-top: var(--border-width) solid var(--border-hairline);
}
.gain-panels {
height: 100%;
display: flex;
gap: var(--space-2);
overflow-x: auto;
} }
/* --- graph strip, sits above the gain section --- /* --- graph strip, sits above the gain section ---

View File

@ -10,29 +10,6 @@ import './GainSection.css';
const PANEL_DESIGN_WIDTH = 90; const PANEL_DESIGN_WIDTH = 90;
const PANEL_DESIGN_HEIGHT = 400; const PANEL_DESIGN_HEIGHT = 400;
// function ChannelGroupScaled({ title, children }: { title: string; children: React.ReactNode }) {
// const wrapperRef = useRef<HTMLDivElement>(null);
// const [scale, setScale] = useState(1);
// useEffect(() => {
// const el = wrapperRef.current;
// if (!el) return;
// const observer = new ResizeObserver((entries) => {
// const { height } = entries[0].contentRect;
// if (height === 0) return;
// setScale(height / PANEL_DESIGN_HEIGHT); // height is the only free axis here
// });
// observer.observe(el);
// return () => observer.disconnect();
// }, []);
// return (
// <div ref={wrapperRef} style={{ height: '100%', '--panel-scale': scale } as React.CSSProperties}>
// <ChannelGroup title={title}>{children}</ChannelGroup>
// </div>
// );
// }
function GainPanel({ function GainPanel({
dsp, dsp,
channel, channel,
@ -101,16 +78,17 @@ function GainPanel({
unit=" dB" unit=" dB"
value={gain} value={gain}
onChange={(value) => setGain(channel, value)} onChange={(value) => setGain(channel, value)}
style={{ width: 70, height: 260, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink style={{ width: 70, height: 250, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink
/> />
<Button variant="toggle" className={muted ? 'active-danger' : ''} onClick={toggleMute}> <Button variant="toggle" className={muted ? 'active-danger' : ''} onClick={toggleMute} style={{ width: '70px', flex: '1 1 auto', minHeight: 0 }}>
{muted ? 'Muted' : 'Mute'} {muted ? 'Muted' : 'Mute '}
</Button> </Button>
<Button <Button
variant="toggle" variant="toggle"
className={inverted ? 'active-warning' : ''} className={inverted ? 'active-warning' : ''}
onClick={toggleInverse} onClick={toggleInverse}
style={{ width: '70px', flex: '1 1 auto', minHeight: 0 }}
> >
{inverted ? 'Inverted' : 'Invert'} {inverted ? 'Inverted' : 'Invert'}
</Button> </Button>

View File

@ -1,35 +1,31 @@
// import VerticalSlider from '../Slider/VerticalSlider'; import VerticalSlider from '../Slider/VerticalSlider';
// import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
// import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
// import { DSP } from '../../../types/types'; import { DSP } from '../../../types/types';
// import { NotificationType } from '../../../types/types'; import { NotificationType } from '../../../types/types';
// import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../ChannelGroup/ChannelGroup';
// import './GateSection.css'; import './GateSection.css';
// function GateSection({ function GateSection({
// dsp, dsp,
// notify, }: {
// }: { dsp: DSP;
// dsp: DSP; }) {
// notify: (message: string, type?: NotificationType, dspName?: string) => void; return (
// }) { <div className="rerere">
// console.log(dsp, notify); <VerticalSlider
// return ( min={-60}
// <div className="rerere"> max={12}
// <VerticalSlider step={0.5}
// min={-60} switchStepValue={-10}
// max={12} switchStep={0.1}
// step={0.5} unit=" dB"
// switchStepValue={-10} value={0}
// switchStep={0.1} style={{ width: 70, height: 250, flex: '0 0 auto' }}
// unit=" dB" />
// onChange={(_f: number) => { </div>
// return; );
// }} }
// />
// </div>
// );
// }
// export default GateSection; export default GateSection;

View File

@ -70,10 +70,6 @@
border-bottom: var(--border-width) solid var(--border-hairline); border-bottom: var(--border-width) solid var(--border-hairline);
} }
.channel-group__label {
font-size: calc(var(--tab-close-font) * var(--panel-scale, 1));
}
/* --- graph strip, sits above the gain section --- /* --- graph strip, sits above the gain section ---
Requires the shared parent (whatever renders <GainSection />) to be Requires the shared parent (whatever renders <GainSection />) to be
position: relative — it already must be, for .gain-section's absolute position: relative — it already must be, for .gain-section's absolute

View File

@ -1,27 +1,29 @@
.gauge-viewport { .gauge-viewport {
position: relative; container-type: size;
container-name: gauge;
position: relative;
flex: 1 1 0; flex: 1 1 0;
min-height: 0; min-height: 0;
width: auto; width: auto;
aspect-ratio: 70 / 260;
overflow: hidden; overflow: hidden;
} }
.gauge-container { .gauge-container {
--s: min(calc(100cqw / 70), calc(100cqh / 240));
position: absolute; position: absolute;
top: 50%; bottom: 0;
left: 50%; left: 50%;
width: 70px; width: calc(70 * var(--s));
height: 240px; height: calc(240 * var(--s));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
transform-origin: center center; transform: translateX(-50%);
box-sizing: border-box; box-sizing: border-box;
} }
@ -29,130 +31,100 @@
.gauge-track { .gauge-track {
position: relative; position: relative;
width: 70px; width: calc(70 * var(--s));
height: 210px; height: calc(210 * var(--s));
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-bottom: 8px; margin-bottom: calc(8 * var(--s));
} }
.slider { .slider {
position: relative; position: relative;
width: 24px; width: calc(24 * var(--s));
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
touch-action: none; touch-action: none;
user-select: none; user-select: none;
} }
.track { .track {
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 0; top: 0;
transform: translateX(-50%); transform: translateX(-50%);
width: 2px; width: calc(2 * var(--s));
height: 100%; height: 100%;
background: var(--border-hairline); background: var(--border-hairline);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.thumb { .thumb {
position: absolute; position: absolute;
left: 50%; left: 50%;
width: 18px; width: calc(18 * var(--s));
aspect-ratio: 1; aspect-ratio: 1;
transform: translate(-50%, 50%); transform: translate(-50%, 50%);
border-radius: 50%; border-radius: 50%;
background: var(--bg-panel); background: var(--bg-panel);
border: var(--border-width-active) solid var(--accent-brand); border: var(--border-width-active) solid var(--accent-brand);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.ticks { .ticks {
position: absolute; position: absolute;
top: 0; top: 0;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
pointer-events: none; pointer-events: none;
} }
.ticks span { .ticks span {
width: 10px; width: calc(10 * var(--s));
height: calc(2 * var(--s));
height: 2px;
background: var(--border-hairline); background: var(--border-hairline);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.left { .left { left: calc(8 * var(--s)); }
left: 8px; .right { right: calc(8 * var(--s)); }
}
.right {
right: 8px;
}
.value { .value {
width: 100%; width: 100%;
margin-top: 4px; margin-top: calc(4 * var(--s));
padding: calc(2 * var(--s)) calc(4 * var(--s));
padding-top: 2px;
padding-bottom: 2px;
padding-right: 4px;
padding-left: 4px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 0.25em; gap: 0.25em;
white-space: nowrap; white-space: nowrap;
color: var(--accent-brand); color: var(--accent-brand);
font-size: var(--font-xxs); font-size: var(--font-xxs);
font-family: var(--font-mono); font-family: var(--font-mono);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
@ -161,15 +133,12 @@
.value input { .value input {
width: auto; width: auto;
min-width: 0; min-width: 0;
height: 1.4em; height: 1.4em;
text-align: center; text-align: center;
background: transparent; background: transparent;
border: var(--border-width) solid var(--border-hairline); border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
appearance: none; appearance: none;
@ -178,10 +147,7 @@
.value input, .value input,
.value-unit { .value-unit {
font-size: inherit; font-size: inherit;
line-height: 1; line-height: 1;
font-family: inherit; font-family: inherit;
color: inherit; color: inherit;
} }

View File

@ -1,10 +1,7 @@
import { useMemo, useRef, useState, useEffect } from 'react'; import { useMemo, useRef, useState } from 'react';
import './VerticalSlider.css'; import './VerticalSlider.css';
const DESIGN_WIDTH = 70;
const DESIGN_HEIGHT = 240;
type VerticalSliderProps = { type VerticalSliderProps = {
min: number; min: number;
max: number; max: number;
@ -50,32 +47,6 @@ export default function VerticalSlider({
const thumbRef = useRef<HTMLDivElement>(null); const thumbRef = useRef<HTMLDivElement>(null);
const keyboardEditing = useRef(false); const keyboardEditing = useRef(false);
const viewportRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
useEffect(() => {
const el = viewportRef.current;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
const { width, height } = entry.contentRect;
if (width === 0 || height === 0) return;
setScale(Math.min(width / DESIGN_WIDTH, height / DESIGN_HEIGHT));
});
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (dragging.current) return;
if (keyboardEditing.current) return;
setDisplayValue(value);
dragValue.current = value;
}, [value]);
function getStep(value: number) { function getStep(value: number) {
if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) { if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) {
return switchStep; return switchStep;
@ -218,11 +189,9 @@ export default function VerticalSlider({
<div <div
className={['gauge-viewport', className].filter(Boolean).join(' ')} className={['gauge-viewport', className].filter(Boolean).join(' ')}
style={style} style={style}
ref={viewportRef}
> >
<div <div
className="gauge-container" className="gauge-container"
style={{ transform: `translate(-50%, -50%) scale(${scale})` }}
> >
<div className="gauge-track"> <div className="gauge-track">
<div className="ticks left"> <div className="ticks left">

View File

@ -6,7 +6,7 @@ import ConnectionPanel from '../../components/dsp408/ConnectionSection/Connectio
import GainSection from '../../components/dsp408/GainSection/GainSection'; import GainSection from '../../components/dsp408/GainSection/GainSection';
import { DSP408Device } from '../../hooks/useDSP408'; import { DSP408Device } from '../../hooks/useDSP408';
import './DspPage.css'; import './DspPage.css';
// import GateSection from '../../components/dsp408/GateSection/GateSection'; import GateSection from '../../components/dsp408/GateSection/GateSection';
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay'; import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
import { InputChannel, OutputChannel } from '../../types/dsp408State'; import { InputChannel, OutputChannel } from '../../types/dsp408State';
@ -147,8 +147,6 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
})), })),
]; ];
// const { recallPreset, setCurrentPresetName } = useDSPPresets(dsp, setDsps, notify);
const onAppChange = async (id: string) => { const onAppChange = async (id: string) => {
const presetIndex = presets.findIndex((preset) => preset.id === id); const presetIndex = presets.findIndex((preset) => preset.id === id);
if (presetIndex === -1) return; if (presetIndex === -1) return;
@ -178,8 +176,8 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
/> />
); );
// case 'gate': case 'gate':
// return <GateSection dsp={dsp} notify={notify} />; return <GateSection dsp={dsp} />;
default: default:
return null; return null;