Changeset 7cf8144 in Klonkt
- 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
- Files:
-
- 1 deleted
- 9 edited
-
assets/css/style.css (modified) (3 diffs)
-
assets/js/mod/read.js (modified) (2 diffs)
-
routes/posts.js (modified) (4 diffs)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/home.ejs (modified) (1 diff)
-
views/pages/read.ejs (deleted)
-
views/partials/home-append.ejs (modified) (1 diff)
-
views/partials/shared-styles.ejs (modified) (2 diffs)
-
views/partials/view-switcher.ejs (modified) (2 diffs)
-
views/shell.ejs (modified) (4 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 -
src/routes/posts.js
r2d2fd8c r7cf8144 232 232 233 233 if (append) { 234 return renderPage(req, res, 'partials/home-append', { posts, hasMore, nextOffset: offset + FEED_PAGE, moreBase }); 234 return renderPage(req, res, 'partials/home-append', { 235 posts, hasMore, nextOffset: offset + FEED_PAGE, moreBase, 236 readerItems: readerItems(site, posts, req), 237 }); 235 238 } 236 239 … … 243 246 pinnedPosts, 244 247 posts, 248 readerItems: readerItems(site, [...pinnedPosts, ...posts], req), 245 249 hasMore, nextOffset: offset + FEED_PAGE, moreBase, 246 250 movedTo, … … 249 253 socialDescr: site.description || site.tagline || '', 250 254 bodyClass: 'on-home', 255 // mod/read.js: alleen nog de tik-op-een-bericht in de leesweergave. 256 pageJs: 'read', 251 257 }); 252 });253 254 // ==================== LEES (meeslepende tijdlijn) ====================255 //256 // Eén bericht vult het scherm, de chrome schuift weg, en je scrollt voorbij de257 // rand naar het vorige of volgende. Naar bartoverkamp.nl, waar prev/next258 // schildwachten boven en onder het artikel staan en het overscrollen zelf de259 // navigatie is.260 //261 // De buren komen uit postNeighbors(), dezelfde die de gewone postpagina al262 // gebruikt -- dus "vorige" en "volgende" betekenen hier precies hetzelfde als263 // daar, ook voor gepinde berichten.264 //265 // ?partial=1 levert alleen het artikel, want dat is wat de schildwacht inruilt.266 router.get('/read/:slug?', (req, res, next) => {267 const site = res.locals.site;268 if (!site) return next();269 270 // Zonder slug: het nieuwste bericht, zodat /read een ingang is en niet een271 // fout. Gepind eerst, net als op de voorpagina.272 const post = req.params.slug273 ? db.prepare(`SELECT p.*, u.username as author_username FROM posts p JOIN users u ON p.author_id = u.id274 WHERE p.site_id = ? AND p.slug = ?`).get(site.id, req.params.slug)275 : db.prepare(`SELECT p.*, u.username as author_username FROM posts p JOIN users u ON p.author_id = u.id276 WHERE p.site_id = ? AND p.status = 'published'277 ORDER BY (p.pinned = 0) ASC, p.pinned ASC, p.published_at DESC LIMIT 1`).get(site.id);278 if (!post) return next();279 280 // Het besluit en het lijf komen uit één plek (PostAccessService). Een dichte281 // poort levert hier GEEN tekst op: die wordt niet eens gerenderd.282 const _u = req.query.u ? verifyBlob(String(req.query.u)) : null;283 const unlockedSlug = (_u && _u.purpose === 'unlocked' && _u.siteId === site.id) ? String(_u.post) : null;284 const entry = postEntry(post, { user: req.session?.user || null, site, unlockedSlug },285 { renderBody: (p) => renderPostBodyHtml(site, p, req) });286 287 if (entry.access === 'forbidden') return next();288 289 const { newerPost, olderPost } = postNeighbors(site, post);290 const model = {291 post, entry, newerPost, olderPost,292 pageTitle: post.title || site.title,293 // on-read zet de chrome weg; de mini-topnav blijft (chrome.ejs, _headerless).294 bodyClass: 'on-read on-special',295 pageJs: 'read',296 };297 // ALLEEN de leesmodule krijgt één artikel; ?fragment=1 is haar eigen teken.298 //299 // Hier stond `?partial=1 || hx-request`, en dat waren twee dingen door elkaar.300 // Op deze site betekent ?partial=1 al iets anders -- "de pagina zonder de301 // schil", voor htmx -- dus een gebooste link naar /read kreeg één kaal302 // artikel in #pcms-main geschoven: geen stroom, geen on-read, geen script.303 // Onzichtbaar zolang je /read alleen intikt; zichtbaar zodra er een link naar304 // toe bestaat (de Lezen-knop in de view-switcher).305 //306 // Alles wat GEEN fragment vraagt gaat naar pages/read, en renderPage bepaalt307 // daar zelf of de schil eromheen moet -- net als bij elke andere pagina.308 if (req.query.fragment === '1') {309 return renderPage(req, res, 'partials/read-article', model);310 }311 recordPageview(site.id, req);312 return renderPage(req, res, 'pages/read', model);313 258 }); 314 259 … … 748 693 // Renders a post's display HTML: baked content + the dynamic audio/embed layer. 749 694 // Extracted so the paid unlock (slice 4) serves the exact same body as the page. 695 // Dezelfde berichten, klaar voor de leesweergave. 696 // 697 // Tijdlijn en Grid tonen kaartjes; Lezen toont het hele stuk. Het is dus geen 698 // andere PAGINA maar een andere vorm van dezelfde rijen -- vandaar dat de feed 699 // ze alledrie meestuurt en CSS kiest, precies zoals timeline/grid dat al deden. 700 // 701 // Het lijf loopt door PostAccessService: een gesloten poort levert hier GEEN 702 // tekst op, want wat niet gerenderd wordt kan ook niet lekken. 703 function readerItems(site, rows, req) { 704 const viewer = { user: req.session?.user || null, site, unlockedSlug: null }; 705 return rows.map((post) => ({ 706 post, 707 entry: postEntry(post, viewer, { renderBody: (p) => renderPostBodyHtml(site, p, req) }), 708 })); 709 } 710 750 711 export function renderPostBodyHtml(site, post, req) { 751 712 let html = (post.content_rendered != null && post.content_rendered !== '') -
src/services/i18n.js
r2d2fd8c r7cf8144 891 891 'fgate.title': 'Alleen voor vrienden', 892 892 'fgate.sub': 'Dit is exclusief voor ingelogde vrienden. Log in om het te bekijken.', 893 'fgate.login': 'Inloggen / aanmelden', 'read.open': 'Reacties en waarderingen', 'read.end': 'Je bent bij het begin van het archief.', 'read.load_error': 'Kon het volgende bericht niet laden.',893 'fgate.login': 'Inloggen / aanmelden', 'read.open': 'Reacties en waarderingen', 894 894 'lbio.empty': 'Nog geen links ingesteld.', 895 895 'lbio.back_to_site': 'naar de site', … … 1897 1897 'fgate.title': 'Friends only', 1898 1898 'fgate.sub': 'This is exclusive to logged-in friends. Log in to view it.', 1899 'fgate.login': 'Log in / sign up', 'read.open': 'Replies and reactions', 'read.end': 'You have reached the beginning of the archive.', 'read.load_error': 'Could not load the next post.',1899 'fgate.login': 'Log in / sign up', 'read.open': 'Replies and reactions', 1900 1900 'lbio.empty': 'No links set up yet.', 1901 1901 'lbio.back_to_site': 'back to the site', … … 2903 2903 'fgate.title': 'Nur für Freunde', 2904 2904 'fgate.sub': 'Dies ist exklusiv für eingeloggte Freunde. Melde dich an, um es anzusehen.', 2905 'fgate.login': 'Anmelden / Registrieren', 'read.open': 'Antworten und Reaktionen', 'read.end': 'Du bist am Anfang des Archivs angekommen.', 'read.load_error': 'Der nächste Beitrag konnte nicht geladen werden.',2905 'fgate.login': 'Anmelden / Registrieren', 'read.open': 'Antworten und Reaktionen', 2906 2906 'lbio.empty': 'Noch keine Links eingerichtet.', 2907 2907 'lbio.back_to_site': 'zurück zur Seite', -
src/views/pages/home.ejs
r2d2fd8c r7cf8144 59 59 </div> 60 60 61 <!-- ========== LEZEN (alleen zichtbaar bij body[data-feed-view="reader"]) ========== 62 Hetzelfde stel berichten, maar heel: één per scherm, met de grenzen door 63 CSS-snapping. Geen eigen route en geen eigen shell -- de balken blijven 64 staan, want je bent nog gewoon op de feed en moet terug kunnen schakelen. 65 Leeg? Dan valt de lege staat van de tijdlijn hierboven in beeld; deze 66 sectie rendert dan niet. --> 67 <% if (typeof readerItems !== 'undefined' && readerItems && readerItems.length) { %> 68 <section class="feed-reader" aria-label="<%= t('switch.reader') %>"> 69 <div class="read-stream" id="read-stream" data-base="<%= _base %>"> 70 <% readerItems.forEach(function(it) { %> 71 <%- include('../partials/read-article', { post: it.post, entry: it.entry }) %> 72 <% }); %> 73 </div> 74 </section> 75 <% } %> 76 61 77 <!-- ========== GRID (only visible when body[data-feed-view="grid"]) ========== --> 62 78 <% if (allPosts.length > 0) { %> -
src/views/partials/home-append.ejs
r2d2fd8c r7cf8144 4 4 items instead of nesting under an extra box. %> 5 5 <div hx-swap-oob="beforeend:#grid-tiles" style="display:contents"><% posts.forEach(function(post){ %><%- include('post-tile', { post: post }) %><% }); %></div> 6 <%# Lezen: dezelfde berichten OOB achteraan de leesstroom, net als de tegels 7 hierboven. Zonder dit zou "meer laden" in de leesweergave niets doen. %> 8 <% if (typeof readerItems !== 'undefined' && readerItems && readerItems.length) { %><div hx-swap-oob="beforeend:#read-stream" style="display:contents"><% readerItems.forEach(function(it){ %><%- include('read-article', { post: it.post, entry: it.entry }) %><% }); %></div><% } %> 6 9 <%- include('load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#post-list', morePath: (typeof morePath !== 'undefined' ? morePath : '/'), oob: true }) %> -
src/views/partials/shared-styles.ejs
r2d2fd8c r7cf8144 27 27 padding: 0.15rem; 28 28 } 29 .view-switch-btn, 30 .view-switch-link { 29 .view-switch-btn { 31 30 display: inline-flex; align-items: center; gap: 0.35rem; 32 31 padding: 0.35rem 0.85rem; … … 36 35 transition: background 120ms, color 120ms; 37 36 } 38 .view-switch-btn:hover, 39 .view-switch-link:hover { color: var(--ink); } 40 .view-switch-link { text-decoration: none; } 37 .view-switch-btn:hover { color: var(--ink); } 41 38 /* Active = white (light background, dark text). Via aria-selected (element 42 39 attribute, set by syncAria + on htmx:afterSettle) → htmx-robust. On non- 43 40 feed pages syncAria sets aria-selected=false → both grey. */ 44 .view-switch-btn[aria-selected="true"], 45 .view-switch-link[aria-selected="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); } 41 .view-switch-btn[aria-selected="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); } 46 42 47 43 /* Agenda = its OWN pill (separate from Timeline/Grid, so the feed-toggle is never -
src/views/partials/view-switcher.ejs
r2d2fd8c r7cf8144 8 8 // Pagina's zonder actieve feed-weergave → niets in de pill is 'actief' (Solo/Cirkel 9 9 // + Tijdlijn/Grid grijs). Alleen Agenda krijgt z'n eigen is-active op de agenda. 10 var _neutral = ['on-shows','on-downloads','on-post','on-epk','on-linkbio','on-newsletter','on-archive' ,'on-read']10 var _neutral = ['on-shows','on-downloads','on-post','on-epk','on-linkbio','on-newsletter','on-archive'] 11 11 .some(function(c){ return _bc.indexOf(c) >= 0; }); 12 var _onRead = _bc.indexOf('on-read') >= 0;13 var _sbR = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';14 12 %> 15 13 <div class="view-switcher-wrap"> … … 47 45 <span><%= t('switch.grid') %></span> 48 46 </button> 49 <%# Lezen is GEEN in-place omschakeling maar een andere route (/read), dus 50 een <a> en niet een <button>. En bewust NIET de klasse view-switch-btn: 51 de gedelegeerde klik in shell.ejs pakt alles met die klasse op, zet 52 body[data-feed-view] erop en springt op niet-feedpagina's terug naar 53 home -- dat zou deze link opeten. Andere klasse, zelfde uiterlijk. 54 aria-selected staat hier vast: syncAria() raakt alleen .view-switch-btn 55 aan, dus de actieve staat op /read blijft staan na een htmx-navigatie. %> 56 <a class="view-switch-link" href="<%= _sbR %>/read" role="tab" 57 aria-selected="<%= _onRead ? 'true' : 'false' %>"> 47 <%# Lezen is een gewone derde weergave: dezelfde berichten, andere vorm. 48 Dus een <button> met data-view, net als de andere twee -- dan doen de 49 gedelegeerde klik, localStorage en syncAria in shell.ejs hun werk al. %> 50 <button type="button" class="view-switch-btn" data-view="reader" role="tab" 51 aria-selected="false"> 58 52 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg> 59 53 <span><%= t('switch.reader') %></span> 60 </ a>54 </button> 61 55 </div> 62 56 <% if (!_neutral) { %> -
src/views/shell.ejs
r2d2fd8c r7cf8144 222 222 223 223 <!-- v9 stylesheet (full palette system) --> 224 <link rel="stylesheet" href="/assets/css/style.css?v= 68">224 <link rel="stylesheet" href="/assets/css/style.css?v=70"> 225 225 <script> 226 226 /* iOS safe-area, built by hand. env(safe-area-inset-top) resolves to 0 on this iOS in … … 435 435 // focusscherm óók via htmx z'n styling krijgt (geen chrome, geen tab, audio op 0). 436 436 // ALLES wat de server als page-class kan sturen moet hier staan: wat er niet in 437 // staat wordt bij een htmx-navigatie stilletjes weggefilterd. 'on-read' ontbrak, 438 // en dan kwam je via de Lezen-knop op een leesweergave waarvan ELKE regel 439 // (.on-read ...) niet aansloeg: geen schermhoge berichten, geen snappen, geen 440 // wegschuivende balken. Bij intikken van /read ging het wel goed, want dan 441 // rendert de shell de class zelf -- vandaar dat het lang onzichtbaar bleef. 437 // staat wordt bij een htmx-navigatie stilletjes weggefilterd. 442 438 var PAGE_CLASSES = ['on-home','on-post','on-special','on-archive','on-search','on-admin','on-auth', 443 439 'on-shows','on-downloads','on-download','on-epk','on-linkbio','on-newsletter', 444 'on-tag','on-type','on-user','on-cirkel','on-hub','on-chat' ,'on-read'];440 'on-tag','on-type','on-user','on-cirkel','on-hub','on-chat']; 445 441 446 442 document.body.addEventListener('pcmsNav', function(ev) { … … 509 505 // Eén nummer voor de hele map. Te vaak bumpen kost één download; te weinig 510 506 // bumpen kost een bugfix die nooit aankomt. 511 var MOD_V = 3;507 var MOD_V = 4; 512 508 513 509 // name -> 1 (aan het laden) of de module-namespace (geladen). Een module … … 554 550 try { 555 551 var v = localStorage.getItem('pcms-feed-view'); 556 if (v === 'timeline' || v === 'grid' ) body.dataset.feedView = v;552 if (v === 'timeline' || v === 'grid' || v === 'reader') body.dataset.feedView = v; 557 553 var c = parseInt(localStorage.getItem('pcms-grid-cols'), 10); 558 554 if (c === 2 || c === 3 || c === 4) body.dataset.gridCols = String(c);
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)