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.
This commit is contained in:
@@ -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,7 +198,6 @@
|
||||
}
|
||||
}
|
||||
/* Dark mode adjustments */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark .divBlogrollContainer {
|
||||
border-color: #444;
|
||||
color: #e0e0e0;
|
||||
@@ -174,19 +208,11 @@
|
||||
.dark .trBlogrollFeed:hover {
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
.dark .divBlogrollFooter {
|
||||
border-top-color: #444;
|
||||
.dark .trBlogrollFeed.selectedFeed {
|
||||
background-color: #1e3a5f;
|
||||
}
|
||||
}
|
||||
.dark .divBlogrollContainer {
|
||||
border-color: #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
.dark .divBlogrollContainer:focus {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
.dark .trBlogrollFeed:hover {
|
||||
background-color: #2a2a2a;
|
||||
.dark .divNewsPod li a {
|
||||
color: #8ab4f8;
|
||||
}
|
||||
.dark .divBlogrollFooter {
|
||||
border-top-color: #444;
|
||||
@@ -219,15 +245,46 @@
|
||||
<table class="divBlogrollTable">
|
||||
<tbody>
|
||||
<template x-for="blog in sortedBlogs" :key="blog.id">
|
||||
<tr class="trBlogrollFeed">
|
||||
<td class="tdBlogrollWedge">
|
||||
<span class="darkCaretColor">▶</span>
|
||||
</td>
|
||||
<td class="tdBlogrollFeedTitle">
|
||||
<tr>
|
||||
{# Feed row #}
|
||||
<td colspan="2" style="padding:0; border:0;">
|
||||
<div class="trBlogrollFeed"
|
||||
:class="selectedId === blog.id ? 'selectedFeed' : ''"
|
||||
style="display:flex; align-items:flex-start;">
|
||||
<div class="tdBlogrollWedge"
|
||||
@click="toggleExpand(blog)">
|
||||
<span :class="expandedId === blog.id ? 'darkCaretColor' : (selectedId === blog.id ? 'darkCaretColor' : 'lightCaretColor')"
|
||||
x-text="expandedId === blog.id ? '\u25BC' : '\u25B6'"></span>
|
||||
</div>
|
||||
<div class="tdBlogrollFeedTitle" style="flex:1; min-width:0;"
|
||||
@click="handleRowClick(blog)">
|
||||
<span class="spWhenUpdated" x-text="relativeTime(blog.lastFetchAt)"></span>
|
||||
<span class="spTitleString">
|
||||
<a :href="blog.siteUrl || blog.feedUrl" target="_blank" rel="noopener" x-text="blog.title"></a>
|
||||
<a :href="blog.siteUrl || blog.feedUrl" target="_blank" rel="noopener"
|
||||
x-text="blog.title" @click.stop></a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{# Expanded items #}
|
||||
<div class="divNewsPod" x-show="expandedId === blog.id" x-collapse>
|
||||
<template x-if="blog._loadingItems">
|
||||
<div class="feedLoading">Loading…</div>
|
||||
</template>
|
||||
<template x-if="!blog._loadingItems && blog._items && blog._items.length > 0">
|
||||
<ul>
|
||||
<template x-for="item in blog._items" :key="item.id">
|
||||
<li>
|
||||
<a :href="item.url" target="_blank" rel="noopener"
|
||||
x-text="truncate(item.title || item.summary || item.url, 100)"></a>
|
||||
<span class="spItemWhen" x-text="relativeTime(item.published)"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
<template x-if="!blog._loadingItems && (!blog._items || blog._items.length === 0)">
|
||||
<div class="feedLoading">No recent items</div>
|
||||
</template>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user