feat(aether): initial commit – self-hosted Cloud, Phasen 1–7

Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 5 committed 2026-08-24 09:25:36 +02:00
commit 31a027c93a
225 files changed
+38042

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
import type { ReactNode } from 'react'
import UserMenu, { useSidebarToggle } from './UserMenu'
/**
* 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.
*/
export const wideEnough = '(min-width: 720px)'
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={{
// Feste Höhe nur auf breiten Schirmen (siehe CSS unten). Auf dem Handy
// wächst die Leiste auf zwei Zeilen, statt ihren Inhalt zu verstecken.
padding: '0 14px',
// Statusleiste der installierten App. Die Kopfleiste ist jetzt die
// oberste Zeile der Anwendung – vorher lag darüber noch eine zweite.
paddingTop: 'var(--safe-top)',
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>{`
.aether-header { min-height: ${headerHeight - 1}px; }
/* 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} {
.aether-header {
padding-top: calc(8px + var(--safe-top));
padding-bottom: 8px;
}
/* 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>
)
}