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:
Ricardo
2026-02-27 10:54:54 +01:00
parent 26225f1f80
commit 6269c7ac98
7 changed files with 166 additions and 18 deletions

View File

@@ -1015,6 +1015,49 @@
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
========================================================================== */
@@ -1072,19 +1115,20 @@
}
.timeline-view__item {
border-radius: var(--border-radius);
position: relative;
}
.timeline-view__item .item-card {
border-left: none;
}
.timeline-view__channel-label {
display: block;
font-size: 0.75rem;
.timeline-view__channel-badge {
border-radius: 3px;
color: #fff;
display: inline-block;
font-size: 0.6875rem;
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 {

View File

@@ -46,9 +46,9 @@ import { getDeckConfig, saveDeckConfig } from "../storage/deck.js";
* @param {object} response - Express 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 view = validViews.includes(lastView) ? lastView : "channels";
const view = validViews.includes(lastView) ? lastView : "timeline";
response.redirect(`${request.baseUrl}/${view}`);
}
@@ -71,6 +71,10 @@ export async function channels(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Channels" },
],
});
}
@@ -85,6 +89,11 @@ export async function newChannel(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
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,
readerBaseUrl: request.baseUrl,
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,
readerBaseUrl: request.baseUrl,
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,
readerBaseUrl: request.baseUrl,
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 });
}
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", {
title: itemDocument.name || "Item",
item: itemDocument,
@@ -361,6 +398,7 @@ export async function item(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: itemBreadcrumbs,
});
}
@@ -473,6 +511,10 @@ export async function compose(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Compose" },
],
});
}
@@ -648,6 +690,10 @@ export async function searchPage(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Search" },
],
});
}
@@ -686,6 +732,10 @@ export async function searchFeeds(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Search" },
],
});
}
@@ -719,6 +769,10 @@ export async function subscribe(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Search" },
],
});
}
@@ -811,6 +865,13 @@ export async function editFeedForm(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
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,
readerBaseUrl: request.baseUrl,
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,
readerBaseUrl: request.baseUrl,
activeView: "channels",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: actor.name || "Actor" },
],
});
} catch (error) {
console.error(`[Microsub] Actor profile fetch failed: ${error.message}`);
@@ -1043,6 +1115,10 @@ export async function actorProfile(request, response) {
readerBaseUrl: request.baseUrl,
activeView: "channels",
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,
readerBaseUrl: request.baseUrl,
activeView: "timeline",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Timeline" },
],
});
}
@@ -1223,6 +1303,10 @@ export async function deck(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "deck",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Deck" },
],
});
}
@@ -1249,6 +1333,11 @@ export async function deckSettings(request, response) {
baseUrl: request.baseUrl,
readerBaseUrl: request.baseUrl,
activeView: "deck",
breadcrumbs: [
{ text: "Reader", href: request.baseUrl },
{ text: "Deck", href: `${request.baseUrl}/deck` },
{ text: "Settings" },
],
});
}

View File

@@ -1,6 +1,6 @@
{
"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.",
"keywords": [
"indiekit",

View File

@@ -3,9 +3,7 @@
{% block reader %}
<div class="channel">
<header class="channel__header">
<a href="{{ baseUrl }}/channels" class="back-link">
{{ icon("previous") }} {{ __("microsub.channels.title") }}
</a>
<h1>{{ channel.name }}</h1>
<div class="channel__actions">
{% if not showRead and items.length > 0 %}
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">

View File

@@ -6,6 +6,7 @@
{% block content %}
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-microsub/styles.css">
{% include "partials/breadcrumbs.njk" %}
{% include "partials/view-switcher.njk" %}
{% block reader %}{% endblock %}
{% endblock %}

View File

@@ -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 %}

View File

@@ -31,13 +31,13 @@
{% if items.length > 0 %}
<div class="timeline" id="timeline">
{% for item in items %}
<div class="timeline-view__item" style="border-left: 4px solid {{ item._channelColor or '#ccc' }}">
{% include "partials/item-card.njk" %}
<div class="timeline-view__item">
{% 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 }}
</span>
{% endif %}
{% include "partials/item-card.njk" %}
</div>
{% endfor %}
</div>