diff --git a/web/app/src/hero.ts b/web/app/src/hero.ts index ebf2391..7471db8 100644 --- a/web/app/src/hero.ts +++ b/web/app/src/hero.ts @@ -83,6 +83,12 @@ export function hero( schriftzug.className = 'hero-schriftzug'; const titel = document.createElement('h1'); titel.className = 'hero-titel'; + + // Kommt der Schriftzug nicht an, tritt der Name an seine Stelle. + schriftzug.onerror = () => { + schriftzug.classList.add('hidden'); + titel.classList.remove('hidden'); + }; const meta = document.createElement('div'); meta.className = 'hero-meta'; const text = document.createElement('p'); @@ -95,19 +101,27 @@ export function hero( // **Statt des Knopfes, wenn es den Titel noch nicht gibt.** Ein Knopf, der // „Abspielen" sagt und eine Absage bringt, ist schlimmer als keiner. + // + // Hier einzeilig, anders als auf der Detailseite: zwei Zeilen wären höher + // als ein Knopf, und dann rückte beim Umblättern die ganze Reihe auf und ab. const bald = document.createElement('div'); - bald.className = 'verfuegbar-ab'; - const baldText = document.createElement('div'); - baldText.className = 'verfuegbar-text'; - const baldOben = document.createElement('strong'); - baldOben.textContent = 'Bald ist es soweit'; - const baldUnten = document.createElement('span'); - baldText.append(baldOben, baldUnten); + bald.className = 'verfuegbar-ab hero-bald'; + const baldText = document.createElement('span'); bald.append(iconElement(icons.uhr), baldText); + // **Ein Platz fester Breite für das, was vorne steht.** + // + // „Abspielen", „Fortsetzen ab 1:23:45" und der Termin sind verschieden + // breit. Standen sie im selben Fluss, wanderte „Mehr" bei jedem Umblättern + // mit — und das Karussell blättert von allein. Was sich ändert, soll den + // Rest in Ruhe lassen. + const haupt = document.createElement('div'); + haupt.className = 'hero-haupt'; + haupt.append(bald, spielen); + const knoepfe = document.createElement('div'); knoepfe.className = 'hero-knoepfe'; - knoepfe.append(bald, spielen, mehr); + knoepfe.append(haupt, mehr); inhalt.append(schriftzug, titel, meta, text, knoepfe); @@ -125,8 +139,29 @@ export function hero( return b; }); + // **Pfeile am Bildrand.** + // + // Die Punkte allein sind drei Pixel hoch und sagen nur, wo man steht — + // zum Blättern muss man sie treffen wollen. Die Pfeile sind gross, liegen + // immer an derselben Stelle und heißen, was sie tun. + // + // Am Telefon gibt es sie nicht: dort nähme ein Knopf über dem Bild ein + // Drittel der Breite weg, und die Geste heisst dort Wischen. + const pfeile = (['links', 'rechts'] as const).map(richtung => { + const p = document.createElement('button'); + p.type = 'button'; + p.className = `hero-pfeil hero-pfeil-${richtung}`; + p.innerHTML = icons[richtung]; + p.setAttribute('aria-label', richtung === 'links' ? 'Vorheriger Titel' : 'Nächster Titel'); + p.onclick = () => { zeige(jetzt + (richtung === 'links' ? -1 : 1)); anhalten(); }; + return p; + }); + wurzel.append(buehne, inhalt); - if (mitBild.length > 1) wurzel.appendChild(punkte); + if (mitBild.length > 1) { + wurzel.append(...pfeile, punkte); + wischenEinrichten(); + } let jetzt = 0; let takt: number | undefined; @@ -141,6 +176,13 @@ export function hero( }); punktknoepfe.forEach((b, n) => b.classList.toggle('an', n === jetzt)); + // **Der Name steht immer bereit**, auch wenn der Schriftzug gewinnt. + // + // Gemessen: fehlt das Logo im Bildspeicher — es kommt von TMDB und wird + // beim Auffrischen geholt —, lud das `` nicht, und weil der Titel + // daneben versteckt war, stand im Aufmacher ein kaputtes Bild und sonst + // nichts. Ein Name ist besser als ein Platzhalter. + titel.textContent = e.titel; const logo = bildAdresse(e.schriftzug); if (logo) { schriftzug.src = logo; @@ -148,8 +190,8 @@ export function hero( schriftzug.classList.remove('hidden'); titel.classList.add('hidden'); } else { + schriftzug.removeAttribute('src'); schriftzug.classList.add('hidden'); - titel.textContent = e.titel; titel.classList.remove('hidden'); } @@ -176,8 +218,10 @@ export function hero( spielen.classList.toggle('hidden', nochNicht); if (nochNicht) { - baldUnten.textContent = `Ab ${geplantAb!.toLocaleString('de-CH', - { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', + // Kurz geschrieben, weil es in eine Knopfzeile passen muss. Der ganze + // Satz steht auf der Detailseite. + baldText.textContent = `Ab ${geplantAb!.toLocaleString('de-CH', + { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' })} Uhr`; return; } @@ -206,6 +250,44 @@ export function hero( takt = window.setInterval(weiter, WECHSEL_MS); } + /** Wischen — die Geste, wo es keine Pfeile gibt. + * + * **Nur waagerecht, und erst ab einer Strecke.** Senkrecht blättert die + * Seite; fasste der Aufmacher jede Bewegung ab, käme man an ihm nicht mehr + * vorbei. Deshalb entscheidet die erste erkennbare Richtung, wem die Geste + * gehört — und unter der Schwelle gehört sie niemandem. + */ + function wischenEinrichten(): void { + const SCHWELLE = 48; + let start: { x: number; y: number } | null = null; + let waagerecht = false; + + wurzel.addEventListener('pointerdown', e => { + if (e.pointerType === 'mouse') return; + start = { x: e.clientX, y: e.clientY }; + waagerecht = false; + }); + + wurzel.addEventListener('pointermove', e => { + if (start === null) return; + const dx = e.clientX - start.x; + const dy = e.clientY - start.y; + if (!waagerecht && Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 12) waagerecht = true; + }); + + const beenden = (e: PointerEvent): void => { + if (start === null) return; + const dx = e.clientX - start.x; + start = null; + if (!waagerecht || Math.abs(dx) < SCHWELLE) return; + zeige(jetzt + (dx < 0 ? 1 : -1)); + anhalten(); + }; + + wurzel.addEventListener('pointerup', beenden); + wurzel.addEventListener('pointercancel', () => { start = null; }); + } + wurzel.addEventListener('mouseenter', anhalten); wurzel.addEventListener('mouseleave', starten); wurzel.addEventListener('focusin', anhalten); diff --git a/web/app/src/style.css b/web/app/src/style.css index 87a0079..2501034 100644 --- a/web/app/src/style.css +++ b/web/app/src/style.css @@ -1119,6 +1119,16 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } position: relative; padding: var(--s-page); max-width: 620px; + /* **Sonst drückt der Inhalt den Aufmacher auseinander.** Ein Flexkind + schrumpft von sich aus nicht unter seinen Inhalt; die Knopfzeile war + damit auf dem Telefon breiter als der Bildschirm, und `overflow: hidden` + schnitt die rechte Hälfte des Textes ab. */ + min-width: 0; + /* **Volle Breite, nicht die des längsten Satzes.** Als blosses Flexkind war + der Block so breit wie sein Inhalt — bei einer kurzen Inhaltsangabe + schmaler als bei einer langen. Auf dem Telefon rückte „Mehr" dadurch beim + Umblättern um fünfzig Pixel. */ + flex: 1 1 auto; display: flex; flex-direction: column; gap: var(--s-regular); } @@ -1143,9 +1153,40 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } overflow: hidden; } -.hero-knoepfe { display: flex; gap: var(--s-snug); margin-top: var(--s-tight); } +.hero-knoepfe { display: flex; gap: var(--s-snug); margin-top: var(--s-tight); min-width: 0; } .hero-knoepfe .btn { height: 40px; padding: 0 var(--s-section); gap: 6px; } +/* **Fester Platz für das, was vorne steht.** + „Abspielen", „Fortsetzen ab 1:23:45" und der Termin sind verschieden breit. + Ohne feste Breite wanderte „Mehr" bei jedem Umblättern mit — und das + Karussell blättert von allein. */ +.hero-haupt { flex: 0 0 258px; display: flex; } +.hero-haupt > * { flex: 1; } + +/* Einzeilig und knopfhoch: zwei Zeilen hoben die ganze Reihe an. */ +.hero-bald { + height: 40px; padding: 0 var(--s-regular); gap: var(--s-snug); + font-size: 13px; white-space: nowrap; overflow: hidden; min-width: 0; +} +.hero-bald .icon { flex: none; } +.hero-bald span:not(.icon) { overflow: hidden; text-overflow: ellipsis; } +.hero-bald .icon svg { width: 18px; height: 18px; } + +/* **Pfeile am Bildrand.** Senkrecht mittig, damit sie nicht über dem Text + liegen, und über allem anderen — sonst fängt der Textblock den Klick ab. */ +.hero-pfeil { + position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; + width: 44px; height: 44px; display: grid; place-items: center; + border: 0; border-radius: 50%; + background: rgba(11, 11, 13, .55); color: var(--text); + backdrop-filter: blur(6px); + opacity: .72; transition: opacity .15s ease, background .15s ease; +} +.hero-pfeil:hover { opacity: 1; background: rgba(11, 11, 13, .8); } +.hero-pfeil svg { width: 24px; height: 24px; } +.hero-pfeil-links { left: var(--s-regular); } +.hero-pfeil-rechts { right: var(--s-regular); } + .hero-punkte { position: absolute; right: var(--s-page); bottom: calc(var(--s-page) - 12px); display: flex; gap: 6px; @@ -1175,6 +1216,14 @@ body:has(.pl.pl-klein) .auswahlleiste { bottom: 92px; } } .hero-inhalt { padding: var(--s-loose); max-width: none; } .hero-punkte { right: var(--s-loose); bottom: calc(var(--s-loose) - 12px); } + /* Ein Knopf über dem Bild nähme hier ein Drittel der Breite. Gewischt + wird stattdessen — siehe `wischenEinrichten` in hero.ts. */ + .hero-pfeil { display: none; } + /* **Hier füllt der vordere Platz die Zeile.** Eine feste Breite plus „Mehr" + wäre breiter als ein Telefon; so steht „Mehr" am rechten Rand und damit + ebenso fest wie am Schreibtisch. */ + .hero-haupt { flex: 1 1 auto; min-width: 0; } + .hero-bald { font-size: 12px; padding: 0 var(--s-snug); } } /* --- Kontextmenü ---------------------------------------------------------