mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
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:
@@ -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 {
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
16
views/partials/breadcrumbs.njk
Normal file
16
views/partials/breadcrumbs.njk
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user