Files
Aether/frontend/src/components/PageHeader.tsx
T
marcandClaude Opus 5 8e27c703d2 fix(ui): eine Breite fuer Seitenleiste und Kopfleiste
Kopfleiste blendete den Menue-Knopf ab 720px aus, die Seitenleiste stand
aber erst ab 768px fest im Bild. Dazwischen lag ein Band von 48 Pixeln
ohne Menue und ohne Knopf dorthin -- ein aufgeklapptes Fold landet mit
rund 757px genau darin.

Die Grenze steht jetzt einmal in src/breakpoints.ts.

Dazu zwei Folgefehler derselben Sache:

* Der abdunkelnde Grund lag mit z-index 40 ueber der fest stehenden
  Leiste (die hat dort keinen). Per Randwisch aufgeschoben, war die
  Leiste danach sichtbar, aber taub. Er blendet sich ab 720px aus.
* Der Randwisch loest breit gar nicht mehr aus, und Aufklappen bei
  aufgeschobener Leiste setzt sie zurueck -- sonst schluckte die
  Zurueck-Geste einmal ins Leere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:33:08 +02:00

209 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ReactNode } from 'react'
import UserMenu, { useSidebarToggle } from './UserMenu'
import { wideEnough } from '../breakpoints'
/**
* Die Kopfleiste jeder Ansicht.
*
* Vorher gab es zwei Muster nebeneinander: Dateien, Papierkorb und
* Freigabe-Links hatten eine feste Leiste, „Geteilt mit mir", Admin und Profil
* dagegen eine große Überschrift im Scrollbereich. Beim Umschalten sprang
* dadurch der Inhalt – und schon zwischen den drei Leisten war die Höhe
* unterschiedlich, weil sie sich aus dem jeweils höchsten Bedienelement ergab.
*
* Deshalb hier eine feste Höhe statt einer gewachsenen: Die Leiste ist überall
* gleich hoch, egal was darin steht.
*/
export const headerHeight = 57
/**
* Ab dieser Breite passt alles in eine Zeile.
*
* Darunter wird umgebrochen statt gequetscht: Auf 390 px stand die Brotkrume
* als einzelner Strich da, das Suchfeld schob sich über die Sortierauswahl, der
* Ansichts-Umschalter war ganz verschwunden und der Nutzerbereich am rechten
* Rand abgeschnitten. Eine Zeile, die nicht umbrechen kann, quetscht – und
* ausgerechnet auf dem Gerät, mit dem man unterwegs einen Beleg sucht.
*
* Dieselbe Grenze schaltet die Seitenleiste um; sie steht deshalb in
* `breakpoints.ts` und nicht mehr hier. Re-exportiert, weil sie hier gesucht
* wird.
*/
export { wideEnough }
interface Props {
/** Strichsymbol, 18 × 18 – dasselbe wie in der Seitenleiste. */
icon?: ReactNode
title?: string
/** Nebenangabe in gedämpfter Schrift, etwa „12 items". */
meta?: ReactNode
/** Statt Symbol und Titel: eigener Inhalt links, z. B. die Brotkrumen. */
left?: ReactNode
/** Bedienelemente – sitzen rechts. */
children?: ReactNode
}
export default function PageHeader({ icon, title, meta, left, children }: Props) {
const openSidebar = useSidebarToggle()
return (
<div className="aether-header" style={{
// **Kein padding hier.** Die Abstände stehen samt und sonders im
// CSS-Block unten.
//
// Grund, gemessen: Ein Inline-Stil schlägt jede Klassenregel. Solange
// hier `padding: '0 14px'` und `paddingTop: 'var(--safe-top)'` standen,
// waren die Regeln der schmalen Ansicht wirkungslos – `padding-top`
// kam als 0px an statt als 8px, und die Titelzeile klebte am oberen
// Rand des Fensters. Im Browser fiel es auf, weil dort --safe-top 0 ist.
background: 'var(--color-surface)',
borderBottom: '1px solid var(--color-border)',
display: 'flex', alignItems: 'center', gap: 10,
flexShrink: 0,
minWidth: 0,
flexWrap: 'wrap',
}}>
{/* Die Seitenleiste wird auf dem Handy von hier geöffnet.
Vorher stand dafür eine **zweite** Leiste darüber, die nichts weiter
enthielt als dieses Symbol und den Namen der Cloud. Gemessen kostete
sie 57 der 915 Pixel Höhe – und der Name stand ohnehin schon in der
Seitenleiste, die sie öffnete. */}
{openSidebar && (
<button
className="aether-header-menu"
onClick={openSidebar}
title="Menu"
aria-label="Menu"
style={{
// Kein `display` hier: Das schaltet die Breite unten um, und ein
// Inline-Stil schlägt jede Klasse. Genau daran war der Knopf schon
// auf dem Desktop zu sehen, obwohl die Regel ihn ausblendet.
padding: 6, marginLeft: -6, borderRadius: 8, flexShrink: 0,
color: 'var(--color-text-muted)', background: 'none',
border: 'none', cursor: 'pointer',
}}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
)}
{left ?? (
<>
{icon && (
// strokeLinecap/-join hier statt an jedem Symbol: Beide Eigenschaften
// vererben sich, und ein Symbol ohne eigene Angabe übernimmt sie
// damit. Vorher hatten Papierkorb und Freigabe-Links eckige Enden,
// „Geteilt mit mir" runde – nebeneinander wirkt das unruhig.
<span style={{
display: 'flex', color: 'var(--color-text-muted)', flexShrink: 0,
strokeLinecap: 'round', strokeLinejoin: 'round',
}}>
{icon}
</span>
)}
{title && (
<span style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)', flexShrink: 0 }}>
{title}
</span>
)}
</>
)}
{meta && (
<span style={{
fontSize: 12, color: 'var(--color-text-muted)', minWidth: 0,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>
{meta}
</span>
)}
{/* Rechte Gruppe: erst die Bedienelemente der Ansicht, dann – durch eine
Trennlinie abgesetzt – der Nutzerbereich. Er steht bewusst in *jeder*
Ansicht an derselben Stelle; ein Zugang, den man je nach Seite
woanders sucht, ist keiner. */}
<div className="aether-header-actions" style={{
display: 'flex', alignItems: 'center', gap: 8, minWidth: 0,
}}>
{children}
</div>
{/* Der Nutzerbereich steht **außerhalb** der Bedienelemente.
Auf dem Handy gehört er in die Titelzeile nach rechts – dorthin, wo
man den eigenen Zugang sucht. Vorher stand er in der Reihe mit
„+ Folder" und „Upload" und sah dort aus wie ein weiterer Knopf für
eine Handlung, die er nicht ist.
Umgestellt wird über `order`, nicht über einen zweiten Baum: Zwei
Nutzermenüs nebeneinander hießen zwei Klappmenüs, von denen eines
immer versteckt ist. */}
<div className="aether-header-user" style={{
display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
}}>
{children && (
<span className="aether-header-divider" style={{
width: 1, height: 22, background: 'var(--color-border)',
margin: '0 2px', flexShrink: 0,
}} />
)}
<UserMenu />
</div>
<style>{`
/* Grundabstände. Oben kommt der Bereich der Statusleiste dazu –
in der installierten App ist die Kopfleiste die oberste Zeile. */
.aether-header {
min-height: ${headerHeight - 1}px;
padding: 0 14px;
padding-top: var(--safe-top);
}
/* Breit: erst die Bedienelemente der Ansicht, dann – durch eine
Trennlinie abgesetzt – der Nutzerbereich, ganz rechts. */
@media ${wideEnough} {
.aether-header-menu { display: none; }
.aether-header-actions { order: 2; margin-left: auto; }
.aether-header-user { order: 3; }
}
@media not all and ${wideEnough} {
/* Schmal braucht die Zeile echte Luft: Ohne sie stoßen Titel und
Nutzerbereich direkt an die Adressleiste des Browsers. */
.aether-header {
padding-top: calc(14px + var(--safe-top));
padding-bottom: 10px;
}
/* Schmal: Nutzerbereich noch in der Titelzeile ganz rechts, die
Bedienelemente darunter über die volle Breite.
Die Umstellung per order ist hier nicht Zierde: Im Baum steht der Nutzerbereich
**hinter** den Bedienelementen, und die nehmen sich schmal die
volle Breite. Ohne die Umstellung landete er dahinter – also in
einer dritten Zeile, allein und rechts. Genau so sah es aus. */
.aether-header-menu { display: flex; }
.aether-header-user { order: 1; margin-left: auto; }
.aether-header-divider { display: none; }
/* Nichts wird versteckt – man kommt an alles heran, es steht nur
untereinander. */
.aether-header-actions {
order: 2;
width: 100%;
flex-wrap: wrap;
}
/* Das Suchfeld nimmt die Zeile für sich – 220px feste Breite waren
der Grund, warum daneben nichts mehr passte. */
.aether-header-actions input[type="text"],
.aether-header-actions input:not([type]) { width: 100% !important; }
}
@media ${wideEnough} {
.aether-header { height: ${headerHeight - 1}px; padding-left: 20px; padding-right: 20px; }
}
`}</style>
</div>
)
}