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>
209 lines
8.5 KiB
TypeScript
209 lines
8.5 KiB
TypeScript
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>
|
||
)
|
||
}
|