chore: phase 2 convention alignment — ms- prefix, onerror removal, visually-hidden fix (v1.0.46)

- Namespace all plugin CSS classes with ms- prefix (20 BEM blocks)
- Update all 19 templates to match prefixed class names
- Replace visually-hidden with -!-visually-hidden (core convention)
- Remove inline onerror handlers from avatar/photo images
- Remove dead source-type SVG icons (Fediverse/Bluesky/Web)

Confab-Link: http://localhost:8080/sessions/bb4a6ec4-b711-48cd-b3d7-942ec2a9851d
This commit is contained in:
Ricardo
2026-03-13 12:32:08 +01:00
parent e48335da2c
commit 4a87773d7f
20 changed files with 595 additions and 621 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@rmdes/indiekit-endpoint-microsub",
"version": "1.0.45",
"version": "1.0.46",
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
"keywords": [
"indiekit",

View File

@@ -1,33 +1,32 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="channel">
<header class="channel__header">
<div class="ms-channel">
<header class="ms-channel__header">
<a href="{{ baseUrl }}/channels/activitypub" class="back-link">
{{ icon("previous") }} Fediverse
</a>
</header>
{# Actor profile card #}
<div class="actor-profile">
<div class="actor-profile__header">
<div class="ms-actor-profile">
<div class="ms-actor-profile__header">
{% if actor.photo %}
<img src="{{ actor.photo }}"
alt=""
class="actor-profile__avatar"
class="ms-actor-profile__avatar"
width="80"
height="80"
onerror="this.style.display='none'">
height="80">
{% endif %}
<div class="actor-profile__info">
<h2 class="actor-profile__name">{{ actor.name }}</h2>
<div class="ms-actor-profile__info">
<h2 class="ms-actor-profile__name">{{ actor.name }}</h2>
{% if actor.handle %}
<span class="actor-profile__handle">@{{ actor.handle }}</span>
<span class="ms-actor-profile__handle">@{{ actor.handle }}</span>
{% endif %}
{% if actor.summary %}
<p class="actor-profile__summary">{{ actor.summary }}</p>
<p class="ms-actor-profile__summary">{{ actor.summary }}</p>
{% endif %}
<div class="actor-profile__stats">
<div class="ms-actor-profile__stats">
{% if actor.followersCount %}
<span>{{ actor.followersCount }} followers</span>
{% endif %}
@@ -37,7 +36,7 @@
</div>
</div>
</div>
<div class="actor-profile__actions">
<div class="ms-actor-profile__actions">
<a href="{{ actor.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
{{ icon("external") }} View profile
</a>
@@ -63,39 +62,38 @@
</div>
{% if error %}
<div class="reader__empty">
<div class="ms-reader__empty">
{{ icon("warning") }}
<p>{{ error }}</p>
</div>
{% elif items.length > 0 %}
<div class="timeline" id="timeline">
<div class="ms-timeline" id="timeline">
{% for item in items %}
<article class="item-card">
<article class="ms-item-card">
{# Author #}
{% if item.author %}
<div class="item-card__author" style="padding: 12px 16px 0;">
<div class="ms-item-card__author" style="padding: 12px 16px 0;">
{% if item.author.photo %}
<img src="{{ item.author.photo }}"
alt=""
class="item-card__author-photo"
class="ms-item-card__author-photo"
width="40"
height="40"
loading="lazy"
onerror="this.style.display='none'">
loading="lazy">
{% endif %}
<div class="item-card__author-info">
<span class="item-card__author-name">{{ item.author.name or "Unknown" }}</span>
<div class="ms-item-card__author-info">
<span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span>
{% if item.author.url %}
<span class="item-card__source">{{ item.author.url | replace("https://", "") | replace("http://", "") }}</span>
<span class="ms-item-card__source">{{ item.author.url | replace("https://", "") | replace("http://", "") }}</span>
{% endif %}
</div>
</div>
{% endif %}
<a href="{{ item.url }}" class="item-card__link" target="_blank" rel="noopener">
<a href="{{ item.url }}" class="ms-item-card__link" target="_blank" rel="noopener">
{# Reply context #}
{% if item["in-reply-to"] and item["in-reply-to"].length > 0 %}
<div class="item-card__context">
<div class="ms-item-card__context">
{{ icon("reply") }}
<span>Reply to</span>
<span>{{ item["in-reply-to"][0] | replace("https://", "") | replace("http://", "") | truncate(50) }}</span>
@@ -104,12 +102,12 @@
{# Title #}
{% if item.name %}
<h3 class="item-card__title">{{ item.name }}</h3>
<h3 class="ms-item-card__title">{{ item.name }}</h3>
{% endif %}
{# Content #}
{% if item.content %}
<div class="item-card__content{% if (item.content.text or '') | length > 300 %} item-card__content--truncated{% endif %}">
<div class="ms-item-card__content{% if (item.content.text or '') | length > 300 %} ms-item-card__content--truncated{% endif %}">
{% if item.content.html %}
{{ item.content.html | safe | striptags | truncate(400) }}
{% elif item.content.text %}
@@ -120,10 +118,10 @@
{# Tags #}
{% if item.category and item.category.length > 0 %}
<div class="item-card__categories">
<div class="ms-item-card__categories">
{% for cat in item.category %}
{% if loop.index0 < 5 %}
<span class="item-card__category">#{{ cat }}</span>
<span class="ms-item-card__category">#{{ cat }}</span>
{% endif %}
{% endfor %}
</div>
@@ -132,20 +130,19 @@
{# Photos #}
{% if item.photo and item.photo.length > 0 %}
{% set photoCount = item.photo.length if item.photo.length <= 4 else 4 %}
<div class="item-card__photos item-card__photos--{{ photoCount }}">
<div class="ms-item-card__photos ms-item-card__photos--{{ photoCount }}">
{% for photo in item.photo %}
{% if loop.index0 < 4 %}
<img src="{{ photo }}" alt="" class="item-card__photo" loading="lazy"
onerror="this.parentElement.removeChild(this)">
<img src="{{ photo }}" alt="" class="ms-item-card__photo" loading="lazy">
{% endif %}
{% endfor %}
</div>
{% endif %}
{# Footer #}
<footer class="item-card__footer">
<footer class="ms-item-card__footer">
{% if item.published %}
<time datetime="{{ item.published }}" class="item-card__date">
<time datetime="{{ item.published }}" class="ms-item-card__date">
{{ item.published | date("PP", { locale: locale, timeZone: application.timeZone }) }}
</time>
{% endif %}
@@ -153,20 +150,20 @@
</a>
{# Actions #}
<div class="item-actions">
<a href="{{ item.url }}" class="item-actions__button" target="_blank" rel="noopener" title="View original">
<div class="ms-item-actions">
<a href="{{ item.url }}" class="ms-item-actions__button" target="_blank" rel="noopener" title="View original">
{{ icon("external") }}
</a>
<a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="item-actions__button" title="Reply">
<a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="ms-item-actions__button" title="Reply">
{{ icon("reply") }}
</a>
<a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="item-actions__button" title="Like">
<a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="ms-item-actions__button" title="Like">
{{ icon("like") }}
</a>
<a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="item-actions__button" title="Repost">
<a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="ms-item-actions__button" title="Repost">
{{ icon("repost") }}
</a>
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="item-actions__button" title="Bookmark">
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="ms-item-actions__button" title="Bookmark">
{{ icon("bookmark") }}
</a>
</div>
@@ -174,7 +171,7 @@
{% endfor %}
</div>
{% else %}
<div class="reader__empty">
<div class="ms-reader__empty">
{{ icon("syndicate") }}
<p>No posts found for this actor.</p>
</div>

View File

@@ -1,10 +1,10 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="channel">
<header class="channel__header">
<div class="ms-channel">
<header class="ms-channel__header">
<h1>{{ channel.name }}</h1>
<div class="channel__actions">
<div class="ms-channel__actions">
{% if not showRead and items.length > 0 %}
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
<input type="hidden" name="channel" value="{{ channel.uid }}">
@@ -33,14 +33,14 @@
</header>
{% if items.length > 0 %}
<div class="timeline" id="timeline" data-channel="{{ channel.uid }}">
<div class="ms-timeline" id="timeline" data-channel="{{ channel.uid }}">
{% for item in items %}
{% include "partials/item-card.njk" %}
{% endfor %}
</div>
{% if paging %}
<nav class="timeline__paging" aria-label="Pagination">
<nav class="ms-timeline__paging" aria-label="Pagination">
{% if paging.before %}
<a href="?before={{ paging.before }}{% if showRead %}&showRead=true{% endif %}" class="button button--secondary">
{{ icon("previous") }} {{ __("microsub.reader.newer") }}
@@ -56,7 +56,7 @@
</nav>
{% endif %}
{% else %}
<div class="reader__empty">
<div class="ms-reader__empty">
{% if readCount > 0 and not showRead %}
{{ icon("checkboxChecked") }}
<p>{{ __("microsub.reader.allRead") }}</p>
@@ -78,16 +78,16 @@
// Keyboard navigation (j/k for items, o to open)
const timeline = document.getElementById('timeline');
if (timeline) {
const items = Array.from(timeline.querySelectorAll('.item-card'));
const items = Array.from(timeline.querySelectorAll('.ms-item-card'));
let currentIndex = -1;
function focusItem(index) {
if (items[currentIndex]) {
items[currentIndex].classList.remove('item-card--focused');
items[currentIndex].classList.remove('ms-item-card--focused');
}
currentIndex = Math.max(0, Math.min(index, items.length - 1));
if (items[currentIndex]) {
items[currentIndex].classList.add('item-card--focused');
items[currentIndex].classList.add('ms-item-card--focused');
items[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
@@ -108,7 +108,7 @@
case 'Enter':
e.preventDefault();
if (items[currentIndex]) {
const link = items[currentIndex].querySelector('.item-card__link');
const link = items[currentIndex].querySelector('.ms-item-card__link');
if (link) link.click();
}
break;
@@ -121,7 +121,7 @@
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
timeline.addEventListener('click', async (e) => {
const button = e.target.closest('.item-actions__mark-read');
const button = e.target.closest('.ms-item-actions__mark-read');
if (!button) return;
e.preventDefault();
@@ -151,7 +151,7 @@
if (response.ok) {
// Hide the item with animation
const card = button.closest('.item-card');
const card = button.closest('.ms-item-card');
if (card) {
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
card.style.opacity = '0';
@@ -159,7 +159,7 @@
setTimeout(() => {
card.remove();
// Check if timeline is now empty
if (timeline.querySelectorAll('.item-card').length === 0) {
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
location.reload();
}
}, 300);
@@ -176,14 +176,14 @@
// Handle caret toggle for mark-source-read popover
timeline.addEventListener('click', (e) => {
const caret = e.target.closest('.item-actions__mark-read-caret');
const caret = e.target.closest('.ms-item-actions__mark-read-caret');
if (!caret) return;
e.preventDefault();
e.stopPropagation();
// Close other open popovers
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
if (p !== caret.nextElementSibling) p.hidden = true;
}
@@ -193,7 +193,7 @@
// Handle mark-source-read button
timeline.addEventListener('click', async (e) => {
const button = e.target.closest('.item-actions__mark-source-read');
const button = e.target.closest('.ms-item-actions__mark-source-read');
if (!button) return;
e.preventDefault();
@@ -220,7 +220,7 @@
if (response.ok) {
// Animate out all cards from this feed
const cards = timeline.querySelectorAll(`.item-card[data-feed-id="${feedId}"]`);
const cards = timeline.querySelectorAll(`.ms-item-card[data-feed-id="${feedId}"]`);
for (const card of cards) {
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
card.style.opacity = '0';
@@ -230,7 +230,7 @@
for (const card of [...cards]) {
card.remove();
}
if (timeline.querySelectorAll('.item-card').length === 0) {
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
location.reload();
}
}, 300);
@@ -245,8 +245,8 @@
// Close popovers on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('.item-actions__mark-read-group')) {
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
if (!e.target.closest('.ms-item-actions__mark-read-group')) {
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
p.hidden = true;
}
}
@@ -254,7 +254,7 @@
// Handle save-for-later buttons
timeline.addEventListener('click', async (e) => {
const button = e.target.closest('.item-actions__save-later');
const button = e.target.closest('.ms-item-actions__save-later');
if (!button) return;
e.preventDefault();
@@ -275,7 +275,7 @@
});
if (response.ok) {
button.classList.add('item-actions__save-later--saved');
button.classList.add('ms-item-actions__save-later--saved');
button.title = 'Saved';
} else {
button.disabled = false;

View File

@@ -1,7 +1,7 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="compose">
<div class="ms-compose">
<a href="{{ backUrl or (baseUrl + '/channels') }}" class="back-link">
{{ icon("previous") }} {{ __("Back") }}
</a>
@@ -9,7 +9,7 @@
<h2>{{ __("microsub.compose.title") }}</h2>
{% if replyTo and replyTo is string %}
<div class="compose__context">
<div class="ms-compose__context">
{{ icon("reply") }} {{ __("microsub.compose.replyTo") }}:
<a href="{{ replyTo }}" target="_blank" rel="noopener">
{{ replyTo | replace("https://", "") | replace("http://", "") }}
@@ -18,7 +18,7 @@
{% endif %}
{% if likeOf and likeOf is string %}
<div class="compose__context">
<div class="ms-compose__context">
{{ icon("like") }} {{ __("microsub.compose.likeOf") }}:
<a href="{{ likeOf }}" target="_blank" rel="noopener">
{{ likeOf | replace("https://", "") | replace("http://", "") }}
@@ -27,7 +27,7 @@
{% endif %}
{% if repostOf and repostOf is string %}
<div class="compose__context">
<div class="ms-compose__context">
{{ icon("repost") }} {{ __("microsub.compose.repostOf") }}:
<a href="{{ repostOf }}" target="_blank" rel="noopener">
{{ repostOf | replace("https://", "") | replace("http://", "") }}
@@ -36,7 +36,7 @@
{% endif %}
{% if bookmarkOf and bookmarkOf is string %}
<div class="compose__context">
<div class="ms-compose__context">
{{ icon("bookmark") }} {{ __("microsub.compose.bookmarkOf") }}:
<a href="{{ bookmarkOf }}" target="_blank" rel="noopener">
{{ bookmarkOf | replace("https://", "") | replace("http://", "") }}
@@ -68,13 +68,13 @@
attributes: { autofocus: true },
hint: __("microsub.compose.commentHint") if isAction else false
}) }}
<div class="compose__counter">
<div class="ms-compose__counter">
<span id="char-count">0</span> characters
</div>
{# Syndication targets #}
{% if syndicationTargets and syndicationTargets.length %}
<fieldset class="compose__syndication">
<fieldset class="ms-compose__syndication">
<legend>{{ __("microsub.compose.syndicateTo") }}</legend>
<p class="hint">{{ __("microsub.compose.syndicateHint") }}</p>
{% for target in syndicationTargets %}

View File

@@ -1,7 +1,7 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="settings">
<div class="ms-settings">
<header>
<a href="{{ baseUrl }}/deck" class="back-link">
{{ __("microsub.views.deck") }}
@@ -12,13 +12,13 @@
<form action="{{ baseUrl }}/deck/settings" method="POST">
<p>Select which channels appear as columns in your deck, and their order.</p>
<div class="deck-settings__channels">
<div class="ms-deck-settings__channels">
{% for channel in channels %}
{% if channel.uid !== "notifications" %}
<label class="deck-settings__channel">
<label class="ms-deck-settings__channel">
<input type="checkbox" name="columns" value="{{ channel._id }}"
{% if channel._id.toString() in selectedIds %}checked{% endif %}>
<span class="timeline-view__filter-color" style="background: {{ channel.color }}"></span>
<span class="ms-timeline-view__filter-color" style="background: {{ channel.color }}"></span>
{{ channel.name }}
</label>
{% endif %}

View File

@@ -1,8 +1,8 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="deck">
<header class="deck__header">
<div class="ms-deck">
<header class="ms-deck__header">
<h1>{{ __("microsub.views.deck") }}</h1>
<a href="{{ baseUrl }}/deck/settings" class="button button--secondary button--small">
Configure columns
@@ -10,29 +10,29 @@
</header>
{% if columns.length > 0 %}
<div class="deck__columns">
<div class="ms-deck__columns">
{% for col in columns %}
<div class="deck__column" data-channel-uid="{{ col.channel.uid }}">
<div class="deck__column-header" style="border-top: 3px solid {{ col.channel.color or '#ccc' }}">
<a href="{{ baseUrl }}/channels/{{ col.channel.uid }}" class="deck__column-name">
<div class="ms-deck__column" data-channel-uid="{{ col.channel.uid }}">
<div class="ms-deck__column-header" style="border-top: 3px solid {{ col.channel.color or '#ccc' }}">
<a href="{{ baseUrl }}/channels/{{ col.channel.uid }}" class="ms-deck__column-name">
{{ col.channel.name }}
</a>
{% if col.channel.unread %}
<span class="reader__channel-badge{% if col.channel.unread === true %} reader__channel-badge--dot{% endif %}">
<span class="ms-reader__channel-badge{% if col.channel.unread === true %} ms-reader__channel-badge--dot{% endif %}">
{% if col.channel.unread !== true %}{{ col.channel.unread }}{% endif %}
</span>
{% endif %}
</div>
<div class="deck__column-items">
<div class="ms-deck__column-items">
{% for item in col.items %}
{% include "partials/item-card-compact.njk" %}
{% endfor %}
{% if col.items.length === 0 %}
<p class="deck__column-empty">No unread items</p>
<p class="ms-deck__column-empty">No unread items</p>
{% endif %}
{% if col.paging and col.paging.after %}
<a href="{{ baseUrl }}/channels/{{ col.channel.uid }}"
class="deck__column-more button button--secondary button--small">
class="ms-deck__column-more button button--secondary button--small">
View more
</a>
{% endif %}
@@ -41,7 +41,7 @@
{% endfor %}
</div>
{% else %}
<div class="reader__empty">
<div class="ms-reader__empty">
<p>No columns configured. Add channels to your deck.</p>
<a href="{{ baseUrl }}/deck/settings" class="button button--primary">
Configure deck

View File

@@ -1,7 +1,7 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="settings">
<div class="ms-settings">
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="back-link">
{{ icon("previous") }} {{ __("microsub.feeds.title") }}
</a>
@@ -9,20 +9,20 @@
<h2>{{ __("microsub.feeds.edit") }}</h2>
{% if error %}
<div class="notice notice--error">
<div class="ms-notice ms-notice--error">
<p>{{ error }}</p>
</div>
{% endif %}
<div class="feed-edit">
<div class="feed-edit__current">
<div class="ms-feed-edit">
<div class="ms-feed-edit__current">
<h3>Current Feed</h3>
<p class="feed-edit__url">{{ feed.url }}</p>
<p class="ms-feed-edit__url">{{ feed.url }}</p>
{% if feed.title %}
<p class="feed-edit__title">{{ feed.title }}</p>
<p class="ms-feed-edit__title">{{ feed.title }}</p>
{% endif %}
{% if feed.status == 'error' %}
<div class="notice notice--error">
<div class="ms-notice ms-notice--error">
<p><strong>Status:</strong> Error</p>
{% if feed.lastError %}
<p><strong>Last error:</strong> {{ feed.lastError }}</p>
@@ -34,7 +34,7 @@
{% endif %}
</div>
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/edit" class="feed-edit__form">
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/edit" class="ms-feed-edit__form">
{{ input({
id: "url",
name: "url",
@@ -46,7 +46,7 @@
autocomplete: "off"
}) }}
<p class="feed-edit__help">
<p class="ms-feed-edit__help">
Enter the direct URL to the RSS, Atom, or JSON Feed. The URL will be validated before updating.
</p>
@@ -60,10 +60,10 @@
<div class="divider"></div>
<div class="feed-edit__actions">
<div class="ms-feed-edit__actions">
<h3>Other Actions</h3>
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/rediscover" class="feed-edit__action">
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/rediscover" class="ms-feed-edit__action">
<p>Run feed discovery on the current URL to find the actual RSS/Atom feed.</p>
{{ button({
text: "Rediscover Feed",
@@ -71,7 +71,7 @@
}) }}
</form>
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/refresh" class="feed-edit__action">
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/refresh" class="ms-feed-edit__action">
<p>Force refresh this feed now.</p>
{{ button({
text: "Refresh Now",

View File

@@ -1,8 +1,8 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="feeds">
<header class="feeds__header">
<div class="ms-feeds">
<header class="ms-feeds__header">
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="back-link">
{{ icon("previous") }} {{ channel.name }}
</a>
@@ -11,27 +11,26 @@
<h2>{{ __("microsub.feeds.title") }}</h2>
{% if error %}
<div class="notice notice--error" role="alert">
<div class="ms-notice ms-notice--error" role="alert">
{{ error }}
</div>
{% endif %}
{% if feeds.length > 0 %}
<div class="feeds__list">
<div class="ms-feeds__list">
{% for feed in feeds %}
<div class="feeds__item{% if feed.status == 'error' %} feeds__item--error{% endif %}">
<div class="feeds__info">
<div class="ms-feeds__item{% if feed.status == 'error' %} ms-feeds__item--error{% endif %}">
<div class="ms-feeds__info">
{% if feed.photo %}
<img src="{{ feed.photo }}"
alt=""
class="feeds__photo"
class="ms-feeds__photo"
width="48"
height="48"
loading="lazy"
onerror="this.style.display='none'">
loading="lazy">
{% endif %}
<div class="feeds__details">
<span class="feeds__name">
<div class="ms-feeds__details">
<span class="ms-feeds__name">
{{ feed.title or feed.url }}
{% if feed.feedType %}
<span class="badge badge--offset badge--small" title="Feed format">{{ feed.feedType | upper }}</span>
@@ -42,21 +41,21 @@
<span class="badge badge--green">Active</span>
{% endif %}
</span>
<a href="{{ feed.url }}" class="feeds__url" target="_blank" rel="noopener">
<a href="{{ feed.url }}" class="ms-feeds__url" target="_blank" rel="noopener">
{{ feed.url | replace("https://", "") | replace("http://", "") }}
</a>
{% if feed.lastError %}
<span class="feeds__error">{{ feed.lastError }}</span>
<span class="ms-feeds__error">{{ feed.lastError }}</span>
{% endif %}
{% if feed.consecutiveErrors > 0 %}
<span class="feeds__error-count">{{ feed.consecutiveErrors }} consecutive errors</span>
<span class="ms-feeds__error-count">{{ feed.consecutiveErrors }} consecutive errors</span>
{% endif %}
{% if feed.lastSuccessAt %}
<span class="feeds__meta">Last success: {{ feed.lastSuccessAt }}</span>
<span class="ms-feeds__meta">Last success: {{ feed.lastSuccessAt }}</span>
{% endif %}
</div>
</div>
<div class="feeds__actions">
<div class="ms-feeds__actions">
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/edit"
class="button button--secondary button--small"
title="Edit feed URL">
@@ -83,15 +82,15 @@
{% endfor %}
</div>
{% else %}
<div class="reader__empty">
<div class="ms-reader__empty">
{{ icon("syndicate") }}
<p>{{ __("microsub.feeds.empty") }}</p>
</div>
{% endif %}
<div class="feeds__add">
<div class="ms-feeds__add">
<h3>{{ __("microsub.feeds.follow") }}</h3>
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="feeds__form">
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="ms-feeds__form">
{{ input({
id: "url",
name: "url",

View File

@@ -1,24 +1,23 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<article class="item">
<article class="ms-item">
<a href="{{ backUrl or (baseUrl + '/channels') }}" class="back-link">
{{ icon("previous") }} {{ __("Back") }}
</a>
{% if item.author %}
<header class="item__author">
<header class="ms-item__author">
{% if item.author.photo %}
<img src="{{ item.author.photo }}"
alt=""
class="item__author-photo"
class="ms-item__author-photo"
width="48"
height="48"
loading="lazy"
onerror="this.style.display='none'">
loading="lazy">
{% endif %}
<div class="item__author-info">
<span class="item__author-name">
<div class="ms-item__author-info">
<span class="ms-item__author-name">
{% if item.author.url %}
<a href="{{ item.author.url }}" target="_blank" rel="noopener">{{ item.author.name or item.author.url }}</a>
{% else %}
@@ -26,7 +25,7 @@
{% endif %}
</span>
{% if item.published %}
<time datetime="{{ item.published }}" class="item__date">
<time datetime="{{ item.published }}" class="ms-item__date">
{{ item.published | date("PPPp", { locale: locale, timeZone: application.timeZone }) }}
</time>
{% endif %}
@@ -36,9 +35,9 @@
{# Context for interactions #}
{% if item["in-reply-to"] or item["like-of"] or item["repost-of"] or item["bookmark-of"] %}
<div class="item__context">
<div class="ms-item__context">
{% if item["in-reply-to"] and item["in-reply-to"].length > 0 %}
<p class="item__context-label">
<p class="ms-item__context-label">
{{ icon("reply") }} {{ __("Reply to") }}:
<a href="{{ item['in-reply-to'][0] }}" target="_blank" rel="noopener">
{{ item["in-reply-to"][0] | replace("https://", "") | replace("http://", "") }}
@@ -46,7 +45,7 @@
</p>
{% endif %}
{% if item["like-of"] and item["like-of"].length > 0 %}
<p class="item__context-label">
<p class="ms-item__context-label">
{{ icon("like") }} {{ __("Liked") }}:
<a href="{{ item['like-of'][0] }}" target="_blank" rel="noopener">
{{ item["like-of"][0] | replace("https://", "") | replace("http://", "") }}
@@ -54,7 +53,7 @@
</p>
{% endif %}
{% if item["repost-of"] and item["repost-of"].length > 0 %}
<p class="item__context-label">
<p class="ms-item__context-label">
{{ icon("repost") }} {{ __("Reposted") }}:
<a href="{{ item['repost-of'][0] }}" target="_blank" rel="noopener">
{{ item["repost-of"][0] | replace("https://", "") | replace("http://", "") }}
@@ -62,7 +61,7 @@
</p>
{% endif %}
{% if item["bookmark-of"] and item["bookmark-of"].length > 0 %}
<p class="item__context-label">
<p class="ms-item__context-label">
{{ icon("bookmark") }} {{ __("Bookmarked") }}:
<a href="{{ item['bookmark-of'][0] }}" target="_blank" rel="noopener">
{{ item["bookmark-of"][0] | replace("https://", "") | replace("http://", "") }}
@@ -73,11 +72,11 @@
{% endif %}
{% if item.name %}
<h2 class="item__title">{{ item.name }}</h2>
<h2 class="ms-item__title">{{ item.name }}</h2>
{% endif %}
{% if item.content %}
<div class="item__content prose">
<div class="ms-item__content prose">
{% if item.content.html %}
{{ item.content.html | safe }}
{% else %}
@@ -88,19 +87,19 @@
{# Categories #}
{% if item.category and item.category.length > 0 %}
<div class="item-card__categories">
<div class="ms-item-card__categories">
{% for cat in item.category %}
<span class="item-card__category">#{{ cat | replace("#", "") }}</span>
<span class="ms-item-card__category">#{{ cat | replace("#", "") }}</span>
{% endfor %}
</div>
{% endif %}
{# Photos #}
{% if item.photo and item.photo.length > 0 %}
<div class="item__photos">
<div class="ms-item__photos">
{% for photo in item.photo %}
<a href="{{ photo }}" target="_blank" rel="noopener">
<img src="{{ photo }}" alt="" class="item__photo" loading="lazy">
<img src="{{ photo }}" alt="" class="ms-item__photo" loading="lazy">
</a>
{% endfor %}
</div>
@@ -108,7 +107,7 @@
{# Video #}
{% if item.video and item.video.length > 0 %}
<div class="item__media">
<div class="ms-item__media">
{% for video in item.video %}
<video src="{{ video }}"
controls
@@ -121,14 +120,14 @@
{# Audio #}
{% if item.audio and item.audio.length > 0 %}
<div class="item__media">
<div class="ms-item__media">
{% for audio in item.audio %}
<audio src="{{ audio }}" controls preload="metadata"></audio>
{% endfor %}
</div>
{% endif %}
<footer class="item__actions">
<footer class="ms-item__actions">
{% if item.url %}
<a href="{{ item.url }}" class="button button--secondary button--small" target="_blank" rel="noopener">
{{ icon("external") }} {{ __("microsub.item.viewOriginal") }}
@@ -148,7 +147,7 @@
</a>
{% if not item._is_read %}
<button type="button"
class="button button--secondary button--small item__mark-read"
class="button button--secondary button--small ms-item__mark-read"
data-item-id="{{ item._id }}"
data-channel="{{ channel.uid }}">
{{ icon("checkboxChecked") }} {{ __("microsub.timeline.markRead") }}
@@ -159,7 +158,7 @@
<script type="module">
// Handle mark-read button
const markReadBtn = document.querySelector('.item__mark-read');
const markReadBtn = document.querySelector('.ms-item__mark-read');
if (markReadBtn) {
markReadBtn.addEventListener('click', async () => {
const itemId = markReadBtn.dataset.itemId;

View File

@@ -1,18 +1,18 @@
{# Item action buttons #}
<div class="item-actions">
<a href="{{ baseUrl }}/compose?replyTo={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.reply') }}">
<div class="ms-item-actions">
<a href="{{ baseUrl }}/compose?replyTo={{ itemUrl | urlencode }}" class="ms-item-actions__button" title="{{ __('microsub.item.reply') }}">
{{ icon("reply") }}
</a>
<a href="{{ baseUrl }}/compose?likeOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.like') }}">
<a href="{{ baseUrl }}/compose?likeOf={{ itemUrl | urlencode }}" class="ms-item-actions__button" title="{{ __('microsub.item.like') }}">
{{ icon("like") }}
</a>
<a href="{{ baseUrl }}/compose?repostOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.repost') }}">
<a href="{{ baseUrl }}/compose?repostOf={{ itemUrl | urlencode }}" class="ms-item-actions__button" title="{{ __('microsub.item.repost') }}">
{{ icon("repost") }}
</a>
<a href="{{ baseUrl }}/compose?bookmarkOf={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.bookmark') }}">
<a href="{{ baseUrl }}/compose?bookmarkOf={{ itemUrl | urlencode }}" class="ms-item-actions__button" title="{{ __('microsub.item.bookmark') }}">
{{ icon("bookmark") }}
</a>
<a href="{{ itemUrl }}" class="item-actions__button" target="_blank" rel="noopener" title="{{ __('microsub.item.viewOriginal') }}">
<a href="{{ itemUrl }}" class="ms-item-actions__button" target="_blank" rel="noopener" title="{{ __('microsub.item.viewOriginal') }}">
{{ icon("public") }}
</a>
</div>

View File

@@ -1,13 +1,13 @@
{# Breadcrumb navigation #}
{% if breadcrumbs and breadcrumbs.length > 0 %}
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol class="breadcrumbs__list">
<nav class="ms-breadcrumbs" aria-label="Breadcrumb">
<ol class="ms-breadcrumbs__list">
{% for crumb in breadcrumbs %}
<li class="breadcrumbs__item">
<li class="ms-breadcrumbs__item">
{% if crumb.href %}
<a href="{{ crumb.href }}" class="breadcrumbs__link">{{ crumb.text }}</a>
<a href="{{ crumb.href }}" class="ms-breadcrumbs__link">{{ crumb.text }}</a>
{% else %}
<span class="breadcrumbs__current" aria-current="page">{{ crumb.text }}</span>
<span class="ms-breadcrumbs__current" aria-current="page">{{ crumb.text }}</span>
{% endif %}
</li>
{% endfor %}

View File

@@ -1,37 +1,36 @@
{# Compact item card for deck columns #}
<article class="item-card-compact{% if item._is_read %} item-card-compact--read{% endif %}"
<article class="ms-item-card-compact{% if item._is_read %} ms-item-card-compact--read{% endif %}"
data-item-id="{{ item._id }}">
<a href="{{ readerBaseUrl }}/item/{{ item._id }}" class="item-card-compact__link">
<a href="{{ readerBaseUrl }}/item/{{ item._id }}" class="ms-item-card-compact__link">
{% if item.photo and item.photo.length > 0 %}
<img src="{{ item.photo[0] }}"
alt=""
class="item-card-compact__photo"
loading="lazy"
onerror="this.style.display='none'">
class="ms-item-card-compact__photo"
loading="lazy">
{% endif %}
<div class="item-card-compact__body">
<div class="ms-item-card-compact__body">
{% if item.name %}
<h4 class="item-card-compact__title">{{ item.name }}</h4>
<h4 class="ms-item-card-compact__title">{{ item.name }}</h4>
{% elif item.content %}
<p class="item-card-compact__text">
<p class="ms-item-card-compact__text">
{% if item.content.text %}{{ item.content.text | truncate(80) }}{% elif item.content.html %}{{ item.content.html | safe | striptags | truncate(80) }}{% endif %}
</p>
{% endif %}
<div class="item-card-compact__meta">
<div class="ms-item-card-compact__meta">
{% if item._source %}
<span class="item-card-compact__source">{{ item._source.name or item._source.url }}</span>
<span class="ms-item-card-compact__source">{{ item._source.name or item._source.url }}</span>
{% elif item.author %}
<span class="item-card-compact__source">{{ item.author.name }}</span>
<span class="ms-item-card-compact__source">{{ item.author.name }}</span>
{% endif %}
{% if item.published %}
<time datetime="{{ item.published }}" class="item-card-compact__date">
<time datetime="{{ item.published }}" class="ms-item-card-compact__date">
{{ item.published | date("PP") }}
</time>
{% endif %}
</div>
</div>
{% if not item._is_read %}
<span class="item-card-compact__unread" aria-label="Unread"></span>
<span class="ms-item-card-compact__unread" aria-label="Unread"></span>
{% endif %}
</a>
</article>

View File

@@ -2,7 +2,7 @@
Item card for timeline display
Inspired by Aperture/Monocle reader
#}
<article class="item-card{% if item._is_read %} item-card--read{% endif %}"
<article class="ms-item-card{% if item._is_read %} ms-item-card--read{% endif %}"
data-item-id="{{ item._id }}"
data-feed-id="{{ item._feedId or '' }}"
data-is-read="{{ item._is_read | default(false) }}">
@@ -13,7 +13,7 @@
{% if item["like-of"] and item["like-of"].length > 0 %}
{% set contextUrl = item['like-of'][0].url or item['like-of'][0].value or item['like-of'][0] %}
<div class="item-card__context">
<div class="ms-item-card__context">
{{ icon("like") }}
<span>Liked</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -22,7 +22,7 @@
</div>
{% elif item["repost-of"] and item["repost-of"].length > 0 %}
{% set contextUrl = item['repost-of'][0].url or item['repost-of'][0].value or item['repost-of'][0] %}
<div class="item-card__context">
<div class="ms-item-card__context">
{{ icon("repost") }}
<span>Reposted</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -31,7 +31,7 @@
</div>
{% elif item["in-reply-to"] and item["in-reply-to"].length > 0 %}
{% set contextUrl = item['in-reply-to'][0].url or item['in-reply-to'][0].value or item['in-reply-to'][0] %}
<div class="item-card__context">
<div class="ms-item-card__context">
{{ icon("reply") }}
<span>Reply to</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -40,7 +40,7 @@
</div>
{% elif item["bookmark-of"] and item["bookmark-of"].length > 0 %}
{% set contextUrl = item['bookmark-of'][0].url or item['bookmark-of'][0].value or item['bookmark-of'][0] %}
<div class="item-card__context">
<div class="ms-item-card__context">
{{ icon("bookmark") }}
<span>Bookmarked</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -49,43 +49,24 @@
</div>
{% endif %}
<a href="{{ baseUrl }}/item/{{ item._id }}" class="item-card__link">
<a href="{{ baseUrl }}/item/{{ item._id }}" class="ms-item-card__link">
{# Author #}
{% if item.author %}
<div class="item-card__author">
<div class="ms-item-card__author">
{% if item.author.photo %}
<img src="{{ item.author.photo }}"
alt=""
class="item-card__author-photo"
class="ms-item-card__author-photo"
width="40"
height="40"
loading="lazy"
onerror="this.style.display='none'">
loading="lazy">
{% endif %}
<div class="item-card__author-info">
<span class="item-card__author-name">{{ item.author.name or "Unknown" }}</span>
<div class="ms-item-card__author-info">
<span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span>
{% if item._source %}
<span class="item-card__source">
{# Protocol source indicator #}
{% set sourceUrl = item._source.url or item.author.url or "" %}
{% set sourceType = item._source.source_type or item._source.type %}
{% if sourceType == "activitypub" or sourceType == "mastodon" or ("mastodon." in sourceUrl) or ("mstdn." in sourceUrl) or ("fosstodon." in sourceUrl) or ("pleroma." in sourceUrl) or ("misskey." in sourceUrl) %}
<svg class="item-card__source-icon" viewBox="0 0 24 24" fill="#6364ff" aria-label="Fediverse" style="width:12px;height:12px;vertical-align:middle;margin-right:3px;display:inline-block">
<path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.668 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/>
</svg>
{% elif sourceType == "bluesky" or ("bsky.app" in sourceUrl) or ("bluesky" in sourceUrl) %}
<svg class="item-card__source-icon" viewBox="0 0 568 501" fill="#0085ff" aria-label="ATmosphere" style="width:12px;height:12px;vertical-align:middle;margin-right:3px;display:inline-block">
<path d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/>
</svg>
{% else %}
<svg class="item-card__source-icon" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" aria-label="Web" style="width:12px;height:12px;vertical-align:middle;margin-right:3px;display:inline-block">
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
{% endif %}
{{ item._source.name or item._source.url }}
</span>
<span class="ms-item-card__source">{{ item._source.name or item._source.url }}</span>
{% elif item.author.url %}
<span class="item-card__source">{{ item.author.url | replace("https://", "") | replace("http://", "") }}</span>
<span class="ms-item-card__source">{{ item.author.url | replace("https://", "") | replace("http://", "") }}</span>
{% endif %}
</div>
</div>
@@ -93,12 +74,12 @@
{# Title (for articles) #}
{% if item.name %}
<h3 class="item-card__title">{{ item.name }}</h3>
<h3 class="ms-item-card__title">{{ item.name }}</h3>
{% endif %}
{# Content with overflow handling #}
{% if item.summary or item.content %}
<div class="item-card__content{% if (item.content.text or item.summary or '') | length > 300 %} item-card__content--truncated{% endif %}">
<div class="ms-item-card__content{% if (item.content.text or item.summary or '') | length > 300 %} ms-item-card__content--truncated{% endif %}">
{% if item.content.html %}
{{ item.content.html | safe | striptags | truncate(400) }}
{% elif item.content.text %}
@@ -111,10 +92,10 @@
{# Categories/Tags #}
{% if item.category and item.category.length > 0 %}
<div class="item-card__categories">
<div class="ms-item-card__categories">
{% for cat in item.category %}
{% if loop.index0 < 5 %}
<span class="item-card__category">#{{ cat | replace("#", "") }}</span>
<span class="ms-item-card__category">#{{ cat | replace("#", "") }}</span>
{% endif %}
{% endfor %}
</div>
@@ -123,14 +104,13 @@
{# Photo grid (Aperture multi-photo pattern) #}
{% if item.photo and item.photo.length > 0 %}
{% set photoCount = item.photo.length if item.photo.length <= 4 else 4 %}
<div class="item-card__photos item-card__photos--{{ photoCount }}">
<div class="ms-item-card__photos ms-item-card__photos--{{ photoCount }}">
{% for photo in item.photo %}
{% if loop.index0 < 4 %}
<img src="{{ photo }}"
alt=""
class="item-card__photo"
loading="lazy"
onerror="this.parentElement.removeChild(this)">
class="ms-item-card__photo"
loading="lazy">
{% endif %}
{% endfor %}
</div>
@@ -138,9 +118,9 @@
{# Video preview #}
{% if item.video and item.video.length > 0 %}
<div class="item-card__media">
<div class="ms-item-card__media">
<video src="{{ item.video[0] }}"
class="item-card__video"
class="ms-item-card__video"
controls
preload="metadata"
{% if item.photo and item.photo.length > 0 %}poster="{{ item.photo[0] }}"{% endif %}>
@@ -150,74 +130,74 @@
{# Audio preview #}
{% if item.audio and item.audio.length > 0 %}
<div class="item-card__media">
<audio src="{{ item.audio[0] }}" class="item-card__audio" controls preload="metadata"></audio>
<div class="ms-item-card__media">
<audio src="{{ item.audio[0] }}" class="ms-item-card__audio" controls preload="metadata"></audio>
</div>
{% endif %}
{# Footer with date and actions #}
<footer class="item-card__footer">
<footer class="ms-item-card__footer">
{% if item.published %}
<time datetime="{{ item.published }}" class="item-card__date">
<time datetime="{{ item.published }}" class="ms-item-card__date">
{{ item.published | date("PP", { locale: locale, timeZone: application.timeZone }) }}
</time>
{% endif %}
{% if not item._is_read %}
<span class="item-card__unread" aria-label="Unread">●</span>
<span class="ms-item-card__unread" aria-label="Unread">●</span>
{% endif %}
</footer>
</a>
{# Inline actions (Aperture pattern) #}
<div class="item-actions">
<div class="ms-item-actions">
{% if item._source and item._source.type === "activitypub" and item.author and item.author.url %}
<a href="{{ baseUrl }}/actor?url={{ item.author.url | urlencode }}" class="item-actions__button" title="View actor profile">
<a href="{{ baseUrl }}/actor?url={{ item.author.url | urlencode }}" class="ms-item-actions__button" title="View actor profile">
{{ icon("mention") }}
<span class="visually-hidden">Actor profile</span>
<span class="-!-visually-hidden">Actor profile</span>
</a>
{% endif %}
{% if item.url %}
<a href="{{ item.url }}" class="item-actions__button" target="_blank" rel="noopener" title="View original">
<a href="{{ item.url }}" class="ms-item-actions__button" target="_blank" rel="noopener" title="View original">
{{ icon("external") }}
<span class="visually-hidden">Original</span>
<span class="-!-visually-hidden">Original</span>
</a>
{% endif %}
<a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="item-actions__button" title="Reply">
<a href="{{ baseUrl }}/compose?reply={{ item.url | urlencode }}" class="ms-item-actions__button" title="Reply">
{{ icon("reply") }}
<span class="visually-hidden">Reply</span>
<span class="-!-visually-hidden">Reply</span>
</a>
<a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="item-actions__button" title="Like">
<a href="{{ baseUrl }}/compose?like={{ item.url | urlencode }}" class="ms-item-actions__button" title="Like">
{{ icon("like") }}
<span class="visually-hidden">Like</span>
<span class="-!-visually-hidden">Like</span>
</a>
<a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="item-actions__button" title="Repost">
<a href="{{ baseUrl }}/compose?repost={{ item.url | urlencode }}" class="ms-item-actions__button" title="Repost">
{{ icon("repost") }}
<span class="visually-hidden">Repost</span>
<span class="-!-visually-hidden">Repost</span>
</a>
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="item-actions__button" title="Bookmark">
<a href="{{ baseUrl }}/compose?bookmark={{ item.url | urlencode }}" class="ms-item-actions__button" title="Bookmark">
{{ icon("bookmark") }}
<span class="visually-hidden">Bookmark</span>
<span class="-!-visually-hidden">Bookmark</span>
</a>
{% if not item._is_read %}
{% if item._feedId %}
<span class="item-actions__mark-read-group">
<span class="ms-item-actions__mark-read-group">
<button type="button"
class="item-actions__button item-actions__mark-read"
class="ms-item-actions__button ms-item-actions__mark-read"
data-action="mark-read"
data-item-id="{{ item._id }}"
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
title="Mark as read">
{{ icon("checkboxChecked") }}
<span class="visually-hidden">Mark read</span>
<span class="-!-visually-hidden">Mark read</span>
</button>
<button type="button"
class="item-actions__button item-actions__mark-read-caret"
class="ms-item-actions__button ms-item-actions__mark-read-caret"
aria-label="More mark-read options"
title="More options">&#9662;</button>
<div class="item-actions__mark-read-popover" hidden>
<div class="ms-item-actions__mark-read-popover" hidden>
<button type="button"
class="item-actions__mark-source-read"
class="ms-item-actions__mark-source-read"
data-feed-id="{{ item._feedId }}"
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}>
@@ -227,26 +207,26 @@
</span>
{% else %}
<button type="button"
class="item-actions__button item-actions__mark-read"
class="ms-item-actions__button ms-item-actions__mark-read"
data-action="mark-read"
data-item-id="{{ item._id }}"
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
title="Mark as read">
{{ icon("checkboxChecked") }}
<span class="visually-hidden">Mark read</span>
<span class="-!-visually-hidden">Mark read</span>
</button>
{% endif %}
{% endif %}
{% if application.readlaterEndpoint %}
<button type="button"
class="item-actions__button item-actions__save-later"
class="ms-item-actions__button ms-item-actions__save-later"
data-action="save-later"
data-url="{{ item.url }}"
data-title="{{ item.name or '' }}"
title="Save for later">
{{ icon("bookmark") }}
<span class="visually-hidden">Save for later</span>
<span class="-!-visually-hidden">Save for later</span>
</button>
{% endif %}
</div>

View File

@@ -1,5 +1,5 @@
{# Timeline of items #}
<div class="timeline">
<div class="ms-timeline">
{% if items.length > 0 %}
{% for item in items %}
{% include "partials/item-card.njk" %}

View File

@@ -1,7 +1,7 @@
{# View mode switcher - icon toolbar #}
<nav class="view-switcher" aria-label="View mode">
<nav class="ms-view-switcher" aria-label="View mode">
<a href="{{ readerBaseUrl }}/channels"
class="view-switcher__button{% if activeView === 'channels' %} view-switcher__button--active{% endif %}"
class="ms-view-switcher__button{% if activeView === 'channels' %} ms-view-switcher__button--active{% endif %}"
title="Channels">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/>
@@ -9,14 +9,14 @@
</svg>
</a>
<a href="{{ readerBaseUrl }}/deck"
class="view-switcher__button{% if activeView === 'deck' %} view-switcher__button--active{% endif %}"
class="ms-view-switcher__button{% if activeView === 'deck' %} ms-view-switcher__button--active{% endif %}"
title="Deck">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="18" rx="1"/><rect x="14" y="3" width="7" height="18" rx="1"/>
</svg>
</a>
<a href="{{ readerBaseUrl }}/timeline"
class="view-switcher__button{% if activeView === 'timeline' %} view-switcher__button--active{% endif %}"
class="ms-view-switcher__button{% if activeView === 'timeline' %} ms-view-switcher__button--active{% endif %}"
title="Timeline">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="22"/><polyline points="19 15 12 22 5 15"/>

View File

@@ -1,26 +1,26 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="reader">
<div class="ms-reader">
{% if channels.length > 0 %}
<div class="reader__channels">
<div class="ms-reader__channels">
{% for channel in channels %}
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="reader__channel{% if channel.uid === currentChannel %} reader__channel--active{% endif %}">
<span class="reader__channel-name">
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="ms-reader__channel{% if channel.uid === currentChannel %} ms-reader__channel--active{% endif %}">
<span class="ms-reader__channel-name">
{% if channel.uid === "notifications" %}
{{ icon("mention") }}
{% endif %}
{{ channel.name }}
</span>
{% if channel.unread %}
<span class="reader__channel-badge{% if channel.unread === true %} reader__channel-badge--dot{% endif %}">
<span class="ms-reader__channel-badge{% if channel.unread === true %} ms-reader__channel-badge--dot{% endif %}">
{% if channel.unread !== true %}{{ channel.unread }}{% endif %}
</span>
{% endif %}
</a>
{% endfor %}
</div>
<div class="reader__actions">
<div class="ms-reader__actions">
<a href="{{ baseUrl }}/search" class="button button--primary">
{{ icon("syndicate") }} {{ __("microsub.feeds.follow") }}
</a>
@@ -29,7 +29,7 @@
</a>
</div>
{% else %}
<div class="reader__empty">
<div class="ms-reader__empty">
{{ icon("syndicate") }}
<p>{{ __("microsub.channels.empty") }}</p>
<a href="{{ baseUrl }}/channels/new" class="button button--primary">

View File

@@ -1,14 +1,14 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="search">
<div class="ms-search">
<a href="{{ baseUrl }}/channels" class="back-link">
{{ icon("previous") }} {{ __("microsub.channels.title") }}
</a>
<h2>{{ __("microsub.search.title") }}</h2>
<form method="post" action="{{ baseUrl }}/search" class="search__form">
<form method="post" action="{{ baseUrl }}/search" class="ms-search__form">
{{ input({
id: "query",
name: "query",
@@ -26,42 +26,42 @@
</form>
{% if validationError %}
<div class="notice notice--error">
<div class="ms-notice ms-notice--error">
<p>{{ validationError }}</p>
</div>
{% endif %}
{% if discoveryError %}
<div class="notice notice--error">
<div class="ms-notice ms-notice--error">
<p>{{ discoveryError }}</p>
</div>
{% endif %}
{% if results and results.length > 0 %}
<div class="search__results">
<div class="ms-search__results">
<h3>{{ __("microsub.search.title") }}</h3>
<div class="search__list">
<div class="ms-search__list">
{% for result in results %}
<div class="search__item{% if not result.valid %} search__item--invalid{% endif %}{% if result.isCommentsFeed %} search__item--comments{% endif %}">
<div class="search__feed">
<span class="search__name">
<div class="ms-search__item{% if not result.valid %} ms-search__item--invalid{% endif %}{% if result.isCommentsFeed %} ms-search__item--comments{% endif %}">
<div class="ms-search__feed">
<span class="ms-search__name">
{{ result.title or "Feed" }}
<span class="search__type badge badge--small{% if result.valid %} badge--green{% else %} badge--yellow{% endif %}">
<span class="ms-search__type badge badge--small{% if result.valid %} badge--green{% else %} badge--yellow{% endif %}">
{{ result.typeLabel }}
</span>
{% if result.isCommentsFeed %}
<span class="search__type badge badge--small badge--yellow">Comments</span>
<span class="ms-search__type badge badge--small badge--yellow">Comments</span>
{% endif %}
</span>
<span class="search__url">{{ result.url | replace("https://", "") | replace("http://", "") }}</span>
<span class="ms-search__url">{{ result.url | replace("https://", "") | replace("http://", "") }}</span>
{% if not result.valid %}
<span class="search__error">{{ result.error }}</span>
<span class="ms-search__error">{{ result.error }}</span>
{% endif %}
</div>
{% if result.valid %}
<form method="post" action="{{ baseUrl }}/subscribe" class="search__subscribe">
<form method="post" action="{{ baseUrl }}/subscribe" class="ms-search__subscribe">
<input type="hidden" name="url" value="{{ result.url }}">
<label for="channel-{{ loop.index }}" class="visually-hidden">{{ __("microsub.channels.title") }}</label>
<label for="channel-{{ loop.index }}" class="-!-visually-hidden">{{ __("microsub.channels.title") }}</label>
<select name="channel" id="channel-{{ loop.index }}" class="select select--small">
{% for channel in channels %}
<option value="{{ channel.uid }}">{{ channel.name }}</option>
@@ -80,7 +80,7 @@
</div>
</div>
{% elif searched %}
<div class="reader__empty">
<div class="ms-reader__empty">
<p>{{ __("microsub.search.noResults") }}</p>
</div>
{% endif %}

View File

@@ -1,7 +1,7 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="settings">
<div class="ms-settings">
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="back-link">
{{ icon("previous") }} {{ channel.name }}
</a>

View File

@@ -1,22 +1,22 @@
{% extends "layouts/reader.njk" %}
{% block reader %}
<div class="timeline-view">
<header class="timeline-view__header">
<div class="ms-timeline-view">
<header class="ms-timeline-view__header">
<h1>{{ __("microsub.views.timeline") }}</h1>
<div class="timeline-view__actions">
<div class="ms-timeline-view__actions">
{% if channels.length > 0 %}
<details class="timeline-view__filter">
<details class="ms-timeline-view__filter">
<summary class="button button--secondary button--small">
Filter channels
</summary>
<form action="{{ baseUrl }}/timeline" method="GET" class="timeline-view__filter-form">
<form action="{{ baseUrl }}/timeline" method="GET" class="ms-timeline-view__filter-form">
{% for ch in channels %}
{% if ch.uid !== "notifications" %}
<label class="timeline-view__filter-label">
<label class="ms-timeline-view__filter-label">
<input type="checkbox" name="exclude" value="{{ ch._id }}"
{% if excludeIds and ch._id.toString() in excludeIds %}checked{% endif %}>
<span class="timeline-view__filter-color" style="background: {{ ch.color }}"></span>
<span class="ms-timeline-view__filter-color" style="background: {{ ch.color }}"></span>
{{ ch.name }}
</label>
{% endif %}
@@ -29,11 +29,11 @@
</header>
{% if items.length > 0 %}
<div class="timeline" id="timeline">
<div class="ms-timeline" id="timeline">
{% for item in items %}
<div class="timeline-view__item">
<div class="ms-timeline-view__item">
{% if item._channelName %}
<span class="timeline-view__channel-badge" style="background: {{ item._channelColor or '#888' }}">
<span class="ms-timeline-view__channel-badge" style="background: {{ item._channelColor or '#888' }}">
{{ item._channelName }}
</span>
{% endif %}
@@ -43,7 +43,7 @@
</div>
{% if paging %}
<nav class="timeline__paging" aria-label="Pagination">
<nav class="ms-timeline__paging" aria-label="Pagination">
{% if paging.before %}
<a href="?before={{ paging.before }}" class="button button--secondary">
{{ __("microsub.reader.newer") }}
@@ -60,7 +60,7 @@
{% endif %}
{% else %}
<div class="reader__empty">
<div class="ms-reader__empty">
<p>{{ __("microsub.reader.empty") }}</p>
</div>
{% endif %}
@@ -69,14 +69,14 @@
<script type="module">
const timeline = document.getElementById('timeline');
if (timeline) {
const items = Array.from(timeline.querySelectorAll('.item-card'));
const items = Array.from(timeline.querySelectorAll('.ms-item-card'));
let currentIndex = -1;
function focusItem(index) {
if (items[currentIndex]) items[currentIndex].classList.remove('item-card--focused');
if (items[currentIndex]) items[currentIndex].classList.remove('ms-item-card--focused');
currentIndex = Math.max(0, Math.min(index, items.length - 1));
if (items[currentIndex]) {
items[currentIndex].classList.add('item-card--focused');
items[currentIndex].classList.add('ms-item-card--focused');
items[currentIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
@@ -89,7 +89,7 @@
case 'o': case 'Enter':
e.preventDefault();
if (items[currentIndex]) {
const link = items[currentIndex].querySelector('.item-card__link');
const link = items[currentIndex].querySelector('.ms-item-card__link');
if (link) link.click();
}
break;
@@ -100,7 +100,7 @@
const microsubApiUrl = '{{ baseUrl }}'.replace(/\/reader$/, '');
timeline.addEventListener('click', async (e) => {
const button = e.target.closest('.item-actions__mark-read');
const button = e.target.closest('.ms-item-actions__mark-read');
if (!button) return;
e.preventDefault();
@@ -128,16 +128,16 @@
});
if (response.ok) {
const card = button.closest('.item-card');
const card = button.closest('.ms-item-card');
if (card) {
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
card.style.opacity = '0';
card.style.transform = 'translateX(-20px)';
setTimeout(() => {
const wrapper = card.closest('.timeline-view__item');
const wrapper = card.closest('.ms-timeline-view__item');
if (wrapper) wrapper.remove();
else card.remove();
if (timeline.querySelectorAll('.item-card').length === 0) {
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
location.reload();
}
}, 300);
@@ -154,14 +154,14 @@
// Handle caret toggle for mark-source-read popover
timeline.addEventListener('click', (e) => {
const caret = e.target.closest('.item-actions__mark-read-caret');
const caret = e.target.closest('.ms-item-actions__mark-read-caret');
if (!caret) return;
e.preventDefault();
e.stopPropagation();
// Close other open popovers
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
if (p !== caret.nextElementSibling) p.hidden = true;
}
@@ -171,7 +171,7 @@
// Handle mark-source-read button
timeline.addEventListener('click', async (e) => {
const button = e.target.closest('.item-actions__mark-source-read');
const button = e.target.closest('.ms-item-actions__mark-source-read');
if (!button) return;
e.preventDefault();
@@ -200,7 +200,7 @@
if (response.ok) {
// Animate out all cards from this feed
const cards = timeline.querySelectorAll(`.item-card[data-feed-id="${feedId}"]`);
const cards = timeline.querySelectorAll(`.ms-item-card[data-feed-id="${feedId}"]`);
for (const card of cards) {
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
card.style.opacity = '0';
@@ -208,11 +208,11 @@
}
setTimeout(() => {
for (const card of [...cards]) {
const wrapper = card.closest('.timeline-view__item');
const wrapper = card.closest('.ms-timeline-view__item');
if (wrapper) wrapper.remove();
else card.remove();
}
if (timeline.querySelectorAll('.item-card').length === 0) {
if (timeline.querySelectorAll('.ms-item-card').length === 0) {
location.reload();
}
}, 300);
@@ -227,8 +227,8 @@
// Close popovers on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('.item-actions__mark-read-group')) {
for (const p of timeline.querySelectorAll('.item-actions__mark-read-popover:not([hidden])')) {
if (!e.target.closest('.ms-item-actions__mark-read-group')) {
for (const p of timeline.querySelectorAll('.ms-item-actions__mark-read-popover:not([hidden])')) {
p.hidden = true;
}
}
@@ -236,7 +236,7 @@
// Handle save-for-later buttons
timeline.addEventListener('click', async (e) => {
const button = e.target.closest('.item-actions__save-later');
const button = e.target.closest('.ms-item-actions__save-later');
if (!button) return;
e.preventDefault();
@@ -257,7 +257,7 @@
});
if (response.ok) {
button.classList.add('item-actions__save-later--saved');
button.classList.add('ms-item-actions__save-later--saved');
button.title = 'Saved';
} else {
button.disabled = false;