Changeset e46ebaa in Klonkt for src/assets
- Timestamp:
- 08/20/2026 12:54:30 AM (3 weeks ago)
- Branches:
- main
- Children:
- 48481cd
- Parents:
- 598f090
- Location:
- src/assets
- Files:
-
- 2 edited
-
css/style.css (modified) (5 diffs)
-
js/mod/read.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r598f090 re46ebaa 2430 2430 } 2431 2431 2432 /* -------- AUTO (data-feed-view="auto") -------- 2433 Lezen op mobiel, Tijdlijn op desktop. GEEN JavaScript: de CSS kiest, dus geen 2434 resize-afhandeling en geen flikkering waarin je eerst de verkeerde weergave 2435 ziet. De tijdlijn is de terugval, dus boven de grens hoeft hier niets te 2436 staan -- die is vanzelf al zichtbaar. 2437 De :has(.feed-reader)-voorwaarde staat er om dezelfde reden als hierboven: 2438 zonder berichten is er geen leessectie, en dan moet de tijdlijn blijven staan 2439 met "nog niets geschreven" erin. */ 2440 @media (max-width: 767px) { 2441 body.on-home[data-feed-view="auto"] .feed-reader { display: block; } 2442 body.on-home[data-feed-view="auto"]:has(.feed-reader) .feed-timeline, 2443 body.on-home[data-feed-view="auto"]:has(.feed-reader) .feed-grid, 2444 body.on-home[data-feed-view="auto"]:has(.feed-reader) .pagination, 2445 body.on-home[data-feed-view="auto"]:has(.feed-reader) .related-posts, 2446 body.on-home[data-feed-view="auto"]:has(.feed-reader) .article-footer { 2447 display: none !important; 2448 } 2449 } 2450 /* In de auto-stand staan BEIDE knoppen in de HTML en laat de mediaquery er een 2451 zien. Zo verspringt de naam van de knop niet terwijl je ernaar kijkt, en is er 2452 geen JavaScript nodig dat op resize moet luisteren. */ 2453 .view-switch-btn.vs-alleen-mobiel { display: none; } 2454 @media (max-width: 767px) { 2455 .view-switch-btn.vs-alleen-mobiel { display: inline-flex; } 2456 .view-switch-btn.vs-alleen-desktop { display: none; } 2457 } 2458 2432 2459 /* Snappen doet de SCROLLCONTAINER, en dat is het document -- niet .read-stream, 2433 2460 die heeft overflow: visible en scrollt zelf niet. Proximity en geen mandatory: 2434 2461 in een bericht dat langer is dan het scherm moet je rustig door kunnen lezen. */ 2435 html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: y proximity; } 2462 html:has(body.on-home[data-feed-view="reader"]) { 2463 scroll-snap-type: y proximity; 2464 /* Vloeiend, niet in een klap. Zonder dit springt een snap er in EEN frame 2465 naartoe en voelt het als een hik in plaats van als meegeven -- Barts 2466 melding (20-8). Geldt ook voor de terug-naar-boven-knop en voor een 2467 ankersprong, en dat is precies wat je wilt. 2468 Uitgezet bij prefers-reduced-motion, onderaan dit blok. */ 2469 scroll-behavior: smooth; 2470 } 2436 2471 /* En de browser niet óók laten compenseren als Load more er berichten bij zet: 2437 2472 Chrome en Firefox schuiven bij invoegen zelf mee (scroll anchoring), en dat … … 2441 2476 2442 2477 .feed-reader .read-post { 2478 /* Snap naar de BOVENkant van een bericht, zacht (proximity op html) en nooit 2479 dwingend (stop: normal). Een korte post vult het scherm, zodat hij niet als 2480 snipper tussen twee lijnen hangt. 2481 De uitzondering staat NIET hier maar in mod/read.js: een bericht dat langer 2482 is dan het scherm krijgt scroll-snap-align:none, anders trekt de browser je 2483 tijdens het lezen terug naar zijn bovenrand en lijkt het of het scrollen 2484 stopt. "Past het op het scherm?" is een vraag over gemeten hoogte, en die 2485 kan CSS niet stellen. */ 2486 /* Je LANDT op de bovenkant van een bericht -- dat is wat "snap to post" 2487 hoort te betekenen. (align:end probeerde ik ook; dan land je op de 2488 ONDERKANT van het vorige bericht, en bij een lang bericht zie je dus zijn 2489 laatste schermvol in plaats van de kop van het volgende.) 2490 Wanneer die bovenkant een snappunt IS, staat in mod/read.js: alleen zolang 2491 je er nog niet voorbij bent. Eenmaal in een bericht snapt er niets meer, 2492 ook niet naar zijn eigen bovenrand -- anders trekt proximity je terug 2493 zodra je een paar regels verder leest. */ 2443 2494 scroll-snap-align: start; 2444 2495 scroll-snap-stop: normal; 2445 min-height: 100svh; 2496 /* GEEN min-height meer. Die rekte elk bericht tot een vol scherm, en bij een 2497 kort bericht leverde dat een halve pagina leegte op onder "Replies and 2498 reactions" -- je zat naar niets te kijken en moest verder scrollen om te 2499 ontdekken dat er nog iets kwam. Robins vondst (20-8), met een 2500 schermafbeelding erbij. 2501 Zonder die hoogte is de stroom gewoon bericht na bericht: je ziet het 2502 volgende meteen onder het vorige beginnen. Het snappen doet nog steeds 2503 zijn werk aan de grens -- dat hangt aan scroll-snap-align en niet aan de 2504 hoogte -- dus de streep lijnt nog altijd uit met de bovenkant van het 2505 volgende bericht. */ 2446 2506 max-width: 46rem; 2447 2507 margin: 0 auto; … … 2461 2521 scrollen om te zien wat het IS. */ 2462 2522 .feed-reader .read-cover { 2463 /* Krimpt om het beeld heen en staat gecentreerd, in plaats van de kolom vol 2464 te trekken. Anders staat een klein plaatje in een veel te grote lijst. */ 2465 width: fit-content; 2466 max-width: 100%; 2467 margin: 0 auto 1.2rem; 2523 /* Volle kolombreedte: de afbeelding erin neemt die breedte, dus de figure 2524 hoeft niet meer om het beeld te krimpen. */ 2525 width: 100%; 2526 margin: 0 0 1.2rem; 2468 2527 border-radius: 10px; 2469 2528 overflow: hidden; … … 2472 2531 .feed-reader .read-cover video { 2473 2532 display: block; 2474 /* GEEN width:100%. Dat blies een cover van 221x165 op tot 505x378 -- ruim 2475 twee keer zo groot als hij is, en dus wazig. De tijdlijn deed dat nooit: 2476 daar zat een cover in een vakje van 120px, dus klein bleef klein. Met 2477 auto + max-width vult een grote afbeelding de kolom en blijft een kleine 2478 op ware grootte staan. 2479 En GEEN object-fit:cover: samen met de hoogtegrens sneed dat de onderkant 2480 eraf (553x464 kwam er als 505x400 uit). Met max-width en max-height houdt 2481 de browser de verhouding zelf en past het beeld HEEL in het vak. */ 2482 width: auto; 2533 /* Neemt de VOLLE kolombreedte, met de verhouding intact (object-fit 2534 weggelaten, dus geen bijsnijden). Een cover die van zichzelf kleiner is 2535 wordt netjes tot de kolom opgeschaald in plaats van als postzegeltje te 2536 blijven staan -- dat was te klein. De hoogtegrens houdt een staande foto 2537 nog binnen de perken zodat titel + eerste alinea boven de vouw blijven; 2538 hij krimpt dan mee in plaats van bijgesneden te worden. */ 2539 display: block; 2540 width: 100%; 2483 2541 height: auto; 2484 max- width: 100%;2485 max-height: 45svh;2542 max-height: 70svh; 2543 object-fit: contain; 2486 2544 /* Een logo met doorzichtige achtergrond zweeft anders op de paginakleur; 2487 2545 dezelfde keuze als bij .post-list-cover in de tijdlijn. */ … … 2506 2564 .feed-reader .read-open:focus-visible { opacity: 1; } 2507 2565 2566 /* Terug naar boven. Rechtsonder, want daar zit de duim -- en boven de tabbalk, 2567 die op mobiel 1050 is en anders overheen zou vallen. Onzichtbaar tot je ver 2568 genoeg bent, zodat hij niet meteen in de weg staat bij het eerste bericht. */ 2569 .read-top { 2570 position: fixed; 2571 right: max(1rem, env(safe-area-inset-right, 0)); 2572 bottom: calc(env(safe-area-inset-bottom, 0) + 4.75rem); 2573 z-index: 1040; 2574 display: grid; 2575 place-items: center; 2576 width: 44px; 2577 height: 44px; /* 44px = de kleinste maat die een duim betrouwbaar raakt */ 2578 border-radius: 999px; 2579 border: 1px solid var(--rule); 2580 background: color-mix(in srgb, var(--paper) 88%, transparent); 2581 backdrop-filter: blur(14px) saturate(140%); 2582 -webkit-backdrop-filter: blur(14px) saturate(140%); 2583 color: var(--ink); 2584 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink) 12%, transparent); 2585 cursor: pointer; 2586 opacity: 0; 2587 pointer-events: none; 2588 transition: opacity .18s ease, transform .18s ease; 2589 transform: translateY(6px); 2590 } 2591 .read-top.is-zichtbaar { opacity: 1; pointer-events: auto; transform: none; } 2592 /* Buiten de leesweergave bestaat hij niet -- home.ejs rendert alle drie de 2593 secties, dus zonder deze regel zweeft hij ook boven Tijdlijn en Grid. */ 2594 body:not([data-feed-view="reader"]) .read-top, 2595 body:not(.on-home) .read-top { display: none; } 2596 /* Op desktop is er geen tabbalk, dus mag hij lager staan. */ 2597 @media (min-width: 768px) { 2598 .read-top { bottom: 1.5rem; right: 1.5rem; } 2599 } 2508 2600 @media (prefers-reduced-motion: reduce) { 2509 html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: none; } 2601 .read-top { transition: none; transform: none; } 2602 } 2603 2604 @media (prefers-reduced-motion: reduce) { 2605 html:has(body.on-home[data-feed-view="reader"]) { 2606 scroll-snap-type: none; 2607 scroll-behavior: auto; 2608 } 2510 2609 } 2511 2610 -
src/assets/js/mod/read.js
r598f090 re46ebaa 17 17 * cmd/ctrl-klik hoort de browser zelf af te handelen. 18 18 */ 19 20 /** 21 * Snappen alleen waar het HELPT. 22 * 23 * Elk bericht heeft scroll-snap-align:start en min-height:100svh. Voor een kort 24 * bericht is dat precies goed: het vult het scherm en de volgende klikt netjes 25 * op zijn plek. Voor een LANG bericht is het een val -- je leest naar beneden en 26 * de browser trekt je terug naar de bovenrand, waardoor het scrollen lijkt te 27 * stoppen terwijl je gewoon doorscrollt. Barts melding (20-8). 28 * 29 * "Past dit bericht op het scherm?" is een vraag over gemeten hoogte, en die kan 30 * CSS niet stellen -- vandaar hier. Past het niet, dan gaat de snap eraf en lees 31 * je ononderbroken door tot het volgende bericht wel weer een snappunt is. 32 * 33 * De marge van 4px vangt afrondingsverschillen tussen svh en de echte hoogte; 34 * zonder die speling wipt een bericht dat toevallig exact past heen en weer. 35 */ 36 /** 37 * Wanneer is de bovenkant van een bericht een snappunt? 38 * 39 * Alleen zolang je er nog NIET voorbij bent. Zit de bovenrand op of onder de 40 * bovenkant van het scherm, dan kom je er nog aan en mag hij vangen. Is hij 41 * eenmaal voorbij -- je leest in het bericht -- dan gaat de snap eraf. 42 * 43 * Robins formulering (20-8), en die is preciezer dan wat ik er eerst van maakte: 44 * "in de post zelf mag er nooit gesnapt worden, ook niet bovenin; enkel de 45 * onderkant mag snappen naar de volgende post, dus daar de bovenkant van". 46 * Precies dat: de onderkant van bericht N is de bovenkant van N+1, en die is een 47 * doelwit omdat je hem nadert. De bovenkant van het bericht waar je IN zit is 48 * dat niet meer, want daar ben je voorbij. 49 * 50 * Zonder deze regel trok proximity je terug naar de bovenrand zodra je een paar 51 * regels verder scrolde, en leek het of het scrollen vastliep. 52 * 53 * De 4px is speling voor afronding: tijdens het snappen zelf loopt top naar 0 en 54 * mag hij niet halverwege afhaken. 55 */ 56 const SPELING = 4; 57 58 /** 59 * En snappen doet alleen mee op de weg NAAR BENEDEN (Robin, 20-8). 60 * 61 * Omhoog scroll je om iets terug te zoeken, en dan is elke vangst een hindernis: 62 * je wilt zelf bepalen waar je stopt. Omlaag lees je door, en dan helpt de 63 * grens juist. Dus bij omhoog gaat de snap er overal af. 64 * 65 * De drempel van 2px is er tegen richtingsruis: tijdens een vloeiende snap 66 * schommelt scrollY een fractie, en zonder speling zou de richting dan heen en 67 * weer klappen -- precies midden in de beweging die net soepel moest zijn. 68 */ 69 let laatsteY = 0; 70 let omlaag = true; 71 72 /** 73 * Hoe DICHT bij de grens hij vangt: de onderste VANGZONE pixels van een bericht. 74 * 75 * In pixels, en bewust niet inhoudelijk. Ik probeerde het aan de voet van het 76 * bericht te hangen ("ben je de reacties voorbij"), maar bij een KORT bericht 77 * gaat dat mis: min-height rekt zo'n bericht tot een vol scherm, dus de voet 78 * staat middenin met lege ruimte eronder en de zone begint op een willekeurige 79 * plek. Robin ving dat (20-8) voordat het uitgerold stond. 80 * 81 * Eerst stond dit op 20px en dat was te krap om ooit te vangen. Honderdvijftig 82 * is ruwweg het laatste stukje van een bericht: kom je daarbinnen tot stilstand, 83 * dan trekt hij de streep recht naar de bovenkant van het volgende. Stop je 84 * eerder, dan blijf je gewoon staan. 85 * 86 * Eén getal, dus makkelijk bij te stellen als het onder een duim anders voelt. 87 */ 88 const VANGZONE = 150; 89 90 function ijkEen(art) { 91 const top = art.getBoundingClientRect().top; 92 // Omhoog: nooit. Omlaag: alleen een grens die je NADERT en die binnen de zone 93 // ligt. De ondergrens blijft krap (SPELING): eenmaal voorbij wordt er niet 94 // teruggetrokken, ook niet over een paar pixels. 95 const wil = (!omlaag || top < -SPELING || top > VANGZONE) ? 'none' : ''; 96 // Alleen aanraken als het echt verandert: elke stijlwijziging tijdens een 97 // vloeiende scroll is een kans op een hik, zeker op iOS. 98 if (art.style.scrollSnapAlign !== wil) art.style.scrollSnapAlign = wil; 99 } 100 101 function ijkSnappen() { 102 const y = window.scrollY; 103 if (Math.abs(y - laatsteY) > 2) omlaag = y > laatsteY; 104 laatsteY = y; 105 document.querySelectorAll('.feed-reader .read-post').forEach(ijkEen); 106 } 107 108 /** 109 * Terug naar boven, en bewust NIET window.scrollTo(0). 110 * 111 * In een stroom wil je terug naar het BEGIN VAN DIT BERICHT als je halverwege een 112 * lang stuk zit, en pas daarna naar de kop van de pagina. Twee keer drukken doet 113 * dus twee verschillende dingen -- dat scheelt op mobiel een halve minuut vegen. 114 */ 115 function naarBoven() { 116 const zacht = !window.matchMedia('(prefers-reduced-motion: reduce)').matches; 117 const gedrag = zacht ? 'smooth' : 'auto'; 118 const posts = [...document.querySelectorAll('.feed-reader .read-post')]; 119 const huidig = posts.find((a) => { 120 const r = a.getBoundingClientRect(); 121 return r.top <= 8 && r.bottom > 8; 122 }); 123 // Sta je al bovenaan dit bericht (of bij het eerste), dan naar de paginakop. 124 if (huidig && huidig.getBoundingClientRect().top < -8) { 125 huidig.scrollIntoView({ behavior: gedrag, block: 'start' }); 126 return; 127 } 128 window.scrollTo({ top: 0, behavior: gedrag }); 129 } 130 131 /** De knop verschijnt pas als er iets ONDER je ligt om naar terug te keren. */ 132 function toonKnop(knop) { 133 knop.classList.toggle('is-zichtbaar', window.scrollY > window.innerHeight * 0.6); 134 } 19 135 20 136 let tapX = 0, tapY = 0; … … 37 153 } 38 154 155 let knop = null; 156 let opScroll = null; 157 39 158 export function init() { 40 159 const s = document.getElementById('read-stream'); … … 47 166 s.addEventListener('pointerdown', onPointerDown, { passive: true }); 48 167 s.addEventListener('click', onTap); 168 169 // De knop staat in de HTML (read-top.ejs), zodat hij er ook is zonder deze 170 // module -- dan doet hij niets, maar hij springt niet in beeld bij het laden. 171 knop = document.getElementById('read-top'); 172 if (knop) { 173 knop.onclick = naarBoven; 174 toonKnop(knop); 175 } 176 177 // Eén luisteraar, en losmaken bij een volgende init(): deze module draait bij 178 // ELKE paginawissel en anders stapelen ze op. 179 if (opScroll) { 180 window.removeEventListener('scroll', opScroll); 181 window.removeEventListener('resize', opScroll); 182 } 183 // Eén keer per frame, niet per scroll-event: scroll vuurt tientallen keren per 184 // seconde en we raken hier de stijl van elk bericht aan. 185 let gepland = false; 186 opScroll = () => { 187 if (knop) toonKnop(knop); 188 if (gepland) return; 189 gepland = true; 190 requestAnimationFrame(() => { gepland = false; ijkSnappen(); }); 191 }; 192 window.addEventListener('scroll', opScroll, { passive: true }); 193 window.addEventListener('resize', opScroll, { passive: true }); 194 ijkSnappen(); // ook meteen bij binnenkomst, niet pas bij de eerste scroll 49 195 } 50 196
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)