Files
Aether/frontend/src/components/Skeleton.tsx
T
marcandClaude Opus 5 4208676aa9 fix(ui): Platzhalter blitzt auch beim Tab-Wechsel nicht mehr auf
Die Frist von 220 ms galt nur beim Wechsel *innerhalb* der Dateiansicht. Beim
Wechsel zwischen den Menüpunkten entsteht die Seite jedes Mal neu – damit war
jeder Tab-Wechsel ein „erstes Laden", und genau dort war die Regel „beim ersten
Mal sofort zeigen" falsch herum. Die Frist gilt jetzt immer; 220 ms Leere ist
das ruhigere Bild, man kommt ohnehin von einer anderen Seite.

Als gemeinsamer Haken `useSkeleton`, statt die Logik in fünf Seiten zu
wiederholen.

Dabei wäre beinahe ein schlimmerer Geist entstanden: Im Wartefenster hat kurz
der **Leerzustand** aufgeblitzt – „Nothing shared with you yet", eine
Zehntelsekunde bevor die Liste kam. Der Platzhalter sieht nur nach Daten aus,
ein Satz behauptet etwas. Das Wartefenster bleibt jetzt überall leer.

Dazu drei Reste der Kalender-Übersetzung, die mein Suchmuster übersehen hatte,
weil sie in Template-Strings standen: der Löschen-Dialog des Kalenders und der
Wochentag-Hinweis im Terminfenster. Und das Datum in der Listenansicht stand
noch im deutschen Format („18. September 2026") in einer englischen
Oberfläche – jetzt „September 18, 2026".

Geprüft: tsc sauber, 41 Vitest-Tests, alle 234 Prüfungen der sieben
Selbsttest-Suiten grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:15:45 +02:00

115 lines
4.0 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 { useEffect, useState } from 'react'
/**
* Platzhalter, solange geladen wird.
*
* Vorher stand dort ein Kreisel in der Mitte. Der sagt „warte", aber er sagt
* nicht, **worauf** – und wenn die Daten da sind, klappt das Layout um: Der
* Kreisel verschwindet, Kacheln erscheinen, die Seite springt. Ein Platzhalter
* in der Form des Kommenden tut beides besser. Er zeigt, was gleich da ist,
* und er hält den Platz, sodass nichts springt.
*
* **Die Maße sind abgeschrieben, nicht geschätzt** – aus FileViews.tsx: Kachel
* mit 12er Radius und 100 Pixel Deckfläche, Zeile mit 8/12er Innenabstand und
* 8er Radius. Weichen sie ab, springt es beim Wechsel wieder, und der ganze
* Zweck wäre dahin.
*/
/**
* Zeigen wir den Platzhalter **wirklich** – oder ist die Liste schneller da?
*
* Ohne Frist blitzt bei jedem Ordnerwechsel und bei jedem Tab-Wechsel kurz ein
* Feld grauer Kästen auf. Sie sehen aus wie Dateien, die sofort wieder
* verschwinden; Marc nannte sie „Geister", und das trifft es: Der Platzhalter
* soll Warten überbrücken, nicht Warten erfinden.
*
* **Die Frist gilt auch beim allerersten Laden.** Das war zuerst anders – ich
* hatte gedacht, beim ersten Mal gebe es ja nichts, was stehenbleiben könnte,
* also könne er sofort kommen. Beim Wechsel zwischen den Menüpunkten entsteht
* die Seite aber jedes Mal neu, und damit war *jeder* Tab-Wechsel ein „erstes
* Laden". Genau dort blitzte es dann. 220 ms Leere ist das ruhigere Bild: Man
* kommt ohnehin von einer anderen Seite, der Wechsel liest sich als Wechsel.
*
* 220 ms ist die Grenze, ab der ein Wechsel als „hakt" empfunden wird –
* darunter merkt niemand, dass überhaupt geladen wurde.
*/
export function useSkeleton(loading: boolean, frist = 220): boolean {
const [zeigen, setZeigen] = useState(false)
useEffect(() => {
if (!loading) { setZeigen(false); return }
const t = setTimeout(() => setZeigen(true), frist)
return () => clearTimeout(t)
}, [loading, frist])
return zeigen
}
/** Ein graues Feld, das leicht pulsiert. */
function Feld({ w, h, r = 6, mb }: { w: number | string; h: number; r?: number; mb?: number }) {
return (
<div
className="aether-skeleton"
style={{ width: w, height: h, borderRadius: r, marginBottom: mb }}
/>
)
}
/**
* Wie viele Platzhalter.
*
* Genug, um die sichtbare Fläche zu füllen, und nicht mehr: Ein Schirm voll
* grauer Kästen, der sich noch scrollen lässt, verspricht mehr Inhalt als da
* ist – und enttäuscht zweimal, wenn der Ordner am Ende drei Dateien hat.
*/
const kacheln = 8
const zeilen = 7
export function SkeletonGrid() {
return (
<div className="aether-grid" aria-hidden="true">
{Array.from({ length: kacheln }, (_, i) => (
<div
key={i}
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: 12,
overflow: 'hidden',
}}
>
<div style={{ height: 100 }}>
<Feld w="100%" h={100} r={0} />
</div>
<div style={{ padding: '8px 10px 10px' }}>
{/* Unterschiedlich lang, sonst sieht es aus wie ein Formular und
nicht wie eine Liste von Namen. */}
<Feld w={`${55 + ((i * 17) % 35)}%`} h={9} mb={6} />
<Feld w="38%" h={8} />
</div>
</div>
))}
</div>
)
}
export function SkeletonRows() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }} aria-hidden="true">
{Array.from({ length: zeilen }, (_, i) => (
<div
key={i}
style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '8px 12px', borderRadius: 8,
}}
>
<Feld w={28} h={28} r={6} />
<div style={{ flex: 1 }}>
<Feld w={`${35 + ((i * 23) % 40)}%`} h={10} />
</div>
<Feld w={64} h={8} />
</div>
))}
</div>
)
}