diff --git a/assets/styles.css b/assets/styles.css index 6c7a4d1..52bc98c 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -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 { diff --git a/lib/controllers/reader.js b/lib/controllers/reader.js index d0f9699..537a413 100644 --- a/lib/controllers/reader.js +++ b/lib/controllers/reader.js @@ -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" }, + ], }); } diff --git a/package.json b/package.json index 098e7a2..b3c03cd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/views/channel.njk b/views/channel.njk index 01a5a42..b666e65 100644 --- a/views/channel.njk +++ b/views/channel.njk @@ -3,9 +3,7 @@ {% block reader %}