feat: responsive card pages

This commit is contained in:
2026-07-24 19:53:57 +02:00
parent e85616eacc
commit e79748aa90
19 changed files with 363 additions and 250 deletions

View File

@ -5,11 +5,11 @@ import './styles/App.css';
import { DSP, DSPStatus, AppPage } from './types/types';
import TopBar from './components/Topbar/Topbar';
import DSPPage from './pages/DspPage/DspPage';
import AddDSPForm from './pages/AddDspPage';
import AddDSPForm from './pages/AddDspPage/AddDspPage';
import { useNotifications } from './hooks/useNotifications';
import Notifications from './components/Notifications/Notifications';
import { DSPState } from './types/dsp408State';
import CreditsPage from './pages/CreditsPage';
import CreditsPage from './pages/CreditsPage/CreditsPage';
import HomePage from './pages/HomePage/HomePage';
function App() {

View File

@ -1,47 +1,36 @@
.btn {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-height: var(--card-control-height);
padding: 0 var(--space-4);
border: var(--border-width) solid transparent;
border-radius: var(--radius-sm);
font-family: var(--font-ui);
font-size: 13px;
font-weight: 500;
font-size: var(--font-md);
font-weight: var(--font-weight-medium);
cursor: pointer;
border: 1px solid transparent;
}
.btn:disabled {
opacity: 0.45;
opacity: var(--disabled-opacity);
cursor: not-allowed;
}
.btn-primary {
background: var(--accent-brand);
color: #0d0f12;
}
.btn-ghost {
background: transparent;
border-color: var(--border-hairline);
color: var(--text-muted);
}
.btn-toggle {
background: transparent;
border-color: var(--border-hairline);
color: var(--text-muted);
color: var(--bg-void);
}
.btn-ghost,
.btn-toggle,
.btn-outline {
background: transparent;
border-color: var(--border-hairline);

View File

@ -1,8 +1,13 @@
.card {
width: min(100%, var(--card-width));
background: var(--bg-panel);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
padding: var(--space-6);
color: var(--text-primary);
@ -12,5 +17,6 @@
display: flex;
flex-direction: column;
width: clamp(280px, 33.333%, 480px);
min-width: 0;
box-sizing: border-box;
}

View File

@ -132,7 +132,7 @@
align-items: center;
font-size: var(--font-size-sm, 13px);
font-weight: 600;
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
white-space: nowrap;

View File

@ -27,7 +27,7 @@
font-family: var(--font-ui);
font-size: var(--tab-close-font);
font-weight: 600;
font-weight: var(--font-weight-semibold);
color: var(--text-muted);
white-space: nowrap;

View File

@ -1,4 +1,4 @@
.connection-panel-frame {
/* .connection-panel-frame {
width: 33.333%;
height: 33.333%;
box-sizing: border-box;
@ -27,7 +27,7 @@
align-items: flex-start;
justify-content: space-between;
gap: 12px;
min-width: 0; /* required so children can actually shrink/ellipsize instead of forcing overflow */
min-width: 0;
}
.panel-header-text {
@ -41,7 +41,7 @@
text-overflow: ellipsis;
margin: 0;
font-size: 18px;
font-weight: 600;
font-weight: var(--font-weight-semibold);
}
.mono {
@ -54,19 +54,11 @@
flex-shrink: 0;
}
.panel-actions {
position: absolute;
right: 16px;
bottom: 16px;
margin: 0; /* drop margin-top: auto, no longer needed */
padding: 0; /* drop padding-top: 16px, spacing now handled by right/bottom offsets */
}
.connection-section {
position: absolute; /* was: fixed — relative to .dsp-content now */
inset: 0; /* replaces top:0; right:0; bottom:0; left:0 */
position: absolute;
inset: 0;
display: flex;
justify-content: center;
align-items: center;
}
} */

View File

@ -74,7 +74,7 @@
font-family: var(--font-ui);
font-size: 12px;
font-weight: 600;
font-weight: var(--font-weight-semibold);
padding-bottom: 6px;

View File

@ -92,15 +92,7 @@ function GainPanel({
};
return (
<div
className="gain-panel-frame"
style={
{
'--panel-w': DESIGN_WIDTH,
'--panel-h': DESIGN_HEIGHT,
} as React.CSSProperties
}
>
<div className="gain-panel-frame">
<AutoScale designWidth={DESIGN_WIDTH} designHeight={DESIGN_HEIGHT}>
<div className="gain-panel" style={{ width: DESIGN_WIDTH, height: DESIGN_HEIGHT }}>
<div className="gain-panel__title">{title}</div>

View File

@ -0,0 +1,118 @@
.gate-section {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: var(--gain-section-height);
min-height: 0;
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: 1px solid var(--border-hairline);
}
.gate-panels {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: safe center;
gap: var(--space-4);
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow-x: auto;
overflow-y: hidden;
}
.gate-panel-frame {
flex: 0 0 auto;
height: calc(100% - 8px);
aspect-ratio: var(--panel-w) / var(--panel-h);
margin: 4px 0;
box-sizing: border-box;
}
.gate-panels::-webkit-scrollbar {
height: 6px;
}
.gate-panels::-webkit-scrollbar-thumb {
background: var(--border-hairline);
border-radius: var(--radius-pill);
}
.gate-panels::-webkit-scrollbar-track {
background: transparent;
}
.gate-panel {
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;
background: var(--bg-void);
}
.gate-panel__title {
width: 100%;
text-align: center;
font-family: var(--font-ui);
font-size: 12px;
font-weight: var(--font-weight-semibold);
padding-bottom: 6px;
border-bottom: 1px solid var(--border-hairline);
}
.gate-panel__sliders {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: center;
gap: var(--space-2);
width: 100%;
flex: 1;
min-height: 0;
}
.gate-panel__slider-col {
display: flex;
flex-direction: column;
align-items: center;
flex: 1 1 0;
min-width: 0;
}
.gate-panel__slider-label {
font-family: var(--font-ui);
font-size: 10px;
font-weight: var(--font-weight-medium);
text-align: center;
padding-top: 4px;
color: var(--text-secondary);
}
/* GateSection.css */
.gate-panels .channel-group__items {
display: grid;
grid-template-columns: repeat(2, auto);
grid-template-rows: repeat(2, 1fr);
gap: var(--space-2);
align-items: stretch;
}

View File

@ -7,7 +7,7 @@
-webkit-app-region: drag;
padding-inline: var(--space-3);
gap: var(--space-3);
box-sizing: border-box
box-sizing: border-box;
}
.logo-slot {
@ -37,7 +37,6 @@
-webkit-app-region: no-drag;
border-left: 1px solid var(--border-hairline);
scrollbar-width: thin;
scrollbar-color: var(--border-hairline) transparent;
scroll-behavior: smooth;
@ -114,7 +113,7 @@
color: var(--text-muted);
font-family: var(--font-ui);
font-weight: 500;
font-weight: var(--font-weight-medium);
flex-shrink: 0;

View File

@ -0,0 +1,69 @@
.add-dsp-form__header {
margin-bottom: var(--space-5);
}
.add-dsp-form__title {
margin: 0;
font-size: var(--font-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.add-dsp-form__hint {
margin: var(--space-2) 0 0;
font-size: var(--font-sm);
color: var(--text-muted);
}
.add-dsp-form__field {
display: flex;
flex-direction: column;
gap: var(--space-2);
margin-bottom: var(--space-4);
flex: 1;
min-width: 0;
}
.add-dsp-form__row {
display: flex;
gap: var(--space-3);
}
.add-dsp-form__field label {
font-size: var(--font-sm);
color: var(--text-muted);
}
.add-dsp-form__field input,
.add-dsp-form__field select {
height: var(--card-control-height);
padding: 0 var(--space-3);
background: var(--bg-panel);
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-ui);
font-size: var(--font-sm);
outline: none;
transition: border-color var(--dur-base) var(--ease-standard);
}
.add-dsp-form__field input.mono {
font-family: var(--font-mono);
}
.add-dsp-form__field input:focus,
.add-dsp-form__field select:focus {
border-color: var(--accent-brand);
}
.add-dsp-form__actions {
display: flex;
justify-content: flex-end;
gap: var(--space-3);
margin-top: var(--space-2);
}

View File

@ -1,6 +1,9 @@
import { useState } from 'react';
import { DSP, DSP_TYPES } from '../types/types';
import Button from '../components/Button/Button';
import { DSP, DSP_TYPES } from '../../types/types';
import Button from '../../components/Button/Button';
import Card from '../../components/Card/Card';
import './AddDspPage.css';
function AddDSPForm({
onCancel,
@ -28,13 +31,13 @@ function AddDSPForm({
}
return (
<div className="card form-card">
<div className="form-header">
<h1 className="panel-title">Add DSP</h1>
<p className="form-hint">Register a new device to control from this app.</p>
<Card>
<div className="add-dsp-form__header">
<h1 className="add-dsp-form__title">Add DSP</h1>
<p className="add-dsp-form__hint">Register a new device to control from this app.</p>
</div>
<div className="field">
<div className="add-dsp-form__field">
<label htmlFor="f-name">Name</label>
<input
id="f-name"
@ -44,8 +47,8 @@ function AddDSPForm({
/>
</div>
<div className="field-row">
<div className="field">
<div className="add-dsp-form__row">
<div className="add-dsp-form__field">
<label htmlFor="f-type">Model</label>
<select
id="f-type"
@ -68,7 +71,7 @@ function AddDSPForm({
</select>
</div>
<div className="field">
<div className="add-dsp-form__field">
<label htmlFor="f-deviceid">Device ID</label>
<input
id="f-deviceid"
@ -84,8 +87,8 @@ function AddDSPForm({
</div>
</div>
<div className="field-row">
<div className="field field--grow">
<div className="add-dsp-form__row">
<div className="add-dsp-form__field">
<label htmlFor="f-ip">IP address</label>
<input
id="f-ip"
@ -95,7 +98,7 @@ function AddDSPForm({
/>
</div>
<div className="field field--narrow">
<div className="add-dsp-form__field">
<label htmlFor="f-port">Port</label>
<input
id="f-port"
@ -107,7 +110,7 @@ function AddDSPForm({
</div>
</div>
<div className="form-actions">
<div className="add-dsp-form__actions">
<Button variant="ghost" onClick={onCancel}>
Cancel
</Button>
@ -116,7 +119,7 @@ function AddDSPForm({
Add DSP
</Button>
</div>
</div>
</Card>
);
}

View File

@ -1,35 +0,0 @@
function CreditsPage() {
return (
<div className="card panel">
<div className="panel-header">
<div>
<h1 className="panel-title">Credits</h1>
<div className="panel-type">Thank you to everyone supporting the project</div>
</div>
</div>
<dl className="specs">
<div className="spec">
<dt>Developer</dt>
<dd>Your Name</dd>
</div>
<div className="spec">
<dt>Design</dt>
<dd>Open Source Community</dd>
</div>
<div className="spec">
<dt>Libraries</dt>
<dd className="mono">React · Tauri · TypeScript</dd>
</div>
</dl>
<div className="panel-actions">
<button className="btn btn-primary">Support the project</button>
</div>
</div>
);
}
export default CreditsPage;

View File

@ -0,0 +1,26 @@
.credits-page__header {
margin-bottom: var(--space-5);
}
.credits-page__title {
margin: 0;
font-size: var(--font-2xl);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.credits-page__subtitle {
margin: var(--space-2) 0 0;
font-size: var(--font-sm);
color: var(--text-muted);
}
.credits-page__actions {
display: flex;
justify-content: flex-end;
margin-top: var(--space-2);
}

View File

@ -0,0 +1,45 @@
import Card from '../../components/Card/Card';
import Button from '../../components/Button/Button';
import './CreditsPage.css';
function CreditsPage() {
return (
<Card>
<div className="credits-page__header">
<h1 className="credits-page__title">Credits</h1>
<p className="credits-page__subtitle">
Thanks to the people, projects, and communities that made this application possible.
</p>
</div>
<dl className="specs">
<div className="spec">
<dt>Developer</dt>
<dd>Lucas</dd>
</div>
<div className="spec">
<dt>Framework</dt>
<dd>React + TypeScript</dd>
</div>
<div className="spec">
<dt>Desktop Runtime</dt>
<dd>Tauri</dd>
</div>
<div className="spec">
<dt>Special Thanks</dt>
<dd></dd>
</div>
</dl>
<div className="credits-page__actions">
<Button variant="primary">Support the Project</Button>
</div>
</Card>
);
}
export default CreditsPage;

View File

@ -1,35 +1,14 @@
.home-page {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.status-block {
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
padding: var(--space-6);
.status-label {
font-weight: 500;
transition: color var(--dur-base) var(--ease-standard);
}
.status-label--connected {
color: var(--accent-live);
}
.status-label--connecting {
color: var(--accent-connecting);
}
.status-label--disconnected {
color: var(--text-muted);
box-sizing: border-box;
}
.specs {
@ -50,18 +29,30 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
}
.spec dt {
color: var(--text-muted);
font-size: var(--font-md);
}
.spec dd {
margin: 0;
color: var(--text-primary);
font-weight: var(--font-weight-medium);
font-size: var(--font-md);
}
.panel-actions {
.home-panel-actions {
display: flex;
justify-content: flex-end;
}
.card-content {
display: flex;
flex-direction: column;
gap: var(--space-6);
}

View File

@ -2,39 +2,41 @@ import Button from '../../components/Button/Button';
import Card from '../../components/Card/Card';
import './HomePage.css';
function HomePage({
dspCount,
connectedCount,
onAddDSP,
}: {
interface HomePageProps {
dspCount: number;
connectedCount: number;
onAddDSP: () => void;
}) {
}
function HomePage({ dspCount, connectedCount, onAddDSP }: HomePageProps) {
return (
<Card>
<dl className="specs">
<div className="spec">
<dt>Connected</dt>
<dd>
{connectedCount} DSP{connectedCount !== 1 ? 's' : ''}
</dd>
</div>
<div className="home-page">
<Card>
<div className="card-content">
<dl className="specs">
<div className="spec">
<dt>Connected</dt>
<dd>
{connectedCount} DSP{connectedCount !== 1 ? 's' : ''}
</dd>
</div>
<div className="spec">
<dt>Devices</dt>
<dd>
{dspCount} DSP{dspCount !== 1 ? 's' : ''} configured
</dd>
</div>
</dl>
<div className="spec">
<dt>Devices</dt>
<dd>
{dspCount} DSP{dspCount !== 1 ? 's' : ''} configured
</dd>
</div>
</dl>
<div className="panel-actions">
<Button variant="primary" onClick={onAddDSP}>
Add a DSP
</Button>
</div>
</Card>
<div className="home-panel-actions">
<Button variant="primary" onClick={onAddDSP}>
Add a DSP
</Button>
</div>
</div>
</Card>
</div>
);
}

View File

@ -58,105 +58,15 @@ button {
transform var(--dur-base) var(--ease-standard);
}
/* ---------- Stage / main area ---------- */
.stage {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px var(--space-6);
background: var(--bg-panel);
overflow-y: auto;
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
color: var(--text-muted);
text-align: center;
}
.empty-mark {
font-size: 28px;
color: var(--border-hairline);
}
.empty p {
margin: 0;
font-size: 14px;
}
/* ---------- Form ---------- */
.form-header {
margin-bottom: var(--space-5);
}
.form-hint {
margin: 6px 0 0;
font-size: 13px;
color: var(--text-muted);
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: var(--space-4);
flex: 1;
}
.field--grow {
flex: 2;
}
.field--narrow {
flex: 1;
}
.field-row {
display: flex;
gap: var(--space-3);
}
.field label {
font-size: 12px;
color: var(--text-muted);
}
.field input,
.field select {
height: 36px;
padding: 0 10px;
background: var(--bg-panel);
border: 1px solid var(--border-hairline);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: var(--font-ui);
font-size: 13px;
outline: none;
transition: border-color var(--dur-base) var(--ease-standard);
}
.field input.mono {
font-family: var(--font-mono);
}
.field input:focus,
.field select:focus {
border-color: var(--accent-brand);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: var(--space-2);
}
/* Global scrollbar */
::-webkit-scrollbar {
width: 4px;

View File

@ -23,6 +23,11 @@
--font-xl: 1.25rem;
--font-2xl: 1.5rem;
/* ---------- Typography ---------- */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
/* ---------- Spacing Scale ---------- */
--space-0: 0;
--space-1: 0.25rem;
@ -45,16 +50,21 @@
--radius-lg: 10px;
--radius-pill: 999px;
/* ---------- Header ---------- */
--header-height: 50px;
--font-size-header: 1rem;
--tab-height: calc(var(--header-height) * 0.75);
/* ---------- Card ---------- */
--card-width: 500px;
--card-control-height: 36px;
/* ---------- Button ---------- */
--disabled-opacity: 0.45;
/* ---------- SideBar ---------- */
--sidebar-width-collapsed: calc(var(--header-height) * 0.85 + 2 * var(--space-3));
--sidebar-width-collapsed: calc(var(--header-height) * 0.85 + 2 * var(--space-3));
--gain-section-height: 40vh;
@ -88,14 +98,10 @@
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
/* ---------- Shadow ---------- */
--shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25);
/* ---------- Animation ---------- */
--transition-fast: 120ms;
--transition-base: 180ms;