From 3d7c0d2332f9325848cdf1837c9f36c22d0f0e77 Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Fri, 24 Jul 2026 14:50:59 +0200 Subject: [PATCH] feat: responsive header --- src/App.tsx | 2 +- src/components/AutoScale/AutoScale.css | 22 -- src/components/AutoScale/AutoScale.tsx | 78 ------ src/components/Button/Button.css | 79 ++---- src/components/Button/Button.tsx | 70 +---- src/components/Led/Led.css | 58 ++++ src/components/Led/Led.tsx | 17 ++ .../ConnectionSection/ConnectionSection.tsx | 9 - .../dsp408/GainSection/GainSection.tsx | 1 - src/components/topbar/Topbar.css | 265 ++++++++++++------ src/components/topbar/Topbar.tsx | 85 ++++-- src/pages/HomePage/HomePage.css | 0 src/pages/{ => HomePage}/HomePage.tsx | 2 +- src/styles/App.css | 264 +---------------- src/styles/variables.css | 22 +- 15 files changed, 364 insertions(+), 610 deletions(-) delete mode 100644 src/components/AutoScale/AutoScale.css delete mode 100644 src/components/AutoScale/AutoScale.tsx create mode 100644 src/components/Led/Led.css create mode 100644 src/components/Led/Led.tsx create mode 100644 src/pages/HomePage/HomePage.css rename src/pages/{ => HomePage}/HomePage.tsx (93%) diff --git a/src/App.tsx b/src/App.tsx index 583dc06..33e4110 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import { useNotifications } from './hooks/useNotifications'; import Notifications from './components/Notifications/Notifications'; import { DSPState } from './types/dsp408State'; import CreditsPage from './pages/CreditsPage'; -import HomePage from './pages/HomePage'; +import HomePage from './pages/HomePage/HomePage'; function App() { const [dsps, setDsps] = useState([]); diff --git a/src/components/AutoScale/AutoScale.css b/src/components/AutoScale/AutoScale.css deleted file mode 100644 index a4c2d62..0000000 --- a/src/components/AutoScale/AutoScale.css +++ /dev/null @@ -1,22 +0,0 @@ -.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; -} diff --git a/src/components/AutoScale/AutoScale.tsx b/src/components/AutoScale/AutoScale.tsx deleted file mode 100644 index ac2533c..0000000 --- a/src/components/AutoScale/AutoScale.tsx +++ /dev/null @@ -1,78 +0,0 @@ -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(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 ( -
-
-
- {children} -
-
-
- ); -} diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css index 00ee527..b143f7b 100644 --- a/src/components/Button/Button.css +++ b/src/components/Button/Button.css @@ -1,20 +1,23 @@ .btn { - width: auto; - height: 36px; - padding: 0 var(--space-4); - border-radius: var(--radius-sm); - font-family: var(--font-ui); - font-size: 13px; - font-weight: 500; - cursor: pointer; - border: 1px solid transparent; + width: 100%; + height: 100%; + display: flex; align-items: center; justify-content: center; - transition: - background var(--dur-base) var(--ease-standard), - border-color var(--dur-base) var(--ease-standard), - opacity var(--dur-base) var(--ease-standard); + + box-sizing: border-box; + + padding: 0 var(--space-4); + + border-radius: var(--radius-sm); + + font-family: var(--font-ui); + font-size: 13px; + font-weight: 500; + + cursor: pointer; + border: 1px solid transparent; } .btn:disabled { @@ -22,73 +25,25 @@ cursor: not-allowed; } -/* Primary */ .btn-primary { background: var(--accent-brand); color: #0d0f12; } -.btn-primary:hover:not(:disabled) { - background: #6f9bf2; -} - -/* Ghost */ .btn-ghost { background: transparent; border-color: var(--border-hairline); color: var(--text-muted); } -.btn-ghost:hover { - color: var(--text-primary); - border-color: var(--text-muted); -} - -/* Toggle */ .btn-toggle { - width: 90%; - height: 40px; - margin: 8px auto; - padding: 0; - - display: flex; - align-items: center; - justify-content: center; - - border-radius: var(--radius-sm); - - /* same as ghost */ background: transparent; border-color: var(--border-hairline); color: var(--text-muted); } -.btn-toggle:hover { - color: var(--text-primary); - border-color: var(--text-muted); - background: transparent; -} - -/* Active states only */ -.btn-toggle.active-warning { - background: var(--accent-connecting); - border-color: var(--accent-connecting); - color: var(--bg-void); -} - -.btn-toggle.active-danger { - background: var(--accent-down); - border-color: var(--accent-down); - color: var(--text-primary); -} - .btn-outline { background: transparent; border-color: var(--border-hairline); color: var(--text-muted); -} - -.btn-outline:hover:not(:disabled) { - color: var(--text-primary); - border-color: var(--text-muted); -} +} \ No newline at end of file diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 0b9403e..5cd04f1 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,42 +1,13 @@ -// Button.tsx import './Button.css'; import type { ButtonHTMLAttributes, ReactNode } from 'react'; -import { useMemo } from 'react'; -import AutoScale from '../AutoScale/AutoScale'; 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 = { - 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 { variant?: ButtonVariant; children: ReactNode; 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) + fontSize?: number; } export default function Button({ @@ -44,41 +15,18 @@ export default function Button({ children, className = '', disabled, - onClick, + fontSize = 13, type = 'button', - fontSize, - designWidth, - designHeight, - scale = true, style, ...props }: ButtonProps) { - 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 = ( + return (
{dsps.map((dsp) => ( onSelect(dsp.id)} @@ -104,28 +145,28 @@ function TopBar({
{hasTabs ? ( - ) : ( - )} -
- +
); } diff --git a/src/pages/HomePage/HomePage.css b/src/pages/HomePage/HomePage.css new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage/HomePage.tsx similarity index 93% rename from src/pages/HomePage.tsx rename to src/pages/HomePage/HomePage.tsx index 5a602a1..9020a58 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -1,4 +1,4 @@ -import Button from '../components/Button/Button'; +import Button from '../../components/Button/Button'; function HomePage({ dspCount, diff --git a/src/styles/App.css b/src/styles/App.css index cfe718b..d0cf788 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -49,270 +49,12 @@ button:focus-visible, box-shadow: inset 0 0 0 1px var(--accent-brand); } -/* ---------- Top bar ---------- */ - -.topbar { - display: flex; - align-items: center; - gap: var(--space-5); - padding: 0 var(--space-4); - height: 48px; - 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: 100%; - 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: 48px; - 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); -} - -.crown-action { - color: #facc15; - background: transparent; - border: none; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; -} - -.tabstrip::-webkit-scrollbar { - height: 5px; -} - -.tabstrip::-webkit-scrollbar-track { - background: transparent; -} - -.tabstrip::-webkit-scrollbar-thumb { - background: var(--border-hairline); - border-radius: var(--radius-pill); -} - -.tabstrip::-webkit-scrollbar-thumb:hover { - background: var(--accent-brand); -} - -.tab { - display: flex; - align-items: center; - gap: var(--space-2); - height: 34px; - 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: 13px; - cursor: pointer; +button { transition: background var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard), - color var(--dur-base) var(--ease-standard); - width: 160px; - flex: 0 0 auto; -} - -.tab:hover { - background: var(--bg-raised); - border-color: var(--text-muted); - color: var(--text-primary); -} - -.tab--active { - background: var(--bg-panel); - border-color: var(--accent-brand); - color: var(--text-primary); -} - -.tab--active:hover { - background: var(--bg-panel); - border-color: var(--accent-brand); -} - -.tab--dimmed { - opacity: 0.55; -} - -.tab-name { - overflow: hidden; - white-space: nowrap; - min-width: 0; - flex: 1 1 auto; - line-height: 1; - display: flex; - align-items: center; - height: 100%; - mask-image: linear-gradient(to right, black calc(100% - 18px), transparent 100%); - -webkit-mask-image: linear-gradient(to right, black calc(100% - 18px), transparent 100%); -} - -.tab-close { - display: flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - margin-left: auto; - width: 18px; - height: 18px; - padding: 0; - font-size: 15px; - 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-close:hover { - background: rgba(255, 255, 255, 0.08); - color: var(--text-primary); -} - -/* Quiet icon button — used once at least one tab exists */ -.tab-add { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - 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); -} - -/* Lives outside .tabstrip so it never scrolls out of view */ -.tabstrip-actions { - display: flex; - align-items: center; - flex-shrink: 0; - -webkit-app-region: no-drag; -} - -.tab-add:hover { - background: var(--bg-raised); - color: var(--text-primary); -} - -.tab-add:active { - background: var(--border-hairline); -} - -/* Clearer CTA — used when the bar has nothing else to anchor to */ -.tab-add-empty { - display: flex; - align-items: center; - justify-content: center; - line-height: 1; - gap: 6px; - height: 30px; - padding: 0 var(--space-3) 0 10px; - border: 1px dashed var(--border-hairline); - border-radius: var(--radius-pill); - background: transparent; - color: var(--text-muted); - font-family: var(--font-ui); - font-size: 12px; - 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); -} - -.tab-add-empty svg, -.tab-add svg { - display: block; - flex-shrink: 0; -} - -.tab-add-empty:hover { - background: var(--bg-raised); - border-color: var(--accent-brand); - border-style: solid; - color: var(--text-primary); -} - -/* ---------- LED status dot ---------- */ - -.led { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; - background: var(--accent-down); - transition: background var(--dur-base) var(--ease-standard); -} - -.led--connected { - background: var(--accent-live); - box-shadow: 0 0 6px 1px var(--accent-live); -} - -.led--connecting { - background: var(--accent-connecting); - box-shadow: 0 0 6px 1px var(--accent-connecting); - animation: pulse 1s var(--ease-standard) infinite; -} - -.led--disconnected { - background: var(--accent-down); - box-shadow: 0 0 4px 0px var(--accent-down); -} - -.led--lg { - width: 12px; - height: 12px; -} - -@keyframes pulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.4; - } + color var(--dur-base) var(--ease-standard), + transform var(--dur-base) var(--ease-standard); } /* ---------- Stage / main area ---------- */ diff --git a/src/styles/variables.css b/src/styles/variables.css index aefd81d..926005c 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -22,13 +22,10 @@ --tab-close-size: 1.8vh; --tab-close-font: 1.5vh; - --tab-add-size: 2.6vh; + /* --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; @@ -68,7 +65,7 @@ /* ---------- Radius ---------- */ --radius-sm: 6px; --radius-md: 8px; - --radius-lg: 12px; + --radius-lg: 1vh; --radius-pill: 999px; /* ---------- Shadow ---------- */ @@ -76,10 +73,21 @@ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25); /* ---------- Border ---------- */ - --border-width: 1px; - --border-width-active: 2px; + --border-width: 0.07vh; + --border-width-active: 0.1vh; /* ---------- Animation ---------- */ --transition-fast: 120ms; --transition-base: 180ms; + + --font-xs: 1.2vh; + --font-sm: 1.4vh; + --font-md: 1.6vh; + --font-lg: 1.9vh; + --font-xl: 2.2vh; + + --font-button: 1.5vh; + --font-label: 1.4vh; + --font-title: 1.8vh; + --font-value: 1.6vh; }