From 02978e482c2fb77a469ceba7032cc44c27f993c5 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Tue, 6 Oct 2026 22:05:44 +0200 Subject: [PATCH] =?UTF-8?q?Die=20Kn=C3=B6pfe=20bleiben,=20die=20Inhaltsang?= =?UTF-8?q?abe=20klappt=20auf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ich hatte „die Beschreibung nur beim Hovern" gelesen und daraufhin Beschreibung *und* Knöpfe ausgeblendet. Die Knöpfe gehören immer dahin: in Ruhe steht oben, was läuft und was man damit tun kann. Die Inhaltsangabe dagegen steht erst da, wenn man es will. Zwei Sekunden auf dem Titel stehenbleiben, dann fährt er hoch und darunter klappt sie auf; wer nur vorbeifährt, sieht nichts. Gibt es keine, passiert gar nichts — der Absatz fällt dann ganz aus dem Fluss. **Angefasst wird am Titel, losgelassen am ganzen Aufmacher.** Das ist die ganze Vorsicht an dieser Stelle. Beim Aufklappen wächst der Block nach oben, der Titel rückt um 77 Bildpunkte weg und steht nicht mehr unter dem Zeiger. Hinge das Zuklappen an seinem `:hover`, wäre das die erste Hälfte einer Schaukel. Der Aufmacher selbst kann das nicht: gemessen, offen wie zu, 476 px hoch ab 48 — auf den Bildpunkt derselbe Kasten. **Die Höhe wird gemessen, nicht geraten.** Nach `auto` lässt sich nicht überblenden, und eine feste Obergrenze wäre bei einer einzeiligen Angabe zu drei Vierteln Leerlauf. Gemessen wird bei jedem Folienwechsel und bei jeder Fensteränderung; `scrollHeight` ginge nicht, der meldet bei einem `-webkit-box` die ungeklammerte Höhe. **Der Rasterabstand wandert mit.** Gemessen: zugeklappt stehen zwischen Eckdaten und Knopfzeile 16 px — mit Inhaltsangabe wie ohne, und genau so viele wie vorher. Aufgeklappt sind es 12 über und 16 unter dem Text, und die Knopfzeile steht still (Unterkante 492 in beiden Zuständen). Nebenbei kommt das Abbruchsignal endlich an: `hero()` nahm eines entgegen, aber `main.ts` baute die Startseite ohne. Sie wird gemerkt — ihr Neun-Sekunden-Takt lief also weiter, während man längst woanders war. Co-Authored-By: Claude Opus 5 --- web/app/src/hero.ts | 102 ++++++++++++++++++++++++++++++++++++-- web/app/src/main.ts | 3 +- web/app/src/style.css | 66 ++++++++++++------------ web/app/src/views/home.ts | 7 ++- 4 files changed, 140 insertions(+), 38 deletions(-) diff --git a/web/app/src/hero.ts b/web/app/src/hero.ts index d385c3c..e00284b 100644 --- a/web/app/src/hero.ts +++ b/web/app/src/hero.ts @@ -204,7 +204,20 @@ export function hero( e.bewertung ? `★ ${e.bewertung.toFixed(1)}` : null, ].filter(Boolean).join(' · '); - text.textContent = e.beschreibung ?? ''; + // **Nichts zu zeigen heisst: kein Aufklappen.** Bei einer angefangenen + // Folge steht hier oft gar nichts, und bei „S2 · F5 — Der Pakt" wäre das + // Auffahren eine Bewegung um einer Zeile willen, die schon oben steht. + // Mit `.hidden` fällt der Absatz aus dem Fluss und nimmt seinen + // Rasterabstand mit; der Block steht dann bei jeder Folie gleich hoch. + // + // **Hier und nicht am Ende von `zeige`** — weiter unten steht ein `return` + // für noch nicht freigegebene Titel. + const beschreibung = e.beschreibung?.trim() ?? ''; + text.textContent = beschreibung; + text.classList.toggle('hidden', beschreibung.length === 0); + hatText = beschreibung.length > 0; + if (!hatText) zuklappen(); + anstossen(); mehr.onclick = () => oeffnen(e.art, e.id); // **Drei Fälle, und jeder sagt die Wahrheit.** @@ -290,11 +303,94 @@ export function hero( wurzel.addEventListener('pointercancel', () => { start = null; }); } + /* ---------- Die Inhaltsangabe klappt auf ---------- */ + + /** Wie lange der Zeiger auf dem Titel stehen muss. */ + const GEDULD_MS = 2000; + + let hatText = false; + let wartet: number | undefined; + let angemeldet = false; + + /** Misst die aufgeklappte Höhe und schreibt sie als Eigenschaft. + * + * **Kurz auf `auto` und wieder zurück.** Dazwischen wird nicht gezeichnet — + * beides liegt in derselben Aufgabe. `scrollHeight` ginge nicht: bei einem + * `-webkit-box` meldet der die *ungeklammerte* Höhe, bei sechs Zeilen also + * das Doppelte dessen, was je zu sehen ist. */ + function messen(): void { + if (!hatText) return; + const vorher = text.style.height; + text.style.height = 'auto'; + const hoehe = Math.round(text.getBoundingClientRect().height); + text.style.height = vorher; + inhalt.style.setProperty('--hero-texthoehe', `${hoehe}px`); + } + + /** Einmal je Bild, nicht einmal je Ereignis — wie `weichNachBildlauf`. */ + function anstossen(): void { + if (angemeldet) return; + angemeldet = true; + requestAnimationFrame(() => { angemeldet = false; messen(); }); + } + + function aufklappen(): void { + if (!hatText) return; + messen(); // Erst messen, dann schalten. + inhalt.classList.add('aufgeklappt'); + } + + function zuklappen(): void { + if (wartet !== undefined) { window.clearTimeout(wartet); wartet = undefined; } + inhalt.classList.remove('aufgeklappt'); + } + + // **Angefasst wird am Titel, losgelassen am ganzen Aufmacher.** + // + // Das ist die ganze Vorsicht an dieser Stelle. Klappt die Inhaltsangabe auf, + // wächst `.hero-inhalt` nach oben — der Titel rückt um zwanzig bis siebzig + // Bildpunkte weg und steht nicht mehr unter dem Zeiger. Hinge das Zuklappen + // an seinem `:hover`, wäre das die erste Hälfte einer Schaukel: weg vom + // Zeiger, zu, wieder unter den Zeiger, auf, und so fort im Takt der + // Überblendung. + // + // Der Aufmacher selbst kann das nicht: er ist `min(62vh, 560px)` hoch, fest. + // Sein Rechteck ändert sich beim Aufklappen um keinen Bildpunkt, ein Zeiger + // darin kann also nicht hinausgeschoben werden. + const anfassen = (e: PointerEvent): void => { + // Ohne Zeiger kein Verweilen — am Finger steht die Inhaltsangabe ohnehin + // die ganze Zeit da, siehe `hover: hover` im Regelwerk. + if (e.pointerType === 'touch') return; + if (!hatText || inhalt.classList.contains('aufgeklappt')) return; + window.clearTimeout(wartet); + wartet = window.setTimeout(() => { wartet = undefined; aufklappen(); }, GEDULD_MS); + }; + + /** Bricht nur das Warten ab, nie das Aufgeklappte. + * + * Ist es offen, ist dieses Verlassen gerade das, was das Aufklappen selbst + * verursacht hat. */ + const loslassen = (): void => { + if (wartet !== undefined) { window.clearTimeout(wartet); wartet = undefined; } + }; + + [schriftzug, titel].forEach(el => { + el.addEventListener('pointerenter', anfassen); + el.addEventListener('pointerleave', loslassen); + }); + + // Drei Zeilen sind bei 620 px Breite andere als bei 380. + window.addEventListener('resize', anstossen, { passive: true }); + wurzel.addEventListener('mouseenter', anhalten); - wurzel.addEventListener('mouseleave', starten); + wurzel.addEventListener('mouseleave', () => { starten(); zuklappen(); }); wurzel.addEventListener('focusin', anhalten); wurzel.addEventListener('focusout', starten); - signal?.addEventListener('abort', anhalten); + signal?.addEventListener('abort', () => { + anhalten(); + loslassen(); + window.removeEventListener('resize', anstossen); + }); zeige(0); starten(); diff --git a/web/app/src/main.ts b/web/app/src/main.ts index aa7c4e0..7c1d33c 100644 --- a/web/app/src/main.ts +++ b/web/app/src/main.ts @@ -478,7 +478,8 @@ function zeichnen(): void { void zaehlerAuffrischen(bekannt); }, signal)); } else { - einhaengen(weg, inhalt, () => startseite(mediatheken, oeffnen, alleZeigen), + einhaengen(weg, inhalt, + signal => startseite(mediatheken, oeffnen, alleZeigen, signal), { merken: true }); } diff --git a/web/app/src/style.css b/web/app/src/style.css index 086ec85..ccb6bdb 100644 --- a/web/app/src/style.css +++ b/web/app/src/style.css @@ -1218,45 +1218,44 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } .hero-bald span:not(.icon) { overflow: hidden; text-overflow: ellipsis; } .hero-bald .icon svg { width: 18px; height: 18px; } -/* **Inhaltsangabe und Knöpfe kommen erst, wenn man hinzeigt.** +/* **Die Inhaltsangabe klappt auf, wenn man auf dem Titel stehenbleibt.** * - * In Ruhe steht oben nur, *was* läuft: Schriftzug oder Name, Jahr, Laufzeit. - * Was man damit tun kann und worum es geht, erscheint beim Überfahren — so - * ist das Bild in Ruhe ein Bild und nicht eine Wand aus Text darüber. + * In Ruhe steht da, *was* läuft und *was man tun kann*: Schriftzug oder Name, + * Eckdaten — und die Knöpfe, immer. Worum es geht, steht nicht da und nimmt + * auch keinen Platz. Wer zwei Sekunden auf dem Titel stehenbleibt, bekommt + * es; wer vorbeifährt, nicht. * - * **Der Platz bleibt reserviert.** Verschwinden würden die Zeilen nur mit - * `display: none` — und dann spränge beim ersten Überfahren der ganze Block - * nach oben. Hier wird nur die Deckung geschaltet; die Höhe steht still. + * **Die zwei Sekunden stehen nicht hier, sondern in `hero.ts`.** Eine + * `transition-delay` käme auch dann zum Zug, wenn der Zeiger nur quer über den + * Titel streicht — die Verzögerung soll aber eine Absicht abwarten und nicht + * eine Bewegung aussitzen. Sie überlebt dadurch auch „Bewegung reduzieren": + * die globale Regel oben flacht die Überblendung ab, nicht die Geduld. * - * **Der Termin bleibt sichtbar**, deshalb `.btn` und nicht die ganze - * Knopfzeile. „Ab Di., 6. Okt., 18:00 Uhr" ist keine Handlung, sondern die - * Auskunft, warum hier nichts zu drücken ist — die darf nicht erst auf - * Zuruf kommen. (Technisch ginge es ohnehin nicht anders: ein Kind kann - * nicht undurchsichtiger sein als sein Elternteil.) + * **Die Höhe kommt gemessen aus `hero.ts`.** Nach `auto` lässt sich nicht + * überblenden, und eine feste Obergrenze wäre bei einer einzeiligen Angabe zu + * drei Vierteln Leerlauf: die Bewegung stünde nach 100 ms und liefe weitere + * 240 ms unsichtbar weiter. Gemessen wird bei jedem Folienwechsel und bei + * jeder Fensteränderung — drei Zeilen sind bei 620 px Breite andere als bei + * 380. * - * **Langsamer herein als hinaus**, 340 gegen 220 Millisekunden. Das - * Erscheinen soll man sehen, das Verschwinden nicht abwarten müssen. + * **Der Rasterabstand wandert mit.** `.hero-inhalt` setzt 12 px zwischen alle + * Kinder; eine Zeile ohne Höhe hätte trotzdem zwei davon um sich, und in Ruhe + * stünden 24 px zwischen Eckdaten und Knopfzeile statt 12. Der negative + * `margin-top` zieht sie ab und gibt sie im selben Takt zurück. Mit `padding` + * ginge das nicht — Polster lässt sich nicht wegkürzen. * - * `hover: hover` wie bei den Pfeilen: ohne Zeiger gäbe es kein Überfahren, - * und „Abspielen" wäre nicht mehr erreichbar. Dort steht alles wie immer. */ + * `hover: hover` wie bei den Pfeilen: wo es kein Überfahren gibt, gibt es auch + * kein Aufklappen — dort steht die Inhaltsangabe wie eh und je da. */ @media (hover: hover) { - .hero-text, - .hero-knoepfe .btn { - opacity: 0; pointer-events: none; - transition: opacity .22s ease; + .hero-text { + height: 0; + margin-top: calc(var(--s-regular) * -1); + transition: height .22s ease, margin-top .22s ease; } - /* **Der Fokus zählt wie der Zeiger.** Ein Knopf, der unsichtbar den Fokus - bekommt, ist eine Sackgasse: man drückt die Eingabetaste auf etwas, das - man nicht sieht. `pointer-events: none` hält nur den Zeiger ab, nicht - den Fokus — die Deckung muss es hier tun. Und wer mit der Tastatur - hineinspringt, soll dasselbe sehen wie der, der hinzeigt: nicht nur den - Knopf, sondern auch, worum es geht. */ - .hero:hover .hero-text, - .hero:focus-within .hero-text, - .hero:hover .hero-knoepfe .btn, - .hero:focus-within .hero-knoepfe .btn { - opacity: 1; pointer-events: auto; - transition: opacity .34s ease; + .hero-inhalt.aufgeklappt .hero-text { + height: var(--hero-texthoehe, 0px); + margin-top: 0; + transition: height .34s ease, margin-top .34s ease; } } @@ -1291,7 +1290,8 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } * den Zeiger ab, nicht den Fokus. */ @media (hover: hover) { .hero-pfeil { opacity: 0; pointer-events: none; } - /* Derselbe Takt wie Text und Knöpfe: langsamer herein, schneller hinaus. */ + /* Langsamer herein als hinaus: das Erscheinen soll man sehen, das + Verschwinden nicht abwarten müssen. */ .hero:hover .hero-pfeil, .hero:focus-within .hero-pfeil { opacity: .72; pointer-events: auto; diff --git a/web/app/src/views/home.ts b/web/app/src/views/home.ts index f2e6a33..e3810a2 100644 --- a/web/app/src/views/home.ts +++ b/web/app/src/views/home.ts @@ -21,6 +21,10 @@ export function startseite( bibliotheken: Bibliothek[], oeffnen: (art: Art, id: string) => void, alleZeigen: (b: Bibliothek) => void, + /** **Wird gebraucht, nicht nur mitgeführt.** Die Startseite wird gemerkt; + * ohne dieses Signal lief der Takt des Aufmachers weiter, während man + * längst woanders war, und seine Zuhörer am Fenster blieben hängen. */ + signal?: AbortSignal, ): HTMLElement { const wurzel = document.createElement('div'); wurzel.innerHTML = '
'; @@ -83,7 +87,8 @@ export function startseite( laenge: e.laenge, danach: () => void aufbauen(), }); - }); + }, + signal); if (aufmacher) wurzel.appendChild(aufmacher); if (weiter.items.length) {