diff --git a/src/App.tsx b/src/App.tsx index 632346c..583dc06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core'; import { useState } from 'react'; import './styles/App.css'; 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 AddDSPForm from './pages/AddDspPage'; import { useNotifications } from './hooks/useNotifications'; diff --git a/src/assets/icons/AnimatedLogo.tsx b/src/assets/icons/AnimatedLogo.tsx index 6e2490e..a9a442e 100644 --- a/src/assets/icons/AnimatedLogo.tsx +++ b/src/assets/icons/AnimatedLogo.tsx @@ -6,7 +6,7 @@ const DONUT_HOVER_SPEED = 360 / 2; const INNER_BASE_SPEED = 360 / 12; const INNER_HOVER_SPEED = 360 / 3; -const AnimatedLogo = ({ size = 45, className = '' }) => { +const AnimatedLogo = ({ className = '' }) => { const [isHovered, setIsHovered] = useState(false); const isHoveredRef = useRef(false); const donutGroupRef = useRef(null); @@ -58,7 +58,6 @@ const AnimatedLogo = ({ size = 45, className = '' }) => { return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > diff --git a/src/assets/icons/AudioIcons.tsx b/src/assets/icons/AudioIcons.tsx index 19624b3..776429c 100644 --- a/src/assets/icons/AudioIcons.tsx +++ b/src/assets/icons/AudioIcons.tsx @@ -31,3 +31,36 @@ export function SlidersIcon(props: React.SVGProps) { ); } + +export function AudioWaveIcon(props: React.SVGProps) { + return ( + + + + ); +} + +export function KnobGainIcon(props: React.SVGProps) { + return ( + + + + + + + + + + ); +} diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css index 5dd8892..00ee527 100644 --- a/src/components/Button/Button.css +++ b/src/components/Button/Button.css @@ -8,6 +8,9 @@ font-weight: 500; cursor: pointer; border: 1px solid transparent; + display: flex; + align-items: center; + justify-content: center; transition: background var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard), diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index f9d2186..0b9403e 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -70,22 +70,22 @@ export default function Button({ }, [fontSize, children, W]); const button = ( - -); + + ); if (!scale) return button; diff --git a/src/components/Sidebar/Sidebar.css b/src/components/Sidebar/Sidebar.css index 3ef1544..6363703 100644 --- a/src/components/Sidebar/Sidebar.css +++ b/src/components/Sidebar/Sidebar.css @@ -1,12 +1,11 @@ /* ---------- Label ---------- */ .sidebar-label { + font-size: var(--sidebar-label-size); opacity: 0; max-width: 0; - overflow: hidden; white-space: nowrap; - transition: opacity var(--dur-fast) var(--ease-standard), max-width var(--dur-base) var(--ease-standard); diff --git a/src/components/dsp408/ChannelGroup/ChannelGroup.css b/src/components/dsp408/ChannelGroup/ChannelGroup.css new file mode 100644 index 0000000..2df995f --- /dev/null +++ b/src/components/dsp408/ChannelGroup/ChannelGroup.css @@ -0,0 +1,43 @@ +.channel-group { + position: relative; + height: calc(100% - var(--space-1) - var(--space-1)); + flex: 0 0 auto; + + display: flex; + align-items: flex-end; + + border: 1px solid var(--border-hairline); + border-radius: var(--radius-md); + + /* enough top padding to fully clear the label's own height, not just its border-overlap */ + padding: calc(1.2vh + var(--space-3)) var(--space-3) 0; + margin-bottom: var(--space-1); + margin-top: var(--space-1); + box-sizing: border-box; + overflow: visible; /* in case something upstream set hidden */ +} + +.channel-group__label { + position: absolute; + top: var(--space-2); /* sits inside the padded space, not straddling the border */ + left: 50%; + transform: translateX(-50%); /* only horizontal centering now — no vertical overlap */ + + padding: 0 var(--space-2); + + font-family: var(--font-ui); + font-size: var(--tab-close-font); + font-weight: 600; + color: var(--text-muted); + + white-space: nowrap; + line-height: 1; +} + +.channel-group__items { + display: flex; + flex-direction: row; + align-items: stretch; + height: 100%; + gap: var(--space-4); +} diff --git a/src/components/dsp408/ChannelGroup/ChannelGroup.tsx b/src/components/dsp408/ChannelGroup/ChannelGroup.tsx new file mode 100644 index 0000000..82a622d --- /dev/null +++ b/src/components/dsp408/ChannelGroup/ChannelGroup.tsx @@ -0,0 +1,13 @@ +// ChannelGroup.tsx +import './ChannelGroup.css'; + +function ChannelGroup({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +export default ChannelGroup; diff --git a/src/components/dsp408/ConnectionSection/ConnectionSection.css b/src/components/dsp408/ConnectionSection/ConnectionSection.css index 161c510..687e444 100644 --- a/src/components/dsp408/ConnectionSection/ConnectionSection.css +++ b/src/components/dsp408/ConnectionSection/ConnectionSection.css @@ -14,6 +14,13 @@ position: relative; } +.panel-type { + margin-top: var(--space-1); + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-mono); + letter-spacing: 0.03em; +} .panel-header { display: flex; @@ -32,6 +39,9 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + margin: 0; + font-size: 18px; + font-weight: 600; } .mono { @@ -48,15 +58,15 @@ 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 */ + 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; /* was: fixed — relative to .dsp-content now */ + inset: 0; /* replaces top:0; right:0; bottom:0; left:0 */ display: flex; justify-content: center; align-items: center; -} \ No newline at end of file +} diff --git a/src/components/dsp408/ConnectionSection/ConnectionSection.tsx b/src/components/dsp408/ConnectionSection/ConnectionSection.tsx index 2816d26..80439ae 100644 --- a/src/components/dsp408/ConnectionSection/ConnectionSection.tsx +++ b/src/components/dsp408/ConnectionSection/ConnectionSection.tsx @@ -27,71 +27,77 @@ function ConnectionPanel({ : 'Disconnected'; return ( -
-
- -
-
-
-

- {dsp.name} -

-
{dsp.type}
+
+
+ +
+
+
+

+ {dsp.name} +

+
{dsp.type}
+
+ +
+ + {statusLabel} +
-
- - {statusLabel} +
+
+
IP address
+
+ {dsp.ip} +
+
+ +
+
Port
+
{dsp.port}
+
+ +
+
Device ID
+
+ {dsp.deviceId || '—'} +
+
+
+ +
+ {dsp.status === 'connected' ? ( + + ) : ( + + )}
- -
-
-
IP address
-
- {dsp.ip} -
-
- -
-
Port
-
{dsp.port}
-
- -
-
Device ID
-
- {dsp.deviceId || '—'} -
-
-
- -
- {dsp.status === 'connected' ? ( - - ) : ( - - )} -
-
-
-
+ +
); } -export default ConnectionPanel; \ No newline at end of file +export default ConnectionPanel; diff --git a/src/components/dsp408/GainSection/GainSection.css b/src/components/dsp408/GainSection/GainSection.css index 178314e..ef15ee5 100644 --- a/src/components/dsp408/GainSection/GainSection.css +++ b/src/components/dsp408/GainSection/GainSection.css @@ -1,10 +1,6 @@ -:root { - --gain-section-height: 40vh; -} - .gain-section { - position: absolute; /* was: fixed — now relative to .dsp-content, which is the fixed+inset parent */ - left: 0; /* was: var(--sidebar-width-collapsed) — .dsp-content already excludes the sidebar */ + position: absolute; /* was: fixed — now relative to .dsp-content, which is the fixed+inset parent */ + left: 0; /* was: var(--sidebar-width-collapsed) — .dsp-content already excludes the sidebar */ right: 0; bottom: 0; @@ -42,7 +38,7 @@ .gain-panel-frame { flex: 0 0 auto; height: calc(100% - 8px); - aspect-ratio: var(--panel-w) / var(--panel-h); /* now driven by JS, not duplicated */ + aspect-ratio: var(--panel-w) / var(--panel-h); /* now driven by JS, not duplicated */ margin: 4px 0; box-sizing: border-box; } diff --git a/src/components/dsp408/GainSection/GainSection.tsx b/src/components/dsp408/GainSection/GainSection.tsx index ddd3df5..0ada2d0 100644 --- a/src/components/dsp408/GainSection/GainSection.tsx +++ b/src/components/dsp408/GainSection/GainSection.tsx @@ -6,6 +6,7 @@ import { NotificationType } from '../../../types/types'; import { useState } from 'react'; import Button from '../../Button/Button'; import AutoScale from '../../AutoScale/AutoScale'; +import ChannelGroup from '../ChannelGroup/ChannelGroup'; import './GainSection.css'; const DESIGN_WIDTH = 90; @@ -139,12 +140,14 @@ function GainSection({ dsp: DSP; notify: (message: string, type?: NotificationType, dspName?: string) => void; }) { - const channels: { channel: Channel; title: string }[] = [ + const inputs: { channel: Channel; title: string }[] = [ { channel: { Input: InputChannel.InA }, title: 'In A' }, { channel: { Input: InputChannel.InB }, title: 'In B' }, { channel: { Input: InputChannel.InC }, title: 'In C' }, { channel: { Input: InputChannel.InD }, title: 'In D' }, + ]; + const outputs: { channel: Channel; title: string }[] = [ { channel: { Output: OutputChannel.Out1 }, title: 'Out 1' }, { channel: { Output: OutputChannel.Out2 }, title: 'Out 2' }, { channel: { Output: OutputChannel.Out3 }, title: 'Out 3' }, @@ -158,9 +161,17 @@ function GainSection({ return (
- {channels.map(({ channel, title }) => ( - - ))} + + {inputs.map(({ channel, title }) => ( + + ))} + + + + {outputs.map(({ channel, title }) => ( + + ))} +
); diff --git a/src/components/topbar/Tab.tsx b/src/components/topbar/Tab.tsx deleted file mode 100644 index a8bed6f..0000000 --- a/src/components/topbar/Tab.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { DSP } from '../../types/types'; - -function Tab({ - dsp, - active, - dimmed, - onSelect, - onRemove, -}: { - dsp: DSP; - active: boolean; - dimmed: boolean; - onSelect: () => void; - onRemove: () => void; -}) { - return ( - - ); -} - -export default Tab; diff --git a/src/components/topbar/Topbar.css b/src/components/topbar/Topbar.css new file mode 100644 index 0000000..dd829b7 --- /dev/null +++ b/src/components/topbar/Topbar.css @@ -0,0 +1,148 @@ +.topbar { + display: flex; + align-items: center; + gap: var(--space-5); + padding: 0 var(--space-4); + height: var(--header-height); + background: var(--bg-void); + border-bottom: 1px solid var(--border-hairline); + -webkit-app-region: drag; +} + +.logo-slot { + display: flex; + align-items: center; + justify-content: center; + height: 80%; + aspect-ratio: 1 / 1; + width: auto; + flex-shrink: 0; + overflow: hidden; + -webkit-app-region: no-drag; +} + +.logo-slot > * { + width: 100%; + height: 100%; +} + +.tabstrip { + display: flex; + align-items: center; + gap: 2px; + height: var(--header-height); + flex: 1 1 auto; + min-width: 0; + overflow-x: auto; + overflow-y: hidden; + -webkit-app-region: no-drag; + scrollbar-width: thin; + scrollbar-color: var(--border-hairline) transparent; + scroll-behavior: smooth; + scroll-padding-inline: var(--space-3); +} + +.tab { + display: flex; + align-items: center; + gap: var(--space-2); + height: var(--tab-height); + padding: 0 var(--space-3); + border: 1px solid var(--border-hairline); + border-radius: var(--radius-md); + background: transparent; + color: var(--text-muted); + font-family: var(--font-ui); + font-size: var(--tab-font-size); + cursor: pointer; + transition: + background var(--dur-base) var(--ease-standard), + border-color var(--dur-base) var(--ease-standard), + color var(--dur-base) var(--ease-standard); + width: var(--tab-width); + flex: 0 0 auto; +} + +.tab-close { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + margin-left: auto; + width: var(--tab-close-size); + height: var(--tab-close-size); + padding: 0; + font-size: var(--tab-close-font); + line-height: 1; + color: var(--text-muted); + border-radius: 4px; + transition: + background var(--dur-fast) var(--ease-standard), + color var(--dur-fast) var(--ease-standard); +} + +.tab-add { + display: flex; + align-items: center; + justify-content: center; + width: var(--tab-add-size); + height: var(--tab-add-size); + margin-left: 6px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--text-muted); + cursor: pointer; + flex-shrink: 0; + transition: + background var(--dur-base) var(--ease-standard), + color var(--dur-base) var(--ease-standard); +} + +.tab-add-empty { + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + gap: 6px; + height: var(--tab-add-empty-height); + padding: 0 var(--space-3) 0 var(--space-3); + border: 1px dashed var(--border-hairline); + border-radius: var(--radius-pill); + background: transparent; + color: var(--text-muted); + font-family: var(--font-ui); + font-size: var(--tab-add-empty-font); + font-weight: 500; + cursor: pointer; + flex-shrink: 0; + transition: + background var(--dur-base) var(--ease-standard), + border-color var(--dur-base) var(--ease-standard), + color var(--dur-base) var(--ease-standard); +} + +.led { + width: var(--led-size); + height: var(--led-size); + border-radius: 50%; + flex-shrink: 0; + background: var(--accent-down); + transition: background var(--dur-base) var(--ease-standard); +} + +.led--lg { + width: var(--led-size-lg); + height: var(--led-size-lg); +} + +.icon-tab-add, +.icon-tab-add-empty { + width: var(--tab-add-icon-size); + height: var(--tab-add-icon-size); +} + +.icon-crown { + width: var(--crown-icon-size); + height: var(--crown-icon-size); +} diff --git a/src/components/topbar/Topbar.tsx b/src/components/topbar/Topbar.tsx index 6acf9aa..00805b9 100644 --- a/src/components/topbar/Topbar.tsx +++ b/src/components/topbar/Topbar.tsx @@ -1,10 +1,48 @@ import { useEffect, useRef } from 'react'; import { DSP, AppPage } from '../../types/types'; -import Tab from './Tab'; import AnimatedLogo from '../../assets/icons/AnimatedLogo'; import { PlusIcon } from '../../assets/icons/PlusIcon'; import { CrownIcon } from '../../assets/icons/CrownIcon'; +import './Topbar.css'; + +function Tab({ + dsp, + active, + dimmed, + onSelect, + onRemove, +}: { + dsp: DSP; + active: boolean; + dimmed: boolean; + onSelect: () => void; + onRemove: () => void; +}) { + return ( + + ); +} + function TopBar({ dsps, selected, @@ -67,11 +105,11 @@ function TopBar({
{hasTabs ? ( ) : ( )} @@ -86,7 +124,7 @@ function TopBar({ setSelect(null); }} > - + ); diff --git a/src/pages/DspPage/DspPage.tsx b/src/pages/DspPage/DspPage.tsx index 70967b0..f03d918 100644 --- a/src/pages/DspPage/DspPage.tsx +++ b/src/pages/DspPage/DspPage.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; import { DSP } from '../../types/types'; import Sidebar from '../../components/Sidebar/Sidebar'; -import { InfoIcon, SlidersIcon } from '../../assets/icons/AudioIcons'; +import { InfoIcon, KnobGainIcon } from '../../assets/icons/AudioIcons'; import ConnectionPanel from '../../components/dsp408/ConnectionSection/ConnectionSection'; import GainSection from '../../components/dsp408/GainSection/GainSection'; import { NotificationType } from '../../types/types'; -import './DspPage.css' +import './DspPage.css'; function DSPPage({ dsp, @@ -30,7 +30,7 @@ function DSPPage({ { id: 'gain', label: 'Gain', - icon: , + icon: , }, ]; diff --git a/src/styles/App.css b/src/styles/App.css index d3623d5..cfe718b 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -357,28 +357,6 @@ button:focus-visible, /* ---------- DSP panel ---------- */ -.panel-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-4); - margin-bottom: var(--space-6); -} - -.panel-title { - margin: 0; - font-size: 18px; - font-weight: 600; -} - -.panel-type { - margin-top: var(--space-1); - font-size: 12px; - color: var(--text-muted); - font-family: var(--font-mono); - letter-spacing: 0.03em; -} - .status-block { display: flex; align-items: center; @@ -501,77 +479,3 @@ button:focus-visible, gap: 10px; margin-top: var(--space-2); } - -/* ---------- DSP detail: side rail + content ---------- */ - -.dsp-detail { - display: flex; - align-items: flex-start; - gap: var(--space-5); - width: 100%; - max-width: 720px; -} - -.dsp-subnav { - display: flex; - flex-direction: column; - gap: 2px; - flex: 0 0 168px; - padding: var(--space-2); - background: var(--bg-panel); - border: 1px solid var(--border-hairline); - border-radius: var(--radius-md); -} - -.dsp-subnav-item { - display: flex; - align-items: center; - height: 34px; - padding: 0 var(--space-3); - border: none; - border-left: 2px solid transparent; - border-radius: var(--radius-sm); - background: transparent; - color: var(--text-muted); - font-family: var(--font-ui); - font-size: 13px; - text-align: left; - cursor: pointer; - transition: - background var(--dur-base) var(--ease-standard), - color var(--dur-base) var(--ease-standard), - border-color var(--dur-base) var(--ease-standard); -} - -.dsp-subnav-item:hover { - background: var(--bg-raised); - color: var(--text-primary); -} - -.dsp-subnav-item--active { - background: var(--bg-raised); - border-left-color: var(--accent-brand); - color: var(--text-primary); - font-weight: 500; -} - -.dsp-subnav-content { - flex: 1 1 auto; - min-width: 0; -} - -/* Let the panel card fill the available width next to the rail, - instead of capping out at 480px like it does standalone in .stage */ -.dsp-subnav-content .card { - max-width: none; - width: 100%; -} - -/* ---------- Layout wrapper below the topbar ---------- */ -/* .app is already: display:flex; flex-direction:column; height:100vh; - this row just needs to fill the remaining space */ -.main-layout { - flex: 1; - display: flex; - min-height: 0; /* lets .stage's own overflow-y:auto do the scrolling */ -} diff --git a/src/styles/variables.css b/src/styles/variables.css index 43cc2d7..aefd81d 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -1,9 +1,37 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap'); :root { - --header-height: 48px; /* matches .topbar's existing height */ - --sidebar-width-collapsed: 56px; - --sidebar-width-expanded: 180px; + --header-height: 7vh; + --gain-section-height: 40vh; + + --sidebar-width-collapsed: 6vh; + --sidebar-width-expanded: 22vh; + + --sidebar-icon-size: 2vh; + --sidebar-item-size: 4.5vh; + --sidebar-toggle-size: 3vh; + --sidebar-label-size: 1.9vh; + --sidebar-item-gap: 1vh; + --sidebar-item-padding: 1vh; + + --font-size-sm: 3vh; + + --tab-height: 4vh; + --tab-width: 18vh; + --tab-font-size: 1.3vh; + --tab-close-size: 1.8vh; + --tab-close-font: 1.5vh; + + --tab-add-size: 2.6vh; + --tab-add-empty-height: 3vh; + --tab-add-empty-font: 1.2vh; + + --led-size: 0.8vh; + --led-size-lg: 1.2vh; + + --crown-icon-size: 2.2vh; + --tab-add-icon-size: 1.6vh; + --logo-size: 18vh; /* ---------- Color ---------- */ --bg-void: #0d0f12; @@ -30,12 +58,12 @@ --ease-in: cubic-bezier(0.4, 0, 1, 1); /* ---------- Spacing scale ---------- */ - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; + --space-1: 0.5vh; + --space-2: 1vh; + --space-3: 1.5vh; + --space-4: 2vh; + --space-5: 2.5vh; + --space-6: 3vh; /* ---------- Radius ---------- */ --radius-sm: 6px; @@ -54,11 +82,4 @@ /* ---------- Animation ---------- */ --transition-fast: 120ms; --transition-base: 180ms; - - /* ---------- Component sizes ---------- */ - --sidebar-item-size: 32px; - --sidebar-item-gap: 10px; - --sidebar-item-padding: 8px; - - --sidebar-toggle-size: 24px; }