fix: improve timeline UX - channel badges, breadcrumbs, default view
- Replace confusing colored left border with readable channel badge pills - Add breadcrumb navigation across all reader views - Default to timeline view when clicking Reader in sidebar - Remove redundant back-link from channel view (breadcrumbs handle it)
This commit is contained in:
+53
-9
@@ -1015,6 +1015,49 @@
|
|||||||
color: #7c3aed;
|
color: #7c3aed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================
|
||||||
|
Breadcrumbs
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
.breadcrumbs {
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__list {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
gap: 0;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item::before {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
content: "/";
|
||||||
|
margin: 0 var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__item:first-child::before {
|
||||||
|
content: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__link {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumbs__current {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
View Switcher
|
View Switcher
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
@@ -1072,19 +1115,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-view__item {
|
.timeline-view__item {
|
||||||
border-radius: var(--border-radius);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-view__item .item-card {
|
.timeline-view__channel-badge {
|
||||||
border-left: none;
|
border-radius: 3px;
|
||||||
}
|
color: #fff;
|
||||||
|
display: inline-block;
|
||||||
.timeline-view__channel-label {
|
font-size: 0.6875rem;
|
||||||
display: block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0 var(--space-s) var(--space-xs);
|
letter-spacing: 0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: var(--space-xs);
|
||||||
|
padding: 3px 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-view__filter {
|
.timeline-view__filter {
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ import { getDeckConfig, saveDeckConfig } from "../storage/deck.js";
|
|||||||
* @param {object} response - Express response
|
* @param {object} response - Express response
|
||||||
*/
|
*/
|
||||||
export async function index(request, response) {
|
export async function index(request, response) {
|
||||||
const lastView = request.session?.microsubView || "channels";
|
const lastView = request.session?.microsubView || "timeline";
|
||||||
const validViews = ["channels", "deck", "timeline"];
|
const validViews = ["channels", "deck", "timeline"];
|
||||||
const view = validViews.includes(lastView) ? lastView : "channels";
|
const view = validViews.includes(lastView) ? lastView : "timeline";
|
||||||
response.redirect(`${request.baseUrl}/${view}`);
|
response.redirect(`${request.baseUrl}/${view}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +71,10 @@ export async function channels(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +89,11 @@ export async function newChannel(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: request.__("microsub.channels.new") },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +166,11 @@ export async function channel(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: channelDocument.name },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +198,12 @@ export async function settings(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
||||||
|
{ text: "Settings" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +293,12 @@ export async function feeds(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
||||||
|
{ text: "Feeds" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +380,17 @@ export async function item(request, response) {
|
|||||||
channel = await channelsCollection.findOne({ _id: itemDocument.channelId });
|
channel = await channelsCollection.findOne({ _id: itemDocument.channelId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemBreadcrumbs = [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
];
|
||||||
|
if (channel) {
|
||||||
|
itemBreadcrumbs.push(
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: channel.name, href: `${request.baseUrl}/channels/${channel.uid}` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
itemBreadcrumbs.push({ text: itemDocument.name || "Item" });
|
||||||
|
|
||||||
response.render("item", {
|
response.render("item", {
|
||||||
title: itemDocument.name || "Item",
|
title: itemDocument.name || "Item",
|
||||||
item: itemDocument,
|
item: itemDocument,
|
||||||
@@ -361,6 +398,7 @@ export async function item(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: itemBreadcrumbs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +511,10 @@ export async function compose(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Compose" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,6 +690,10 @@ export async function searchPage(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Search" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +732,10 @@ export async function searchFeeds(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Search" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,6 +769,10 @@ export async function subscribe(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Search" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,6 +865,13 @@ export async function editFeedForm(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
||||||
|
{ text: "Feeds", href: `${request.baseUrl}/channels/${uid}/feeds` },
|
||||||
|
{ text: "Edit" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,6 +909,13 @@ export async function updateFeedUrl(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
||||||
|
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
||||||
|
{ text: "Feeds", href: `${request.baseUrl}/channels/${uid}/feeds` },
|
||||||
|
{ text: "Edit" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1029,6 +1097,10 @@ export async function actorProfile(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: actor.name || "Actor" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Microsub] Actor profile fetch failed: ${error.message}`);
|
console.error(`[Microsub] Actor profile fetch failed: ${error.message}`);
|
||||||
@@ -1043,6 +1115,10 @@ export async function actorProfile(request, response) {
|
|||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "channels",
|
activeView: "channels",
|
||||||
error: "Could not fetch this actor's profile. They may have restricted access.",
|
error: "Could not fetch this actor's profile. They may have restricted access.",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Actor" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1163,6 +1239,10 @@ export async function timeline(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "timeline",
|
activeView: "timeline",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Timeline" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,6 +1303,10 @@ export async function deck(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "deck",
|
activeView: "deck",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Deck" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1249,6 +1333,11 @@ export async function deckSettings(request, response) {
|
|||||||
baseUrl: request.baseUrl,
|
baseUrl: request.baseUrl,
|
||||||
readerBaseUrl: request.baseUrl,
|
readerBaseUrl: request.baseUrl,
|
||||||
activeView: "deck",
|
activeView: "deck",
|
||||||
|
breadcrumbs: [
|
||||||
|
{ text: "Reader", href: request.baseUrl },
|
||||||
|
{ text: "Deck", href: `${request.baseUrl}/deck` },
|
||||||
|
{ text: "Settings" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-microsub",
|
"name": "@rmdes/indiekit-endpoint-microsub",
|
||||||
"version": "1.0.38",
|
"version": "1.0.39",
|
||||||
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
|
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"indiekit",
|
"indiekit",
|
||||||
|
|||||||
+1
-3
@@ -3,9 +3,7 @@
|
|||||||
{% block reader %}
|
{% block reader %}
|
||||||
<div class="channel">
|
<div class="channel">
|
||||||
<header class="channel__header">
|
<header class="channel__header">
|
||||||
<a href="{{ baseUrl }}/channels" class="back-link">
|
<h1>{{ channel.name }}</h1>
|
||||||
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
||||||
</a>
|
|
||||||
<div class="channel__actions">
|
<div class="channel__actions">
|
||||||
{% if not showRead and items.length > 0 %}
|
{% if not showRead and items.length > 0 %}
|
||||||
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
|
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-microsub/styles.css">
|
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-microsub/styles.css">
|
||||||
|
{% include "partials/breadcrumbs.njk" %}
|
||||||
{% include "partials/view-switcher.njk" %}
|
{% include "partials/view-switcher.njk" %}
|
||||||
{% block reader %}{% endblock %}
|
{% block reader %}{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{# Breadcrumb navigation #}
|
||||||
|
{% if breadcrumbs and breadcrumbs.length > 0 %}
|
||||||
|
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||||
|
<ol class="breadcrumbs__list">
|
||||||
|
{% for crumb in breadcrumbs %}
|
||||||
|
<li class="breadcrumbs__item">
|
||||||
|
{% if crumb.href %}
|
||||||
|
<a href="{{ crumb.href }}" class="breadcrumbs__link">{{ crumb.text }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="breadcrumbs__current" aria-current="page">{{ crumb.text }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
+3
-3
@@ -31,13 +31,13 @@
|
|||||||
{% if items.length > 0 %}
|
{% if items.length > 0 %}
|
||||||
<div class="timeline" id="timeline">
|
<div class="timeline" id="timeline">
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<div class="timeline-view__item" style="border-left: 4px solid {{ item._channelColor or '#ccc' }}">
|
<div class="timeline-view__item">
|
||||||
{% include "partials/item-card.njk" %}
|
|
||||||
{% if item._channelName %}
|
{% if item._channelName %}
|
||||||
<span class="timeline-view__channel-label" style="color: {{ item._channelColor or '#888' }}">
|
<span class="timeline-view__channel-badge" style="background: {{ item._channelColor or '#888' }}">
|
||||||
{{ item._channelName }}
|
{{ item._channelName }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "partials/item-card.njk" %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user