feat: responsive header

This commit is contained in:
2026-07-24 14:50:59 +02:00
parent 4c146902e3
commit 3d7c0d2332
15 changed files with 364 additions and 610 deletions

View File

@ -3,31 +3,37 @@ 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 './Topbar.css';
function getStep(count: number) {
if (count <= 4) return 4;
if (count <= 6) return 6;
if (count <= 8) return 8;
return 10;
}
function Tab({
dsp,
active,
dimmed,
onSelect,
onRemove,
dsp, active, dimmed, step, onSelect, onRemove,
}: {
dsp: DSP;
active: boolean;
dimmed: boolean;
step: number;
onSelect: () => void;
onRemove: () => void;
}) {
return (
<button
className={'tab' + (active ? ' tab--active' : '') + (dimmed ? ' tab--dimmed' : '')}
data-step={step}
onClick={onSelect}
>
<span className={'led led--' + dsp.status} />
<span className="tab-name" title={dsp.name}>
{dsp.name}
<span className="tab-led">
<Led status={dsp.status} />
</span>
<span title={dsp.name}>{dsp.name}</span>
<span
className="tab-close"
role="button"
@ -65,6 +71,40 @@ function TopBar({
const hasTabs = dsps.length > 0;
const tabstripRef = useRef<HTMLDivElement>(null);
const prevCount = useRef(dsps.length);
const step = getStep(dsps.length);
const updateFade = () => {
const el = tabstripRef.current;
if (!el) return;
const atStart = el.scrollLeft <= 1;
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
el.dataset.fadeStart = String(!atStart);
el.dataset.fadeEnd = String(!atEnd && el.scrollWidth > el.clientWidth);
};
useEffect(() => {
const el = tabstripRef.current;
if (!el) return;
const onWheel = (e: WheelEvent) => {
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
el.scrollLeft += e.deltaY;
e.preventDefault();
}
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
useEffect(() => {
updateFade();
const el = tabstripRef.current;
el?.addEventListener('scroll', updateFade);
window.addEventListener('resize', updateFade);
return () => {
el?.removeEventListener('scroll', updateFade);
window.removeEventListener('resize', updateFade);
};
}, [dsps.length]);
useEffect(() => {
if (dsps.length > prevCount.current && tabstripRef.current) {
@ -78,7 +118,7 @@ function TopBar({
return (
<header className="topbar">
<div
<button
className="logo-slot"
onClick={() => {
setSelect(null);
@ -87,13 +127,14 @@ function TopBar({
aria-label="Go to home"
>
<AnimatedLogo />
</div>
</button>
<div className="tabstrip" ref={tabstripRef}>
{dsps.map((dsp) => (
<Tab
key={dsp.id}
dsp={dsp}
step={step}
active={selected === dsp.id && !(page == 'add-dsp')}
dimmed={page === 'add-dsp'}
onSelect={() => onSelect(dsp.id)}
@ -104,28 +145,28 @@ function TopBar({
<div className="tabstrip-actions">
{hasTabs ? (
<button className="tab-add" aria-label="Add DSP" onClick={onAddClick}>
<button className="tab-add tab-action" aria-label="Add DSP" onClick={onAddClick}>
<PlusIcon className="icon-tab-add" />
</button>
) : (
<button className="tab-add-empty" onClick={onAddClick}>
<button className="tab-add-empty tab-action" onClick={onAddClick}>
<PlusIcon className="icon-tab-add-empty" />
<span>Add DSP</span>
</button>
)}
</div>
<button
className="crown-action"
title="Credits & Support"
aria-label="Credits & Support"
onClick={() => {
setPage('credits');
setSelect(null);
}}
>
<button
className="crown-action tab-action"
title="Credits & Support"
aria-label="Credits & Support"
onClick={() => {
setPage('credits');
setSelect(null);
}}
>
<CrownIcon className="icon-crown" />
</button>
</div>
</header>
);
}