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 { DSP, DSPStatus, AppPage } from './types/types';
import TopBar from './components/Topbar/Topbar'; import TopBar from './components/Topbar/Topbar';
import DSPPage from './pages/DspPage/DspPage'; import DSPPage from './pages/DspPage/DspPage';
import AddDSPForm from './pages/AddDspPage'; import AddDSPForm from './pages/AddDspPage/AddDspPage';
import { useNotifications } from './hooks/useNotifications'; import { useNotifications } from './hooks/useNotifications';
import Notifications from './components/Notifications/Notifications'; import Notifications from './components/Notifications/Notifications';
import { DSPState } from './types/dsp408State'; import { DSPState } from './types/dsp408State';
import CreditsPage from './pages/CreditsPage'; import CreditsPage from './pages/CreditsPage/CreditsPage';
import HomePage from './pages/HomePage/HomePage'; import HomePage from './pages/HomePage/HomePage';
function App() { function App() {

View File

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

View File

@ -1,8 +1,13 @@
.card { .card {
width: min(100%, var(--card-width));
background: var(--bg-panel); background: var(--bg-panel);
border: var(--border-width) solid var(--border-hairline); border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
padding: var(--space-6); padding: var(--space-6);
color: var(--text-primary); color: var(--text-primary);
@ -12,5 +17,6 @@
display: flex; display: flex;
flex-direction: column; 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; align-items: center;
font-size: var(--font-size-sm, 13px); font-size: var(--font-size-sm, 13px);
font-weight: 600; font-weight: var(--font-weight-semibold);
color: var(--text-primary); color: var(--text-primary);
white-space: nowrap; white-space: nowrap;

View File

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

View File

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

View File

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

View File

@ -92,15 +92,7 @@ function GainPanel({
}; };
return ( return (
<div <div className="gain-panel-frame">
className="gain-panel-frame"
style={
{
'--panel-w': DESIGN_WIDTH,
'--panel-h': DESIGN_HEIGHT,
} as React.CSSProperties
}
>
<AutoScale designWidth={DESIGN_WIDTH} designHeight={DESIGN_HEIGHT}> <AutoScale designWidth={DESIGN_WIDTH} designHeight={DESIGN_HEIGHT}>
<div className="gain-panel" style={{ width: DESIGN_WIDTH, height: DESIGN_HEIGHT }}> <div className="gain-panel" style={{ width: DESIGN_WIDTH, height: DESIGN_HEIGHT }}>
<div className="gain-panel__title">{title}</div> <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; -webkit-app-region: drag;
padding-inline: var(--space-3); padding-inline: var(--space-3);
gap: var(--space-3); gap: var(--space-3);
box-sizing: border-box box-sizing: border-box;
} }
.logo-slot { .logo-slot {
@ -37,7 +37,6 @@
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
border-left: 1px solid var(--border-hairline); border-left: 1px solid var(--border-hairline);
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: var(--border-hairline) transparent; scrollbar-color: var(--border-hairline) transparent;
scroll-behavior: smooth; scroll-behavior: smooth;
@ -114,7 +113,7 @@
color: var(--text-muted); color: var(--text-muted);
font-family: var(--font-ui); font-family: var(--font-ui);
font-weight: 500; font-weight: var(--font-weight-medium);
flex-shrink: 0; 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 { useState } from 'react';
import { DSP, DSP_TYPES } from '../types/types'; import { DSP, DSP_TYPES } from '../../types/types';
import Button from '../components/Button/Button'; import Button from '../../components/Button/Button';
import Card from '../../components/Card/Card';
import './AddDspPage.css';
function AddDSPForm({ function AddDSPForm({
onCancel, onCancel,
@ -28,13 +31,13 @@ function AddDSPForm({
} }
return ( return (
<div className="card form-card"> <Card>
<div className="form-header"> <div className="add-dsp-form__header">
<h1 className="panel-title">Add DSP</h1> <h1 className="add-dsp-form__title">Add DSP</h1>
<p className="form-hint">Register a new device to control from this app.</p> <p className="add-dsp-form__hint">Register a new device to control from this app.</p>
</div> </div>
<div className="field"> <div className="add-dsp-form__field">
<label htmlFor="f-name">Name</label> <label htmlFor="f-name">Name</label>
<input <input
id="f-name" id="f-name"
@ -44,8 +47,8 @@ function AddDSPForm({
/> />
</div> </div>
<div className="field-row"> <div className="add-dsp-form__row">
<div className="field"> <div className="add-dsp-form__field">
<label htmlFor="f-type">Model</label> <label htmlFor="f-type">Model</label>
<select <select
id="f-type" id="f-type"
@ -68,7 +71,7 @@ function AddDSPForm({
</select> </select>
</div> </div>
<div className="field"> <div className="add-dsp-form__field">
<label htmlFor="f-deviceid">Device ID</label> <label htmlFor="f-deviceid">Device ID</label>
<input <input
id="f-deviceid" id="f-deviceid"
@ -84,8 +87,8 @@ function AddDSPForm({
</div> </div>
</div> </div>
<div className="field-row"> <div className="add-dsp-form__row">
<div className="field field--grow"> <div className="add-dsp-form__field">
<label htmlFor="f-ip">IP address</label> <label htmlFor="f-ip">IP address</label>
<input <input
id="f-ip" id="f-ip"
@ -95,7 +98,7 @@ function AddDSPForm({
/> />
</div> </div>
<div className="field field--narrow"> <div className="add-dsp-form__field">
<label htmlFor="f-port">Port</label> <label htmlFor="f-port">Port</label>
<input <input
id="f-port" id="f-port"
@ -107,7 +110,7 @@ function AddDSPForm({
</div> </div>
</div> </div>
<div className="form-actions"> <div className="add-dsp-form__actions">
<Button variant="ghost" onClick={onCancel}> <Button variant="ghost" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
@ -116,7 +119,7 @@ function AddDSPForm({
Add DSP Add DSP
</Button> </Button>
</div> </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 { .home-page {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
}
.status-block { padding: var(--space-6);
display: flex;
align-items: center;
gap: var(--space-2);
flex-shrink: 0;
}
.status-label { box-sizing: border-box;
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);
} }
.specs { .specs {
@ -50,18 +29,30 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: var(--space-4);
} }
.spec dt { .spec dt {
color: var(--text-muted); color: var(--text-muted);
font-size: var(--font-md);
} }
.spec dd { .spec dd {
margin: 0; margin: 0;
color: var(--text-primary); color: var(--text-primary);
font-weight: var(--font-weight-medium);
font-size: var(--font-md);
} }
.panel-actions { .home-panel-actions {
display: flex; display: flex;
justify-content: flex-end; 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 Card from '../../components/Card/Card';
import './HomePage.css'; import './HomePage.css';
function HomePage({ interface HomePageProps {
dspCount,
connectedCount,
onAddDSP,
}: {
dspCount: number; dspCount: number;
connectedCount: number; connectedCount: number;
onAddDSP: () => void; onAddDSP: () => void;
}) { }
function HomePage({ dspCount, connectedCount, onAddDSP }: HomePageProps) {
return ( return (
<Card> <div className="home-page">
<dl className="specs"> <Card>
<div className="spec"> <div className="card-content">
<dt>Connected</dt> <dl className="specs">
<dd> <div className="spec">
{connectedCount} DSP{connectedCount !== 1 ? 's' : ''} <dt>Connected</dt>
</dd> <dd>
</div> {connectedCount} DSP{connectedCount !== 1 ? 's' : ''}
</dd>
</div>
<div className="spec"> <div className="spec">
<dt>Devices</dt> <dt>Devices</dt>
<dd> <dd>
{dspCount} DSP{dspCount !== 1 ? 's' : ''} configured {dspCount} DSP{dspCount !== 1 ? 's' : ''} configured
</dd> </dd>
</div> </div>
</dl> </dl>
<div className="panel-actions"> <div className="home-panel-actions">
<Button variant="primary" onClick={onAddDSP}> <Button variant="primary" onClick={onAddDSP}>
Add a DSP Add a DSP
</Button> </Button>
</div> </div>
</Card> </div>
</Card>
</div>
); );
} }

View File

@ -58,105 +58,15 @@ button {
transform var(--dur-base) var(--ease-standard); transform var(--dur-base) var(--ease-standard);
} }
/* ---------- Stage / main area ---------- */
.stage { .stage {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 40px var(--space-6);
background: var(--bg-panel); background: var(--bg-panel);
overflow-y: auto; 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 */ /* Global scrollbar */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 4px; width: 4px;

View File

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