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) {