feat: fully responsive gain tab
This commit is contained in:
22
src/components/AutoScale/AutoScale.css
Normal file
22
src/components/AutoScale/AutoScale.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.autoscale-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoscale-box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoscale-content {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
78
src/components/AutoScale/AutoScale.tsx
Normal file
78
src/components/AutoScale/AutoScale.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
type AutoScaleProps = {
|
||||||
|
designWidth: number;
|
||||||
|
designHeight: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AutoScale({
|
||||||
|
designWidth,
|
||||||
|
designHeight,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: AutoScaleProps) {
|
||||||
|
const outerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = outerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(([entry]) => {
|
||||||
|
const { width, height } = entry.contentRect;
|
||||||
|
|
||||||
|
if (!width || !height) return;
|
||||||
|
|
||||||
|
const nextScale = Math.min(width / designWidth, height / designHeight);
|
||||||
|
|
||||||
|
setScale(nextScale > 0 ? nextScale : 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
ro.observe(el);
|
||||||
|
|
||||||
|
return () => ro.disconnect();
|
||||||
|
}, [designWidth, designHeight]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={outerRef}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
minWidth: 0,
|
||||||
|
minHeight: 0,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
width: designWidth * scale,
|
||||||
|
height: designHeight * scale,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: designWidth,
|
||||||
|
height: designHeight,
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
transformOrigin: 'top left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,12 +1,42 @@
|
|||||||
|
// Button.tsx
|
||||||
import './Button.css';
|
import './Button.css';
|
||||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import AutoScale from '../AutoScale/AutoScale';
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'ghost' | 'toggle' | 'outline';
|
type ButtonVariant = 'primary' | 'ghost' | 'toggle' | 'outline';
|
||||||
|
|
||||||
|
// Design-space size per variant (same idea as DESIGN_WIDTH/HEIGHT in the slider)
|
||||||
|
const DEFAULT_DESIGN_SIZE: Record<ButtonVariant, { width: number; height: number }> = {
|
||||||
|
primary: { width: 90, height: 36 },
|
||||||
|
ghost: { width: 90, height: 36 },
|
||||||
|
toggle: { width: 90, height: 40 },
|
||||||
|
outline: { width: 90, height: 36 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_FONT_SIZE = 8;
|
||||||
|
const MAX_FONT_SIZE = 16;
|
||||||
|
const AVG_CHAR_WIDTH_RATIO = 0.6;
|
||||||
|
const HORIZONTAL_PADDING = 12; // reserved px inside the box, keeps text off the edges
|
||||||
|
|
||||||
|
function getTextLength(children: ReactNode): number {
|
||||||
|
if (typeof children === 'string' || typeof children === 'number') {
|
||||||
|
return String(children).length;
|
||||||
|
}
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
return children.reduce((acc: number, c) => acc + getTextLength(c), 0);
|
||||||
|
}
|
||||||
|
return 6; // icons / non-text children: reasonable fallback estimate
|
||||||
|
}
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
fontSize?: number; // explicit override, in design px — skips auto-calc
|
||||||
|
designWidth?: number; // override default design size if needed
|
||||||
|
designHeight?: number;
|
||||||
|
scale?: boolean; // wrap in AutoScale (default true)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Button({
|
export default function Button({
|
||||||
@ -16,17 +46,47 @@ export default function Button({
|
|||||||
disabled,
|
disabled,
|
||||||
onClick,
|
onClick,
|
||||||
type = 'button',
|
type = 'button',
|
||||||
|
fontSize,
|
||||||
|
designWidth,
|
||||||
|
designHeight,
|
||||||
|
scale = true,
|
||||||
|
style,
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
return (
|
const { width: defaultW, height: defaultH } = DEFAULT_DESIGN_SIZE[variant];
|
||||||
|
const W = designWidth ?? defaultW;
|
||||||
|
const H = designHeight ?? defaultH;
|
||||||
|
|
||||||
|
const computedFontSize = useMemo(() => {
|
||||||
|
if (fontSize !== undefined) return fontSize; // user overload wins, no calc
|
||||||
|
|
||||||
|
const textLength = getTextLength(children);
|
||||||
|
if (textLength === 0) return MAX_FONT_SIZE;
|
||||||
|
|
||||||
|
const availableWidth = W - HORIZONTAL_PADDING;
|
||||||
|
const sizeByWidth = availableWidth / (textLength * AVG_CHAR_WIDTH_RATIO);
|
||||||
|
|
||||||
|
return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, sizeByWidth));
|
||||||
|
}, [fontSize, children, W]);
|
||||||
|
|
||||||
|
const button = (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
className={`btn btn-${variant} ${className}`}
|
className={`btn btn-${variant} ${className}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
style={{ width: '100%', height: '100%', fontSize: `${computedFontSize}px`, ...style }}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!scale) return button;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoScale designWidth={W} designHeight={H}>
|
||||||
|
{button}
|
||||||
|
</AutoScale>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import './Sidebar.css';
|
import './Sidebar.css';
|
||||||
import { ChevronLeftIcon } from '../../assets/icons/ChevronLeftIcon';
|
import { ChevronLeftIcon } from '../../assets/icons/ChevronLeftIcon';
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,3 @@
|
|||||||
.gauge-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gauge-container {
|
.gauge-container {
|
||||||
width: 70px;
|
width: 70px;
|
||||||
height: 260px;
|
height: 260px;
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import { useMemo, useRef, useState, useEffect } from 'react';
|
|||||||
|
|
||||||
import './VerticalSlider.css';
|
import './VerticalSlider.css';
|
||||||
|
|
||||||
|
// Design size 260*70
|
||||||
|
|
||||||
|
const DESIGN_WIDTH = 70;
|
||||||
|
|
||||||
type VerticalSliderProps = {
|
type VerticalSliderProps = {
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
@ -19,9 +23,6 @@ type VerticalSliderProps = {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DESIGN_WIDTH = 70;
|
|
||||||
const DESIGN_HEIGHT = 260;
|
|
||||||
|
|
||||||
function getDecimals(n: number) {
|
function getDecimals(n: number) {
|
||||||
const s = n.toString();
|
const s = n.toString();
|
||||||
const i = s.indexOf('.');
|
const i = s.indexOf('.');
|
||||||
@ -53,29 +54,11 @@ export default function VerticalSlider({
|
|||||||
const rafRef = useRef<number | null>(null);
|
const rafRef = useRef<number | null>(null);
|
||||||
const dragging = useRef(false);
|
const dragging = useRef(false);
|
||||||
const thumbRef = useRef<HTMLDivElement>(null);
|
const thumbRef = useRef<HTMLDivElement>(null);
|
||||||
|
const keyboardEditing = useRef(false);
|
||||||
// --- NEW: auto-fit scale ---
|
|
||||||
const outerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [scale, setScale] = useState(1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = outerRef.current;
|
if (keyboardEditing.current) return;
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries) => {
|
|
||||||
const { width, height } = entries[0].contentRect;
|
|
||||||
if (width === 0 || height === 0) return;
|
|
||||||
|
|
||||||
const s = Math.min(width / DESIGN_WIDTH, height / DESIGN_HEIGHT);
|
|
||||||
setScale(s > 0 ? s : 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
ro.observe(el);
|
|
||||||
return () => ro.disconnect();
|
|
||||||
}, []);
|
|
||||||
// ---------------------------
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDisplayValue(value);
|
setDisplayValue(value);
|
||||||
dragValue.current = value;
|
dragValue.current = value;
|
||||||
}, [value]);
|
}, [value]);
|
||||||
@ -129,21 +112,40 @@ export default function VerticalSlider({
|
|||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return;
|
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return;
|
||||||
|
|
||||||
|
keyboardEditing.current = true;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let next = value;
|
const base = keyboardValue.current ?? value;
|
||||||
|
|
||||||
|
let next = base;
|
||||||
|
|
||||||
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') next += currentStep;
|
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') next += currentStep;
|
||||||
|
|
||||||
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') next -= currentStep;
|
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') next -= currentStep;
|
||||||
|
|
||||||
updateValue(next);
|
next = normalizeValue(next);
|
||||||
keyboardValue.current = normalizeValue(next);
|
|
||||||
|
keyboardValue.current = next;
|
||||||
|
|
||||||
|
updateValue(next); // updates displayValue only
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp() {
|
function handleKeyUp() {
|
||||||
if (keyboardValue.current !== null) {
|
if (keyboardValue.current == null) return;
|
||||||
onChange?.(keyboardValue.current);
|
|
||||||
keyboardValue.current = null;
|
keyboardEditing.current = false;
|
||||||
|
|
||||||
|
const next = keyboardValue.current;
|
||||||
|
|
||||||
|
if (controlledValue === undefined) {
|
||||||
|
setInternalValue(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChange?.(next);
|
||||||
|
|
||||||
|
keyboardValue.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function valueFromPointer(clientY: number) {
|
function valueFromPointer(clientY: number) {
|
||||||
@ -190,78 +192,56 @@ export default function VerticalSlider({
|
|||||||
const percent = (value - min) / (max - min);
|
const percent = (value - min) / (max - min);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={['gauge-container', className].filter(Boolean).join(' ')} style={style}>
|
||||||
ref={outerRef}
|
<div className="gauge-track">
|
||||||
className="gauge-wrapper"
|
<div className="ticks left">
|
||||||
style={{ width: '100%', height: '100%', ...style }}
|
{Array.from({ length: 15 }).map((_, i) => (
|
||||||
>
|
<span key={i} />
|
||||||
<div
|
))}
|
||||||
className="gauge-scale-box"
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
width: DESIGN_WIDTH * scale,
|
|
||||||
height: DESIGN_HEIGHT * scale,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={['gauge-container', className].filter(Boolean).join(' ')}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: DESIGN_WIDTH,
|
|
||||||
height: DESIGN_HEIGHT,
|
|
||||||
transform: `scale(${scale})`,
|
|
||||||
transformOrigin: 'top left',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="gauge-track">
|
|
||||||
<div className="ticks left">
|
|
||||||
{Array.from({ length: 15 }).map((_, i) => <span key={i} />)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="slider"
|
|
||||||
ref={sliderRef}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onPointerDown={startDrag}
|
|
||||||
onPointerMove={handlePointerMove}
|
|
||||||
onPointerUp={endDrag}
|
|
||||||
onPointerCancel={endDrag}
|
|
||||||
>
|
|
||||||
<div className="track" />
|
|
||||||
<div ref={thumbRef} className="thumb" style={{ bottom: `${percent * 100}%` }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ticks right">
|
|
||||||
{Array.from({ length: 15 }).map((_, i) => <span key={i} />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="value" style={{ fontSize: `${valueFontSize}px` }}>
|
<div
|
||||||
<input
|
className="slider"
|
||||||
ref={inputRef}
|
ref={sliderRef}
|
||||||
type="text"
|
tabIndex={0}
|
||||||
inputMode="decimal"
|
onKeyDown={handleKeyDown}
|
||||||
value={editingText ?? displayValue.toFixed(decimals)}
|
onKeyUp={handleKeyUp}
|
||||||
style={{ width: '100%' }}
|
onPointerDown={startDrag}
|
||||||
onFocus={() => setEditingText(value.toFixed(decimals))}
|
onPointerMove={handlePointerMove}
|
||||||
onChange={(e) => setEditingText(e.target.value)}
|
onPointerUp={endDrag}
|
||||||
onBlur={commitEditing}
|
onPointerCancel={endDrag}
|
||||||
onKeyDown={(e) => {
|
>
|
||||||
if (e.key === 'Enter') inputRef.current?.blur();
|
<div className="track" />
|
||||||
if (e.key === 'Escape') {
|
<div ref={thumbRef} className="thumb" style={{ bottom: `${percent * 100}%` }} />
|
||||||
setEditingText(null);
|
</div>
|
||||||
inputRef.current?.blur();
|
|
||||||
}
|
<div className="ticks right">
|
||||||
}}
|
{Array.from({ length: 15 }).map((_, i) => (
|
||||||
/>
|
<span key={i} />
|
||||||
<span className="value-unit">{unit}</span>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="value" style={{ fontSize: `${valueFontSize}px` }}>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={editingText ?? displayValue.toFixed(decimals)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
onFocus={() => setEditingText(value.toFixed(decimals))}
|
||||||
|
onChange={(e) => setEditingText(e.target.value)}
|
||||||
|
onBlur={commitEditing}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') inputRef.current?.blur();
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setEditingText(null);
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="value-unit">{unit}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@ -1,82 +0,0 @@
|
|||||||
.vertical-stack {
|
|
||||||
height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
border: 1px solid var(--border-hairline);
|
|
||||||
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* header */
|
|
||||||
|
|
||||||
.vertical-stack__title {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
padding: var(--space-1);
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
color: var(--text-primary);
|
|
||||||
|
|
||||||
border-bottom: 1px solid var(--border-hairline);
|
|
||||||
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* area containing components */
|
|
||||||
|
|
||||||
.vertical-stack__content {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* each component slot */
|
|
||||||
|
|
||||||
.vertical-stack__item {
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
The child keeps its natural dimensions.
|
|
||||||
Only horizontal scaling happens.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.vertical-stack__scale {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
// VerticalStack.tsx
|
|
||||||
import React from 'react';
|
|
||||||
import './VerticalStack.css';
|
|
||||||
|
|
||||||
type VerticalStackProps = {
|
|
||||||
title: string;
|
|
||||||
titleFontSize?: number;
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function VerticalStack({ title, children }: VerticalStackProps) {
|
|
||||||
return (
|
|
||||||
<div className="vertical-stack">
|
|
||||||
<div className="vertical-stack__title">
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="vertical-stack__content">
|
|
||||||
{React.Children.map(children, (child, index) => (
|
|
||||||
<div className="vertical-stack__item" key={index}>
|
|
||||||
{child}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
--gain-section-height: 40vh; /* your fixed % of screen height */
|
--gain-section-height: 50vh; /* your fixed % of screen height */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gain-section {
|
.gain-section {
|
||||||
@ -33,26 +33,22 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: safe center;
|
justify-content: safe center; /* centers when it fits, falls back to start-aligned scroll when it overflows */
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--border-hairline) transparent;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
.gain-panels > * {
|
|
||||||
height: 100%;
|
.gain-panel-frame {
|
||||||
aspect-ratio: 90 / 340; /* your panel's design ratio (title+slider+buttons) — tune */
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
min-width: 0;
|
height: calc(100% - 8px);
|
||||||
min-height: 0;
|
aspect-ratio: var(--panel-w) / var(--panel-h); /* now driven by JS, not duplicated */
|
||||||
|
margin: 4px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gain-panels::-webkit-scrollbar {
|
.gain-panels::-webkit-scrollbar {
|
||||||
@ -65,3 +61,29 @@
|
|||||||
.gain-panels::-webkit-scrollbar-track {
|
.gain-panels::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gain-panel {
|
||||||
|
/* width/height now set inline from DESIGN_WIDTH/HEIGHT, drop the fixed px here */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border: 1px solid var(--border-hairline);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gain-panel__title {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
padding-bottom: 6px;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--border-hairline);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import VerticalStack from '../../VerticalStack/VerticalStack';
|
|
||||||
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';
|
||||||
@ -6,8 +5,12 @@ import { DSP } from '../../../types/types';
|
|||||||
import { NotificationType } from '../../../types/types';
|
import { NotificationType } from '../../../types/types';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Button from '../../Button/Button';
|
import Button from '../../Button/Button';
|
||||||
|
import AutoScale from '../../AutoScale/AutoScale';
|
||||||
import './GainSection.css';
|
import './GainSection.css';
|
||||||
|
|
||||||
|
const DESIGN_WIDTH = 90;
|
||||||
|
const DESIGN_HEIGHT = 380;
|
||||||
|
|
||||||
function GainPanel({
|
function GainPanel({
|
||||||
dsp,
|
dsp,
|
||||||
notify,
|
notify,
|
||||||
@ -89,32 +92,43 @@ function GainPanel({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalStack title={title}>
|
<div
|
||||||
<VerticalSlider
|
className="gain-panel-frame"
|
||||||
min={-60}
|
style={
|
||||||
max={12}
|
{
|
||||||
step={0.5}
|
'--panel-w': DESIGN_WIDTH,
|
||||||
switchStepValue={-10}
|
'--panel-h': DESIGN_HEIGHT,
|
||||||
switchStep={0.1}
|
} as React.CSSProperties
|
||||||
unit=" dB"
|
}
|
||||||
onChange={setGain}
|
>
|
||||||
/>
|
<AutoScale designWidth={DESIGN_WIDTH} designHeight={DESIGN_HEIGHT}>
|
||||||
{/* <Button
|
<div className="gain-panel" style={{ width: DESIGN_WIDTH, height: DESIGN_HEIGHT }}>
|
||||||
variant="toggle"
|
<div className="gain-panel__title">{title}</div>
|
||||||
className={muted ? "active-danger" : ""}
|
|
||||||
onClick={toggleMute}
|
|
||||||
>
|
|
||||||
{muted ? "Muted" : "Mute"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<VerticalSlider
|
||||||
variant="toggle"
|
min={-60}
|
||||||
className={inverted ? "active-warning" : ""}
|
max={12}
|
||||||
onClick={toggleInverse}
|
step={0.5}
|
||||||
>
|
switchStepValue={-10}
|
||||||
{inverted ? "Inverted" : "Invert"}
|
switchStep={0.1}
|
||||||
</Button> */}
|
unit=" dB"
|
||||||
</VerticalStack>
|
onChange={setGain}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="toggle" className={muted ? 'active-danger' : ''} onClick={toggleMute}>
|
||||||
|
{muted ? 'Muted' : 'Mute'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="toggle"
|
||||||
|
className={inverted ? 'active-warning' : ''}
|
||||||
|
onClick={toggleInverse}
|
||||||
|
>
|
||||||
|
{inverted ? 'Inverted' : 'Invert'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AutoScale>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,14 +145,14 @@ function GainSection({
|
|||||||
{ channel: { Input: InputChannel.InC }, title: 'In C' },
|
{ channel: { Input: InputChannel.InC }, title: 'In C' },
|
||||||
{ channel: { Input: InputChannel.InD }, title: 'In D' },
|
{ channel: { Input: InputChannel.InD }, title: 'In D' },
|
||||||
|
|
||||||
{ channel: { Output: OutputChannel.Out1 }, title: "Out 1" },
|
{ channel: { Output: OutputChannel.Out1 }, title: 'Out 1' },
|
||||||
{ channel: { Output: OutputChannel.Out2 }, title: "Out 2" },
|
{ channel: { Output: OutputChannel.Out2 }, title: 'Out 2' },
|
||||||
{ channel: { Output: OutputChannel.Out3 }, title: "Out 3" },
|
{ channel: { Output: OutputChannel.Out3 }, title: 'Out 3' },
|
||||||
{ channel: { Output: OutputChannel.Out4 }, title: "Out 4" },
|
{ channel: { Output: OutputChannel.Out4 }, title: 'Out 4' },
|
||||||
{ channel: { Output: OutputChannel.Out5 }, title: "Out 5" },
|
{ channel: { Output: OutputChannel.Out5 }, title: 'Out 5' },
|
||||||
{ channel: { Output: OutputChannel.Out6 }, title: "Out 6" },
|
{ channel: { Output: OutputChannel.Out6 }, title: 'Out 6' },
|
||||||
{ channel: { Output: OutputChannel.Out7 }, title: "Out 7" },
|
{ channel: { Output: OutputChannel.Out7 }, title: 'Out 7' },
|
||||||
{ channel: { Output: OutputChannel.Out8 }, title: "Out 8" },
|
{ channel: { Output: OutputChannel.Out8 }, title: 'Out 8' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -47,11 +47,7 @@ function DSPPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`dsp-layout app-root ${sidebarExpanded ? 'app-root--sidebar-expanded' : ''}`}>
|
||||||
className={`dsp-layout app-root ${
|
|
||||||
sidebarExpanded ? 'app-root--sidebar-expanded' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Sidebar
|
<Sidebar
|
||||||
items={sidebarItems}
|
items={sidebarItems}
|
||||||
activeId={activeSidebar}
|
activeId={activeSidebar}
|
||||||
|
|||||||
Reference in New Issue
Block a user