Changeset 7cf8144 in Klonkt for src/assets
- Timestamp:
- 08/18/2026 06:47:58 PM (3 weeks ago)
- Branches:
- main
- Children:
- 9da8027
- Parents:
- 2d2fd8c
- git-author:
- Bart <bart@…> (08/18/2026 05:28:31 AM)
- git-committer:
- Bart <bart@…> (08/18/2026 06:47:58 PM)
- Location:
- src/assets
- Files:
-
- 2 edited
-
css/style.css (modified) (3 diffs)
-
js/mod/read.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/css/style.css
r2d2fd8c r7cf8144 2353 2353 padding: .35rem 0; 2354 2354 } 2355 .view-switch-btn, 2356 .view-switch-link { 2355 .view-switch-btn { 2357 2356 display: inline-flex; 2358 2357 align-items: center; … … 2369 2368 transition: all .15s; 2370 2369 } 2371 .view-switch-btn:hover, 2372 .view-switch-link:hover { 2370 .view-switch-btn:hover { 2373 2371 color: var(--ink); 2374 2372 border-color: var(--ink-muted); … … 2400 2398 body.on-home[data-feed-view="grid"] .feed-grid, 2401 2399 body.on-cirkel[data-feed-view="grid"] .feed-grid { display: block; } 2400 2401 /* -------- LEESWEERGAVE (data-feed-view="reader") -------- 2402 Dezelfde berichten als de tijdlijn, maar heel: één per scherm, grenzen door 2403 scroll-snapping. GEEN eigen route en geen eigen shell -- de balken blijven 2404 staan, want je bent nog op de feed en moet terug kunnen schakelen. 2405 Pagina's zonder .feed-reader (cirkel, archief, post) negeren de keuze en 2406 houden hun tijdlijn: er is hieronder simpelweg geen regel die die daar 2407 verbergt. Zo is Lezen nooit een leeg scherm. */ 2408 .feed-reader { display: none; } 2409 body.on-home[data-feed-view="reader"] .feed-reader { display: block; } 2410 /* :has(.feed-reader) is hier de LEGE STAAT, niet een truc. Zonder berichten 2411 rendert de leessectie niet, en dan mag de tijdlijn niet verborgen worden -- 2412 want dáár staat "nog niets geschreven" met de knop om te beginnen. Zonder deze 2413 voorwaarde is Lezen op een verse site een wit scherm. Hetzelfde geldt voor 2414 feeds die (nog) geen leessectie hebben. */ 2415 body.on-home[data-feed-view="reader"]:has(.feed-reader) .feed-timeline, 2416 body.on-home[data-feed-view="reader"]:has(.feed-reader) .feed-grid, 2417 body.on-home[data-feed-view="reader"]:has(.feed-reader) .pagination, 2418 body.on-home[data-feed-view="reader"]:has(.feed-reader) .related-posts, 2419 body.on-home[data-feed-view="reader"]:has(.feed-reader) .article-footer { 2420 display: none !important; 2421 } 2422 2423 /* Snappen doet de SCROLLCONTAINER, en dat is het document -- niet .read-stream, 2424 die heeft overflow: visible en scrollt zelf niet. Proximity en geen mandatory: 2425 in een bericht dat langer is dan het scherm moet je rustig door kunnen lezen. */ 2426 html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: y proximity; } 2427 /* En de browser niet óók laten compenseren als Load more er berichten bij zet: 2428 Chrome en Firefox schuiven bij invoegen zelf mee (scroll anchoring), en dat 2429 vecht met het snappen. */ 2430 html:has(body.on-home[data-feed-view="reader"]), 2431 .feed-reader .read-stream { overflow-anchor: none; } 2432 2433 .feed-reader .read-post { 2434 scroll-snap-align: start; 2435 scroll-snap-stop: normal; 2436 min-height: 100svh; 2437 max-width: 46rem; 2438 margin: 0 auto; 2439 /* De onderrand loopt achter de vaste tabbalk door; deze padding houdt de 2440 TEKST erboven. */ 2441 padding: 2rem 1.2rem 4rem; 2442 box-sizing: border-box; 2443 } 2444 /* Een lijn tussen twee berichten: zonder grens is een stroom een lange brij. */ 2445 .feed-reader .read-post + .read-post { border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent); } 2446 2447 .feed-reader .read-head { padding-bottom: .5rem; } 2448 .feed-reader .read-title { font-size: clamp(1.6rem, 4vw, 2.4rem); line-height: 1.15; margin: 0 0 .3rem; } 2449 /* De titel is een link, maar mag er niet als een link uitzien: dit is een 2450 leesscherm, geen index. */ 2451 .feed-reader .read-title a { color: inherit; text-decoration: none; } 2452 .feed-reader .read-title a:hover, 2453 .feed-reader .read-title a:focus-visible { text-decoration: underline; } 2454 .feed-reader .read-when { font-size: .85rem; opacity: .6; margin: 0; } 2455 2456 .feed-reader .read-foot { margin-top: 2rem; } 2457 .feed-reader .read-open { 2458 display: inline-block; font-size: .9rem; opacity: .75; 2459 text-decoration: none; border-bottom: 1px solid currentColor; padding-bottom: 1px; 2460 } 2461 .feed-reader .read-open:hover, 2462 .feed-reader .read-open:focus-visible { opacity: 1; } 2463 2464 @media (prefers-reduced-motion: reduce) { 2465 html:has(body.on-home[data-feed-view="reader"]) { scroll-snap-type: none; } 2466 } 2402 2467 2403 2468 /* -------- GRID VIEW TILES (Instagram-stijl) -------- */ -
src/assets/js/mod/read.js
r2d2fd8c r7cf8144 1 1 /** 2 * De leesweergave: een stroom van hele berichten, één per beeld.2 * De leesweergave: tikken op een bericht opent dat bericht. 3 3 * 4 * NIET de truc van bartoverkamp.nl. Daar springt de pagina bij de rand naarde5 * andere kant en wordt het nieuwe artikel "vloeiend" in beeld gezet -- een naad6 * die verborgen wordt. Hier groeit de stroom in de richting waarin je leest:7 * bij de onderrand komt de oudere buur eronder te staan, bij de bovenrandde8 * nieuwere erboven. De browser snapt naar de berichtgrenzen (CSS, proximity).9 * Er is dan geen naad, dus ook niets te verbergen.4 * Dit was een heel scherm met een eigen route, dat zijn buren zelf ophaalde, de 5 * scrollpositie corrigeerde bij invoegen en de balken wegschoof. Dat is allemaal 6 * weg, en dat is winst: Lezen is nu een DERDE WEERGAVE van de feed 7 * (body[data-feed-view="reader"]), naast Tijdlijn en Grid. De feed levert de 8 * berichten al, "meer laden" vult al aan, en het snappen naar berichtgrenzen 9 * doet CSS. Wat overblijft is dit ene gemak. 10 10 * 11 * Elk artikel draagt zijn eigen buren in data-attributen. Wat er nog te halen 12 * valt staat dus altijd op het stuk dat er al is, en deze module hoeft geen 13 * lijst bij te houden die uit de pas kan lopen. 11 * De titel en de voetlink in read-article.ejs zijn echte <a>'s en doen het werk 12 * voor toetsenbord en schermlezer; dit hier is er voor een duim. 14 13 * 15 * De namen hieronder zijn Engels, zoals overal in assets/js/mod. Het commentaar 16 * blijft Nederlands, zoals overal in deze repo. 14 * Vier uitzonderingen, want een tik die je niet bedoelde is erger dan geen tik: 15 * iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen werking, 16 * een geselecteerde tekst is geen tik, een verschoven vinger is scrollen, en 17 * cmd/ctrl-klik hoort de browser zelf af te handelen. 17 18 */ 18 19 19 // Hoeveel schermen vooruit we alvast halen. Eén is genoeg: je leest niet20 // sneller dan je scrolt, en meer betekent alleen meer verkeer.21 const ROOT_MARGIN = '1px 0px 100% 0px';22 23 // De teksten die dit script plaatst. Ze staan in data-i18n op de stroom, want24 // een zin die hier staat, staat in één taal -- en dat is nooit de taal van de25 // lezer. Zelfde afspraak als de topnav (mod/chrome.js).26 let strings = {};27 function str(key) { return strings[key] || ''; }28 29 const stream = () => document.getElementById('read-stream');30 const loaded = new Set(); // slugs die al in de stroom staan31 let busyBelow = false;32 let busyAbove = false;33 let observer = null;34 let lastY = 0;35 36 // Niet `chrome`: dat is in deze browser ook een global, en een module-scope die37 // window.chrome overschaduwt is een val voor de volgende lezer.38 function toggleChrome(hide) {39 document.body.classList.toggle('read-chrome-hidden', !!hide);40 }41 42 /** Het artikel dat nu het meest in beeld is; die bepaalt titel en URL. */43 function inView() {44 const posts = [...document.querySelectorAll('.read-post')];45 const middle = window.innerHeight / 2;46 return posts.find((p) => {47 const r = p.getBoundingClientRect();48 return r.top <= middle && r.bottom >= middle;49 }) || posts[0];50 }51 52 /** Titel en adres volgen wat je leest. replaceState: je bladert, je stapelt niet. */53 function syncAddress() {54 const p = inView();55 if (!p) return;56 const slug = p.dataset.slug;57 const base = p.dataset.base || '';58 if (location.pathname === `${base}/read/${slug}`) return;59 history.replaceState(null, '', `${base}/read/${slug}`);60 if (p.dataset.title) document.title = p.dataset.title;61 }62 63 async function fetchArticle(slug, base) {64 const r = await fetch(`${base}/read/${encodeURIComponent(slug)}?fragment=1`, {65 headers: { 'HX-Request': 'true' },66 credentials: 'same-origin',67 });68 if (!r.ok) throw new Error('HTTP ' + r.status);69 const box = document.createElement('div');70 box.innerHTML = await r.text();71 const art = box.querySelector('.read-post');72 if (!art) throw new Error('no post in the response');73 return art;74 }75 76 /** Onderaan aanvullen: gewoon erbij zetten, de scrollpositie verandert niet. */77 async function appendOlder() {78 const s = stream();79 if (!s || busyBelow) return;80 const last = s.querySelector('.read-post:last-of-type');81 const slug = last && last.dataset.older;82 if (!slug) return markEnd(s);83 if (loaded.has(slug)) return;84 busyBelow = true;85 const pending = notice(s, 'read-loading', '…');86 try {87 const art = await fetchArticle(slug, last.dataset.base || '');88 loaded.add(slug);89 pending.remove();90 s.appendChild(art);91 watch();92 } catch (e) {93 pending.textContent = str('load_error');94 console.warn('[read] below:', e && e.message);95 } finally { busyBelow = false; }96 }97 98 /**99 * Bovenaan aanvullen. Dit is het enige plek waar we de scrollpositie WEL100 * moeten bijstellen: er komt hoogte bij bóven het beeld, en zonder correctie101 * schuift alles wat je las naar beneden weg. We meten de hoogte na het invoegen102 * en tellen die bij de scrollpositie op -- geen animatie, geen sprong.103 */104 async function prependNewer() {105 const s = stream();106 if (!s || busyAbove) return;107 const first = s.querySelector('.read-post');108 const slug = first && first.dataset.newer;109 if (!slug || loaded.has(slug)) return;110 busyAbove = true;111 const root = document.documentElement;112 try {113 const art = await fetchArticle(slug, first.dataset.base || '');114 loaded.add(slug);115 // Snappen even UIT. De correctie hieronder zet de scrollpositie precies116 // goed, maar een proximity-snap trekt hem daarna alsnog naar de grens van117 // het bericht dat er net bij kwam -- gemeten: een sprong ter grootte van118 // dat hele bericht, precies de ruk die deze opzet moet vermijden.119 root.style.scrollSnapType = 'none';120 const before = s.scrollHeight;121 s.insertBefore(art, first);122 const added = s.scrollHeight - before;123 window.scrollBy(0, added);124 watch();125 // Twee frames: één om de correctie te laten landen, één om te snappen pas126 // weer toe te staan als de browser klaar is met deze scroll.127 requestAnimationFrame(() => requestAnimationFrame(() => { root.style.scrollSnapType = ''; }));128 } catch (e) {129 root.style.scrollSnapType = '';130 console.warn('[read] above:', e && e.message);131 } finally { busyAbove = false; }132 }133 134 function notice(s, className, text) {135 const d = document.createElement('div');136 d.className = className;137 d.textContent = text;138 s.appendChild(d);139 return d;140 }141 142 function markEnd(s) {143 if (s.querySelector('.read-end')) return;144 notice(s, 'read-end', str('end'));145 }146 147 /** (Her)richt de waarnemer op de huidige eerste en laatste post. */148 function watch() {149 if (observer) observer.disconnect();150 observer = new IntersectionObserver((entries) => {151 for (const e of entries) {152 if (!e.isIntersecting) continue;153 // GEEN else-if. Bij het laden staat er één artikel in de stroom, en dat is154 // tegelijk het eerste én het laatste. Met een else-if wint de eerste tak155 // altijd, en dan draait prependNewer() nooit: wie op het OUDSTE bericht156 // binnenkomt, krijgt "einde archief" te zien terwijl er zes nieuwere157 // boven hem staan die hij nooit te pakken krijgt.158 if (e.target.matches('.read-post:last-of-type')) appendOlder();159 if (e.target.matches('.read-post:first-of-type')) prependNewer();160 }161 }, { rootMargin: ROOT_MARGIN });162 const first = stream() && stream().querySelector('.read-post');163 const last = stream() && stream().querySelector('.read-post:last-of-type');164 if (first) observer.observe(first);165 if (last && last !== first) observer.observe(last);166 }167 168 /**169 * Tikken op een bericht opent dat bericht.170 *171 * De stroom toont de tekst; het gesprek -- reacties, waarderingen, boosts --172 * staat op de berichtpagina. De titel en de voetlink zijn echte <a>'s en doen173 * het werk voor toetsenbord en schermlezer; dit hier is het gemak voor een duim.174 *175 * Daarom vier uitzonderingen, want een tik die je niet bedoelde is erger dan176 * geen tik: iets dat zelf al een doel heeft (link, knop, veld) houdt zijn eigen177 * werking, een geselecteerde tekst is geen tik, een verschoven vinger is178 * scrollen, en cmd/ctrl-klik hoort de browser zelf af te handelen.179 */180 20 let tapX = 0, tapY = 0; 181 21 function onPointerDown(e) { tapX = e.clientX; tapY = e.clientY; } … … 197 37 } 198 38 199 function onScroll() {200 const y = window.scrollY || 0;201 if (y < 8) toggleChrome(false);202 else if (y > lastY + 4) toggleChrome(true);203 else if (y < lastY - 24) toggleChrome(false);204 lastY = y;205 syncAddress();206 }207 208 39 export function init() { 209 const s = stream();40 const s = document.getElementById('read-stream'); 210 41 if (!s) return; 211 try { strings = JSON.parse(s.getAttribute('data-i18n') || '{}'); } catch (e) { strings = {}; } 212 s.querySelectorAll('.read-post').forEach((p) => loaded.add(p.dataset.slug)); 213 lastY = window.scrollY || 0; 214 window.addEventListener('scroll', onScroll, { passive: true }); 215 window.addEventListener('resize', watch, { passive: true }); 216 // Op de stroom, niet per artikel: aangevulde berichten doen zo vanzelf mee. 42 // Op de stroom, niet per artikel: wat "meer laden" erbij zet doet vanzelf mee. 43 // init() draait bij ELKE paginawissel, dus eerst losmaken -- anders stapelt 44 // dezelfde afhandelaar zich op en vuurt hij twee keer. 45 s.removeEventListener('pointerdown', onPointerDown); 46 s.removeEventListener('click', onTap); 217 47 s.addEventListener('pointerdown', onPointerDown, { passive: true }); 218 48 s.addEventListener('click', onTap); 219 watch();220 49 } 221 50
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)