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

+208
View File
@@ -0,0 +1,208 @@
/*
* Textebene von PDF.js.
*
* Der Block unten ist **wörtlich** aus pdfjs-dist/web/pdf_viewer.css
* übernommen, nur die Klasse heißt `.pdf-textLayer` statt `.textLayer`.
*
* Beim ersten Anlauf hatte ich ihn von Hand gekürzt – „wir zeigen nur Seiten
* an, der Rest ist Werkzeugleiste". Dabei fielen drei Zeilen weg, die wie
* Kleinkram aussahen und keiner waren:
*
* --min-font-size / --text-scale-factor / scale(var(--min-font-size-inv))
*
* PDF.js misst beim ersten Aufbau, wie klein der Browser Schrift überhaupt
* noch darstellt (Chrome erzwingt eine Mindestgröße), setzt daraus
* `--min-font-size` und rechnet sie mit der Skalierung wieder heraus. Ohne
* dieses Paar wird jede kleine Schrift vom Browser aufgeblasen: Die
* unsichtbaren Textstücke sitzen dann größer da als die gemalten Buchstaben
* darunter – zu sehen erst beim **Markieren**, wo die Auswahl als grobe,
* versetzte Kästen über dem Text lag statt auf ihm.
*
* Deshalb hier nichts mehr von Hand kürzen. Was die Datei mitbringt, gehört
* zusammen; was wir zusätzlich wollen, steht darunter.
*/
.pdf-textLayer{
color-scheme:only light;
position:absolute;
text-align:initial;
inset:0;
overflow:clip;
opacity:1;
line-height:1;
letter-spacing:normal;
word-spacing:normal;
-webkit-text-size-adjust:none;
-moz-text-size-adjust:none;
text-size-adjust:none;
forced-color-adjust:none;
transform-origin:0 0;
caret-color:CanvasText;
z-index:0;
&.highlighting{
touch-action:none;
}
:is(span, br){
color:transparent;
position:absolute;
white-space:pre;
cursor:text;
transform-origin:0% 0%;
-webkit-user-select:text;
-moz-user-select:text;
user-select:text;
}
--min-font-size:1;
--text-scale-factor:calc(var(--total-scale-factor) * var(--min-font-size));
--min-font-size-inv:calc(1 / var(--min-font-size));
> :not(.markedContent),
.markedContent span:not(.markedContent){
z-index:1;
--font-height:0;
font-size:calc(var(--text-scale-factor) * var(--font-height));
--scale-x:1;
--rotate:0deg;
transform:rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv));
}
.markedContent{
display:contents;
}
span[role="img"]{
-webkit-user-select:none;
-moz-user-select:none;
user-select:none;
cursor:default;
}
.highlight{
--highlight-bg-color:rgb(180 0 170 / 0.25);
--highlight-selected-bg-color:rgb(0 100 0 / 0.25);
--highlight-backdrop-filter:none;
--highlight-selected-backdrop-filter:none;
@media screen and (forced-colors: active){
--highlight-bg-color:transparent;
--highlight-selected-bg-color:transparent;
--highlight-backdrop-filter:var(--hcm-highlight-filter);
--highlight-selected-backdrop-filter:var(
--hcm-highlight-selected-filter
);
}
margin:-1px;
padding:1px;
background-color:var(--highlight-bg-color);
backdrop-filter:var(--highlight-backdrop-filter);
border-radius:4px;
&.appended{
position:initial;
}
&.begin{
border-radius:4px 0 0 4px;
}
&.end{
border-radius:0 4px 4px 0;
}
&.middle{
border-radius:0;
}
&.selected{
background-color:var(--highlight-selected-bg-color);
backdrop-filter:var(--highlight-selected-backdrop-filter);
scroll-margin-top:50px;
}
}
::-moz-selection{
background:rgba(0 0 255 / 0.25);
background:color-mix(in srgb, AccentColor, transparent 50%);
color:transparent;
}
::selection{
background:rgba(0 0 255 / 0.25);
background:color-mix(in srgb, AccentColor, transparent 50%);
color:transparent;
}
&.selectionRendering{
::-moz-selection{
background:transparent;
color:transparent;
}
::selection{
background:transparent;
color:transparent;
}
}
br::-moz-selection{
background:transparent;
}
br::selection{
background:transparent;
}
.endOfContent{
display:block;
position:absolute;
inset:100% 0 0;
z-index:0;
cursor:default;
-webkit-user-select:none;
-moz-user-select:none;
user-select:none;
}
&.selecting .endOfContent{
top:0;
}
}
/*
* Fundstellen der Suche.
*
* Eine eigene Ebene über der Textebene, kein <mark> im Text. Warum, steht
* ausführlich in PdfViewer.tsx bei `paint` – kurz: Der Grund eines <mark>
* ragte oben aus der Textebene heraus, und die schneidet ab (`overflow:
* clip`). Das sah man als angeschnittene Streifen. Diese Ebene schneidet
* nichts ab; die Kästen sind dieselben, die der Browser beim Markieren mit
* der Maus malt.
*
* `pointer-events: none`, damit die Ebene das Markieren mit der Maus nicht
* abfängt – darunter liegt die Schrift, die man auswählen können soll.
*
* Die Farbe bleibt durchsichtig: Der lesbare Text steht auf dem gemalten Bild
* *darunter*. Mit 0,95 Deckkraft lag ein satter Block über dem Wort, und
* ausgerechnet das gesuchte Wort war das einzige, das man nicht mehr lesen
* konnte. Der Betrachter von PDF.js selbst arbeitet aus demselben Grund mit
* 0,25.
*/
.pdf-hitLayer {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
.pdf-hit {
position: absolute;
background: rgba(245, 158, 11, 0.3);
border-radius: 2px;
}
.pdf-hit.pdf-hit-current {
background: rgba(245, 158, 11, 0.55);
/* Der aktuelle Treffer bekommt zusätzlich eine Kante statt noch mehr Farbe –
Deckkraft würde wieder das Wort verschlucken. */
box-shadow: 0 0 0 1px rgba(180, 83, 9, 0.75);
}