From fec999793defbb357bf6a6a2a3e74b6458ea4d47 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 17 Feb 2026 15:54:31 +0100 Subject: [PATCH] fix: FeedLand widget click-to-expand and dark mode for expanded items Add row selection and expand/collapse behavior matching Dave Winer's blogroll.js: first click selects, second click (or caret click) expands to show up to 5 recent items fetched from blogroll API. Items cached after first fetch. Added dark mode styles for expanded items. --- _includes/components/widgets/feedland.njk | 152 ++++++++++++++++++---- 1 file changed, 127 insertions(+), 25 deletions(-) diff --git a/_includes/components/widgets/feedland.njk b/_includes/components/widgets/feedland.njk index 4e78d4a..ed1b61a 100644 --- a/_includes/components/widgets/feedland.njk +++ b/_includes/components/widgets/feedland.njk @@ -86,6 +86,7 @@ padding-right: 0; vertical-align: top; width: 12px; + cursor: pointer; } .trBlogrollFeed .tdBlogrollFeedTitle { font-size: 15px; @@ -117,12 +118,46 @@ .trBlogrollFeed:hover { background-color: whitesmoke; } +.trBlogrollFeed.selectedFeed { + background-color: #e8f0fe; +} .darkCaretColor { opacity: .9; } .lightCaretColor { opacity: .2; } +/* Expanded items (newspod) */ +.divNewsPod { + padding: 2px 0 4px 16px; +} +.divNewsPod ul { + list-style: none; + margin: 0; + padding: 0; +} +.divNewsPod li { + padding: 2px 0; + font-size: 13px; + line-height: 1.3; +} +.divNewsPod li a { + color: #1a73e8; + text-decoration: none; +} +.divNewsPod li a:hover { + text-decoration: underline; +} +.divNewsPod li .spItemWhen { + font-size: 11px; + opacity: 0.6; + margin-left: 4px; +} +.divNewsPod .feedLoading { + font-size: 12px; + opacity: 0.5; + padding: 4px 0; +} .divBlogrollFooter { text-align: center; font-size: 13px; @@ -163,21 +198,6 @@ } } /* Dark mode adjustments */ -@media (prefers-color-scheme: dark) { - .dark .divBlogrollContainer { - border-color: #444; - color: #e0e0e0; - } - .dark .divBlogrollContainer:focus { - background-color: #1a1a1a; - } - .dark .trBlogrollFeed:hover { - background-color: #2a2a2a; - } - .dark .divBlogrollFooter { - border-top-color: #444; - } -} .dark .divBlogrollContainer { border-color: #444; color: #e0e0e0; @@ -188,6 +208,12 @@ .dark .trBlogrollFeed:hover { background-color: #2a2a2a; } +.dark .trBlogrollFeed.selectedFeed { + background-color: #1e3a5f; +} +.dark .divNewsPod li a { + color: #8ab4f8; +} .dark .divBlogrollFooter { border-top-color: #444; } @@ -219,15 +245,46 @@ @@ -250,6 +307,8 @@ function feedlandWidget() { title: 'FeedLand', riverUrl: 'https://feedland.com', loading: true, + selectedId: null, + expandedId: null, get sortedBlogs() { const sorted = [...this.blogs]; @@ -265,6 +324,45 @@ function feedlandWidget() { return sorted; }, + handleRowClick(blog) { + if (this.selectedId !== blog.id) { + // First click: select + this.selectedId = blog.id; + } else { + // Second click: toggle expand + this.toggleExpand(blog); + } + }, + + async toggleExpand(blog) { + this.selectedId = blog.id; + if (this.expandedId === blog.id) { + // Collapse + this.expandedId = null; + return; + } + // Expand — fetch items if not cached + this.expandedId = blog.id; + if (!blog._items) { + blog._loadingItems = true; + try { + const res = await fetch('/blogrollapi/api/blogs/' + blog.id); + const data = await res.json(); + blog._items = (data.items || []).slice(0, 5); + } catch (err) { + console.error('FeedLand: failed to load items for', blog.title, err); + blog._items = []; + } finally { + blog._loadingItems = false; + } + } + }, + + truncate(str, max) { + if (!str) return ''; + return str.length > max ? str.slice(0, max) + '…' : str; + }, + relativeTime(iso) { if (!iso) return ''; const diff = Date.now() - new Date(iso).getTime(); @@ -280,7 +378,11 @@ function feedlandWidget() { try { const res = await fetch('/blogrollapi/api/blogs?source=feedland&sort=recent&limit=100'); const data = await res.json(); - this.blogs = data.items || []; + this.blogs = (data.items || []).map(b => ({ + ...b, + _items: null, + _loadingItems: false, + })); } catch (err) { console.error('FeedLand widget error:', err); } finally {