From a157b5f1987640f941b1e371a2564550c3dff3ab Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Fri, 24 Jul 2026 22:25:50 +0200 Subject: [PATCH] feat: responsive connection section --- src/{styles => }/App.css | 0 src/App.tsx | 2 +- src/components/Led/Led.tsx | 4 +- .../ConnectionSection/ConnectionSection.css | 109 ++++++++++++------ .../ConnectionSection/ConnectionSection.tsx | 103 ++++++++--------- .../dsp408/GainSection/GainSection.tsx | 48 ++++---- .../{ => dsp408}/Slider/VerticalSlider.css | 54 +++++---- .../{ => dsp408}/Slider/VerticalSlider.tsx | 106 +++++++++-------- src/components/topbar/Topbar.tsx | 2 +- src/{styles => }/variables.css | 0 10 files changed, 245 insertions(+), 183 deletions(-) rename src/{styles => }/App.css (100%) rename src/components/{ => dsp408}/Slider/VerticalSlider.css (69%) rename src/components/{ => dsp408}/Slider/VerticalSlider.tsx (67%) rename src/{styles => }/variables.css (100%) diff --git a/src/styles/App.css b/src/App.css similarity index 100% rename from src/styles/App.css rename to src/App.css diff --git a/src/App.tsx b/src/App.tsx index 36bd543..791ee9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { invoke } from '@tauri-apps/api/core'; import { useState } from 'react'; -import './styles/App.css'; +import './App.css'; import { DSP, DSPStatus, AppPage } from './types/types'; import TopBar from './components/Topbar/Topbar'; import DSPPage from './pages/DspPage/DspPage'; diff --git a/src/components/Led/Led.tsx b/src/components/Led/Led.tsx index c3e6b81..13d1f28 100644 --- a/src/components/Led/Led.tsx +++ b/src/components/Led/Led.tsx @@ -5,10 +5,12 @@ type LedProps = { className?: string; }; -export function Led({ status = 'disconnected', className = '' }: LedProps) { +function Led({ status = 'disconnected', className = '' }: LedProps) { return ( ); } + +export default Led; diff --git a/src/components/dsp408/ConnectionSection/ConnectionSection.css b/src/components/dsp408/ConnectionSection/ConnectionSection.css index cde4110..a888b51 100644 --- a/src/components/dsp408/ConnectionSection/ConnectionSection.css +++ b/src/components/dsp408/ConnectionSection/ConnectionSection.css @@ -1,64 +1,107 @@ -/* .connection-panel-frame { - width: 33.333%; - height: 33.333%; - box-sizing: border-box; -} - -.panel { +.connection-section { width: 100%; height: 100%; + display: flex; - flex-direction: column; - box-sizing: border-box; - overflow: hidden; - position: relative; + justify-content: center; + align-items: center; } -.panel-type { +.connection-panel { + width: 340px; + display: flex; + flex-direction: column; +} + +.connection-panel__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-3); + min-width: 0; +} + +.connection-panel__header-text { + flex: 1; + min-width: 0; +} + +.connection-panel__title { + margin: 0; + + font-size: var(--font-lg); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.connection-panel__type { margin-top: var(--space-1); - font-size: 12px; + + font-size: var(--font-sm); color: var(--text-muted); font-family: var(--font-mono); letter-spacing: 0.03em; } -.panel-header { +.connection-panel__specs { + margin: var(--space-5) 0; + + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.connection-panel__spec { display: flex; - align-items: flex-start; justify-content: space-between; - gap: 12px; - min-width: 0; + align-items: center; + gap: var(--space-3); } -.panel-header-text { - min-width: 0; - flex: 1 1 auto; +.connection-panel__spec dt { + color: var(--text-muted); + font-size: var(--font-sm); } -.panel-title { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; +.connection-panel__spec dd { margin: 0; - font-size: 18px; - font-weight: var(--font-weight-semibold); + color: var(--text-primary); } -.mono { +.connection-panel__mono { + font-family: var(--font-mono); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.specs { +.connection-panel__actions { + margin-top: auto; + + display: flex; + justify-content: flex-end; +} + +.connection-panel__status { + display: flex; + align-items: center; + gap: var(--space-2); + flex-shrink: 0; } -.connection-section { - position: absolute; - inset: 0; +.connection-panel__led { + height: 14px; + aspect-ratio: 1 / 1; display: flex; - justify-content: center; align-items: center; -} */ + justify-content: center; + + flex: 0 0 auto; +} diff --git a/src/components/dsp408/ConnectionSection/ConnectionSection.tsx b/src/components/dsp408/ConnectionSection/ConnectionSection.tsx index 6ec2825..ef1b689 100644 --- a/src/components/dsp408/ConnectionSection/ConnectionSection.tsx +++ b/src/components/dsp408/ConnectionSection/ConnectionSection.tsx @@ -1,14 +1,10 @@ -// ConnectionPanel.tsx import { DSP } from '../../../types/types'; import Button from '../../Button/Button'; +import Card from '../../Card/Card'; +import Led from '../../Led/Led'; + import './ConnectionSection.css'; -const DESIGN_WIDTH = 340; -const DESIGN_HEIGHT = 280; - -const BTN_W = 100; -const BTN_H = 40; - function ConnectionPanel({ dsp, onConnect, @@ -27,59 +23,58 @@ function ConnectionPanel({ return (
-
-
-
-
-

- {dsp.name} -

-
{dsp.type}
-
+ +
+
+

+ {dsp.name} +

-
- - {statusLabel} -
+
{dsp.type}
-
-
-
IP address
-
- {dsp.ip} -
-
+
+ + + -
-
Port
-
{dsp.port}
-
- -
-
Device ID
-
- {dsp.deviceId || '—'} -
-
-
- -
- {dsp.status === 'connected' ? ( - - ) : ( - - )} + {statusLabel}
-
+ +
+
+
IP address
+
+ {dsp.ip} +
+
+ +
+
Port
+
{dsp.port}
+
+ +
+
Device ID
+
+ {dsp.deviceId || '—'} +
+
+
+ +
+ {dsp.status === 'connected' ? ( + + ) : ( + + )} +
+
); } diff --git a/src/components/dsp408/GainSection/GainSection.tsx b/src/components/dsp408/GainSection/GainSection.tsx index faa10db..b2da21d 100644 --- a/src/components/dsp408/GainSection/GainSection.tsx +++ b/src/components/dsp408/GainSection/GainSection.tsx @@ -1,4 +1,4 @@ -import VerticalSlider from '../../Slider/VerticalSlider'; +import VerticalSlider from '../Slider/VerticalSlider'; import { invoke } from '@tauri-apps/api/core'; import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; import { DSP } from '../../../types/types'; @@ -93,33 +93,31 @@ function GainPanel({ return (
- -
-
{title}
+
+
{title}
- + - + - -
- + +
); } diff --git a/src/components/Slider/VerticalSlider.css b/src/components/dsp408/Slider/VerticalSlider.css similarity index 69% rename from src/components/Slider/VerticalSlider.css rename to src/components/dsp408/Slider/VerticalSlider.css index 9177cf0..19a7e50 100644 --- a/src/components/Slider/VerticalSlider.css +++ b/src/components/dsp408/Slider/VerticalSlider.css @@ -1,9 +1,19 @@ -.gauge-container { - width: 70px; - height: 260px; +.gauge-viewport { + container-type: inline-size; + container-name: gauge; - padding-top: 20px; - padding-bottom: 10px; + width: 100%; + aspect-ratio: 70 / 260; + + box-sizing: border-box; +} + +.gauge-container { + width: 100%; + height: 100%; + + padding-top: 28.571cqw; /* 20px */ + padding-bottom: 14.286cqw; /* 10px */ display: flex; flex-direction: column; @@ -17,21 +27,23 @@ .gauge-track { position: relative; - width: 70px; - height: 210px; + width: 100%; + height: 300cqw; /* 210px */ display: flex; justify-content: center; align-items: center; - margin-bottom: 8px; + margin-bottom: 11.429cqw; /* 8px */ + + box-sizing: border-box; } .slider { position: relative; - width: 24px; + width: 34.286cqw; /* 24px */ height: 100%; cursor: pointer; @@ -48,7 +60,7 @@ transform: translateX(-50%); - width: 2px; + width: 2.857cqw; /* 2px */ height: 100%; @@ -62,7 +74,7 @@ left: 50%; - width: 18px; + width: 25.714cqw; /* 18px */ aspect-ratio: 1; @@ -94,9 +106,9 @@ } .ticks span { - width: 10px; + width: 14.286cqw; /* 10px */ - height: 2px; + height: 2.857cqw; /* 2px */ background: var(--border-hairline); @@ -104,22 +116,22 @@ } .left { - left: 8px; + left: 11.429cqw; /* 8px */ } .right { - right: 8px; + right: 11.429cqw; /* 8px */ } .value { width: 100%; - margin-top: 4px; + margin-top: 5.714cqw; /* 4px */ - padding-top: 2px; - padding-bottom: 2px; - padding-right: 4px; - padding-left: 4px; + padding-top: 2.857cqw; /* 2px */ + padding-bottom: 2.857cqw; /* 2px */ + padding-right: 5.714cqw; /* 4px */ + padding-left: 5.714cqw; /* 4px */ box-sizing: border-box; @@ -134,7 +146,7 @@ color: var(--accent-brand); - font-size: 16px; + font-size: 22.857cqw; font-family: var(--font-ui); diff --git a/src/components/Slider/VerticalSlider.tsx b/src/components/dsp408/Slider/VerticalSlider.tsx similarity index 67% rename from src/components/Slider/VerticalSlider.tsx rename to src/components/dsp408/Slider/VerticalSlider.tsx index 5caf54e..6bb70e4 100644 --- a/src/components/Slider/VerticalSlider.tsx +++ b/src/components/dsp408/Slider/VerticalSlider.tsx @@ -2,7 +2,10 @@ import { useMemo, useRef, useState, useEffect } from 'react'; import './VerticalSlider.css'; -// Design size 260*70 +// Design size 260*70 (height*width) — kept as the reference ratio. +// All CSS dimensions are expressed in container-query width units (cqw) +// relative to this DESIGN_WIDTH, so the whole component scales together +// and preserves this aspect ratio at any rendered size. const DESIGN_WIDTH = 70; @@ -79,6 +82,9 @@ export default function VerticalSlider({ return Math.max(minStr.length, maxStr.length) + unit.length; }, [min, max, decimals, unit]); + // Computed in "design pixels" (i.e. assuming the component is rendered at + // DESIGN_WIDTH px wide), then converted below to cqw so it scales with + // the actual rendered size instead of staying a fixed pixel value. const valueFontSize = useMemo(() => { const availableWidth = DESIGN_WIDTH - 8; const AVG_CHAR_WIDTH_RATIO = 0.6; @@ -86,6 +92,10 @@ export default function VerticalSlider({ return Math.min(16, Math.max(8, sizeByWidth)); }, [maxChars]); + // Convert the "design px" font size into cqw units relative to the + // component's own width, so it scales proportionally with everything else. + const valueFontSizeCqw = useMemo(() => (valueFontSize / DESIGN_WIDTH) * 100, [valueFontSize]); + function normalizeValue(input: number) { const clamped = Math.min(max, Math.max(min, input)); const snapped = Math.round(clamped / currentStep) * currentStep; @@ -192,55 +202,57 @@ export default function VerticalSlider({ const percent = (value - min) / (max - min); return ( -
-
-
- {Array.from({ length: 15 }).map((_, i) => ( - - ))} +
+
+
+
+ {Array.from({ length: 15 }).map((_, i) => ( + + ))} +
+ +
+
+
+
+ +
+ {Array.from({ length: 15 }).map((_, i) => ( + + ))} +
-
-
-
+
+ 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(); + } + }} + /> + {unit}
- -
- {Array.from({ length: 15 }).map((_, i) => ( - - ))} -
-
- -
- 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(); - } - }} - /> - {unit}
); diff --git a/src/components/topbar/Topbar.tsx b/src/components/topbar/Topbar.tsx index b989fbd..220ffae 100644 --- a/src/components/topbar/Topbar.tsx +++ b/src/components/topbar/Topbar.tsx @@ -3,7 +3,7 @@ import { DSP, AppPage } from '../../types/types'; import AnimatedLogo from '../../assets/icons/AnimatedLogo'; import { PlusIcon } from '../../assets/icons/PlusIcon'; import { CrownIcon } from '../../assets/icons/CrownIcon'; -import { Led } from '../Led/Led'; +import Led from '../Led/Led'; import { CloseIcon } from '../../assets/icons/CloseIcon'; import './Topbar.css'; diff --git a/src/styles/variables.css b/src/variables.css similarity index 100% rename from src/styles/variables.css rename to src/variables.css