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", "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.", "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
"keywords": [ "keywords": [
"indiekit", "indiekit",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,18 @@
{# Item action buttons #} {# Item action buttons #}
<div class="item-actions"> <div class="ms-item-actions">
<a href="{{ baseUrl }}/compose?replyTo={{ itemUrl | urlencode }}" class="item-actions__button" title="{{ __('microsub.item.reply') }}"> <a href="{{ baseUrl }}/compose?replyTo={{ itemUrl | urlencode }}" class="ms-item-actions__button" title="{{ __('microsub.item.reply') }}">
{{ icon("reply") }} {{ icon("reply") }}
</a> </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") }} {{ icon("like") }}
</a> </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") }} {{ icon("repost") }}
</a> </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") }} {{ icon("bookmark") }}
</a> </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") }} {{ icon("public") }}
</a> </a>
</div> </div>

View File

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

View File

@@ -1,37 +1,36 @@
{# Compact item card for deck columns #} {# 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 }}"> 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 %} {% if item.photo and item.photo.length > 0 %}
<img src="{{ item.photo[0] }}" <img src="{{ item.photo[0] }}"
alt="" alt=""
class="item-card-compact__photo" class="ms-item-card-compact__photo"
loading="lazy" loading="lazy">
onerror="this.style.display='none'">
{% endif %} {% endif %}
<div class="item-card-compact__body"> <div class="ms-item-card-compact__body">
{% if item.name %} {% 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 %} {% 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 %} {% if item.content.text %}{{ item.content.text | truncate(80) }}{% elif item.content.html %}{{ item.content.html | safe | striptags | truncate(80) }}{% endif %}
</p> </p>
{% endif %} {% endif %}
<div class="item-card-compact__meta"> <div class="ms-item-card-compact__meta">
{% if item._source %} {% 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 %} {% 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 %} {% endif %}
{% if item.published %} {% 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") }} {{ item.published | date("PP") }}
</time> </time>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% if not item._is_read %} {% 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 %} {% endif %}
</a> </a>
</article> </article>

View File

@@ -2,7 +2,7 @@
Item card for timeline display Item card for timeline display
Inspired by Aperture/Monocle reader 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-item-id="{{ item._id }}"
data-feed-id="{{ item._feedId or '' }}" data-feed-id="{{ item._feedId or '' }}"
data-is-read="{{ item._is_read | default(false) }}"> data-is-read="{{ item._is_read | default(false) }}">
@@ -13,7 +13,7 @@
{% if item["like-of"] and item["like-of"].length > 0 %} {% 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] %} {% 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") }} {{ icon("like") }}
<span>Liked</span> <span>Liked</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener"> <a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -22,7 +22,7 @@
</div> </div>
{% elif item["repost-of"] and item["repost-of"].length > 0 %} {% 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] %} {% 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") }} {{ icon("repost") }}
<span>Reposted</span> <span>Reposted</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener"> <a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -31,7 +31,7 @@
</div> </div>
{% elif item["in-reply-to"] and item["in-reply-to"].length > 0 %} {% 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] %} {% 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") }} {{ icon("reply") }}
<span>Reply to</span> <span>Reply to</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener"> <a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -40,7 +40,7 @@
</div> </div>
{% elif item["bookmark-of"] and item["bookmark-of"].length > 0 %} {% 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] %} {% 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") }} {{ icon("bookmark") }}
<span>Bookmarked</span> <span>Bookmarked</span>
<a href="{{ contextUrl }}" target="_blank" rel="noopener"> <a href="{{ contextUrl }}" target="_blank" rel="noopener">
@@ -49,43 +49,24 @@
</div> </div>
{% endif %} {% endif %}
<a href="{{ baseUrl }}/item/{{ item._id }}" class="item-card__link"> <a href="{{ baseUrl }}/item/{{ item._id }}" class="ms-item-card__link">
{# Author #} {# Author #}
{% if item.author %} {% if item.author %}
<div class="item-card__author"> <div class="ms-item-card__author">
{% if item.author.photo %} {% if item.author.photo %}
<img src="{{ item.author.photo }}" <img src="{{ item.author.photo }}"
alt="" alt=""
class="item-card__author-photo" class="ms-item-card__author-photo"
width="40" width="40"
height="40" height="40"
loading="lazy" loading="lazy">
onerror="this.style.display='none'">
{% endif %} {% endif %}
<div class="item-card__author-info"> <div class="ms-item-card__author-info">
<span class="item-card__author-name">{{ item.author.name or "Unknown" }}</span> <span class="ms-item-card__author-name">{{ item.author.name or "Unknown" }}</span>
{% if item._source %} {% if item._source %}
<span class="item-card__source"> <span class="ms-item-card__source">{{ item._source.name or item._source.url }}</span>
{# 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>
{% elif item.author.url %} {% 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 %} {% endif %}
</div> </div>
</div> </div>
@@ -93,12 +74,12 @@
{# Title (for articles) #} {# Title (for articles) #}
{% if item.name %} {% if item.name %}
<h3 class="item-card__title">{{ item.name }}</h3> <h3 class="ms-item-card__title">{{ item.name }}</h3>
{% endif %} {% endif %}
{# Content with overflow handling #} {# Content with overflow handling #}
{% if item.summary or item.content %} {% 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 %} {% if item.content.html %}
{{ item.content.html | safe | striptags | truncate(400) }} {{ item.content.html | safe | striptags | truncate(400) }}
{% elif item.content.text %} {% elif item.content.text %}
@@ -111,10 +92,10 @@
{# Categories/Tags #} {# Categories/Tags #}
{% if item.category and item.category.length > 0 %} {% if item.category and item.category.length > 0 %}
<div class="item-card__categories"> <div class="ms-item-card__categories">
{% for cat in item.category %} {% for cat in item.category %}
{% if loop.index0 < 5 %} {% if loop.index0 < 5 %}
<span class="item-card__category">#{{ cat | replace("#", "") }}</span> <span class="ms-item-card__category">#{{ cat | replace("#", "") }}</span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@@ -123,14 +104,13 @@
{# Photo grid (Aperture multi-photo pattern) #} {# Photo grid (Aperture multi-photo pattern) #}
{% if item.photo and item.photo.length > 0 %} {% if item.photo and item.photo.length > 0 %}
{% set photoCount = item.photo.length if item.photo.length <= 4 else 4 %} {% 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 %} {% for photo in item.photo %}
{% if loop.index0 < 4 %} {% if loop.index0 < 4 %}
<img src="{{ photo }}" <img src="{{ photo }}"
alt="" alt=""
class="item-card__photo" class="ms-item-card__photo"
loading="lazy" loading="lazy">
onerror="this.parentElement.removeChild(this)">
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@@ -138,9 +118,9 @@
{# Video preview #} {# Video preview #}
{% if item.video and item.video.length > 0 %} {% if item.video and item.video.length > 0 %}
<div class="item-card__media"> <div class="ms-item-card__media">
<video src="{{ item.video[0] }}" <video src="{{ item.video[0] }}"
class="item-card__video" class="ms-item-card__video"
controls controls
preload="metadata" preload="metadata"
{% if item.photo and item.photo.length > 0 %}poster="{{ item.photo[0] }}"{% endif %}> {% if item.photo and item.photo.length > 0 %}poster="{{ item.photo[0] }}"{% endif %}>
@@ -150,74 +130,74 @@
{# Audio preview #} {# Audio preview #}
{% if item.audio and item.audio.length > 0 %} {% if item.audio and item.audio.length > 0 %}
<div class="item-card__media"> <div class="ms-item-card__media">
<audio src="{{ item.audio[0] }}" class="item-card__audio" controls preload="metadata"></audio> <audio src="{{ item.audio[0] }}" class="ms-item-card__audio" controls preload="metadata"></audio>
</div> </div>
{% endif %} {% endif %}
{# Footer with date and actions #} {# Footer with date and actions #}
<footer class="item-card__footer"> <footer class="ms-item-card__footer">
{% if item.published %} {% 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 }) }} {{ item.published | date("PP", { locale: locale, timeZone: application.timeZone }) }}
</time> </time>
{% endif %} {% endif %}
{% if not item._is_read %} {% 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 %} {% endif %}
</footer> </footer>
</a> </a>
{# Inline actions (Aperture pattern) #} {# 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 %} {% 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") }} {{ icon("mention") }}
<span class="visually-hidden">Actor profile</span> <span class="-!-visually-hidden">Actor profile</span>
</a> </a>
{% endif %} {% endif %}
{% if item.url %} {% 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") }} {{ icon("external") }}
<span class="visually-hidden">Original</span> <span class="-!-visually-hidden">Original</span>
</a> </a>
{% endif %} {% 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") }} {{ icon("reply") }}
<span class="visually-hidden">Reply</span> <span class="-!-visually-hidden">Reply</span>
</a> </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") }} {{ icon("like") }}
<span class="visually-hidden">Like</span> <span class="-!-visually-hidden">Like</span>
</a> </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") }} {{ icon("repost") }}
<span class="visually-hidden">Repost</span> <span class="-!-visually-hidden">Repost</span>
</a> </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") }} {{ icon("bookmark") }}
<span class="visually-hidden">Bookmark</span> <span class="-!-visually-hidden">Bookmark</span>
</a> </a>
{% if not item._is_read %} {% if not item._is_read %}
{% if item._feedId %} {% if item._feedId %}
<span class="item-actions__mark-read-group"> <span class="ms-item-actions__mark-read-group">
<button type="button" <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-action="mark-read"
data-item-id="{{ item._id }}" data-item-id="{{ item._id }}"
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %} {% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %} {% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
title="Mark as read"> title="Mark as read">
{{ icon("checkboxChecked") }} {{ icon("checkboxChecked") }}
<span class="visually-hidden">Mark read</span> <span class="-!-visually-hidden">Mark read</span>
</button> </button>
<button type="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" aria-label="More mark-read options"
title="More options">&#9662;</button> 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" <button type="button"
class="item-actions__mark-source-read" class="ms-item-actions__mark-source-read"
data-feed-id="{{ item._feedId }}" data-feed-id="{{ item._feedId }}"
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %} {% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}> {% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}>
@@ -227,26 +207,26 @@
</span> </span>
{% else %} {% else %}
<button type="button" <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-action="mark-read"
data-item-id="{{ item._id }}" data-item-id="{{ item._id }}"
{% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %} {% if item._channelUid %}data-channel-uid="{{ item._channelUid }}"{% endif %}
{% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %} {% if item._channelId %}data-channel-id="{{ item._channelId }}"{% endif %}
title="Mark as read"> title="Mark as read">
{{ icon("checkboxChecked") }} {{ icon("checkboxChecked") }}
<span class="visually-hidden">Mark read</span> <span class="-!-visually-hidden">Mark read</span>
</button> </button>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if application.readlaterEndpoint %} {% if application.readlaterEndpoint %}
<button type="button" <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-action="save-later"
data-url="{{ item.url }}" data-url="{{ item.url }}"
data-title="{{ item.name or '' }}" data-title="{{ item.name or '' }}"
title="Save for later"> title="Save for later">
{{ icon("bookmark") }} {{ icon("bookmark") }}
<span class="visually-hidden">Save for later</span> <span class="-!-visually-hidden">Save for later</span>
</button> </button>
{% endif %} {% endif %}
</div> </div>

View File

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

View File

@@ -1,7 +1,7 @@
{# View mode switcher - icon toolbar #} {# 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" <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"> 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"> <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"/> <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> </svg>
</a> </a>
<a href="{{ readerBaseUrl }}/deck" <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"> 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"> <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"/> <rect x="3" y="3" width="7" height="18" rx="1"/><rect x="14" y="3" width="7" height="18" rx="1"/>
</svg> </svg>
</a> </a>
<a href="{{ readerBaseUrl }}/timeline" <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"> 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"> <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"/> <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" %} {% extends "layouts/reader.njk" %}
{% block reader %} {% block reader %}
<div class="reader"> <div class="ms-reader">
{% if channels.length > 0 %} {% if channels.length > 0 %}
<div class="reader__channels"> <div class="ms-reader__channels">
{% for channel in channels %} {% for channel in channels %}
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="reader__channel{% if channel.uid === currentChannel %} reader__channel--active{% endif %}"> <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="ms-reader__channel{% if channel.uid === currentChannel %} ms-reader__channel--active{% endif %}">
<span class="reader__channel-name"> <span class="ms-reader__channel-name">
{% if channel.uid === "notifications" %} {% if channel.uid === "notifications" %}
{{ icon("mention") }} {{ icon("mention") }}
{% endif %} {% endif %}
{{ channel.name }} {{ channel.name }}
</span> </span>
{% if channel.unread %} {% 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 %} {% if channel.unread !== true %}{{ channel.unread }}{% endif %}
</span> </span>
{% endif %} {% endif %}
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div class="reader__actions"> <div class="ms-reader__actions">
<a href="{{ baseUrl }}/search" class="button button--primary"> <a href="{{ baseUrl }}/search" class="button button--primary">
{{ icon("syndicate") }} {{ __("microsub.feeds.follow") }} {{ icon("syndicate") }} {{ __("microsub.feeds.follow") }}
</a> </a>
@@ -29,7 +29,7 @@
</a> </a>
</div> </div>
{% else %} {% else %}
<div class="reader__empty"> <div class="ms-reader__empty">
{{ icon("syndicate") }} {{ icon("syndicate") }}
<p>{{ __("microsub.channels.empty") }}</p> <p>{{ __("microsub.channels.empty") }}</p>
<a href="{{ baseUrl }}/channels/new" class="button button--primary"> <a href="{{ baseUrl }}/channels/new" class="button button--primary">

View File

@@ -1,14 +1,14 @@
{% extends "layouts/reader.njk" %} {% extends "layouts/reader.njk" %}
{% block reader %} {% block reader %}
<div class="search"> <div class="ms-search">
<a href="{{ baseUrl }}/channels" class="back-link"> <a href="{{ baseUrl }}/channels" class="back-link">
{{ icon("previous") }} {{ __("microsub.channels.title") }} {{ icon("previous") }} {{ __("microsub.channels.title") }}
</a> </a>
<h2>{{ __("microsub.search.title") }}</h2> <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({ {{ input({
id: "query", id: "query",
name: "query", name: "query",
@@ -26,42 +26,42 @@
</form> </form>
{% if validationError %} {% if validationError %}
<div class="notice notice--error"> <div class="ms-notice ms-notice--error">
<p>{{ validationError }}</p> <p>{{ validationError }}</p>
</div> </div>
{% endif %} {% endif %}
{% if discoveryError %} {% if discoveryError %}
<div class="notice notice--error"> <div class="ms-notice ms-notice--error">
<p>{{ discoveryError }}</p> <p>{{ discoveryError }}</p>
</div> </div>
{% endif %} {% endif %}
{% if results and results.length > 0 %} {% if results and results.length > 0 %}
<div class="search__results"> <div class="ms-search__results">
<h3>{{ __("microsub.search.title") }}</h3> <h3>{{ __("microsub.search.title") }}</h3>
<div class="search__list"> <div class="ms-search__list">
{% for result in results %} {% 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="ms-search__item{% if not result.valid %} ms-search__item--invalid{% endif %}{% if result.isCommentsFeed %} ms-search__item--comments{% endif %}">
<div class="search__feed"> <div class="ms-search__feed">
<span class="search__name"> <span class="ms-search__name">
{{ result.title or "Feed" }} {{ 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 }} {{ result.typeLabel }}
</span> </span>
{% if result.isCommentsFeed %} {% 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 %} {% endif %}
</span> </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 %} {% if not result.valid %}
<span class="search__error">{{ result.error }}</span> <span class="ms-search__error">{{ result.error }}</span>
{% endif %} {% endif %}
</div> </div>
{% if result.valid %} {% 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 }}"> <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"> <select name="channel" id="channel-{{ loop.index }}" class="select select--small">
{% for channel in channels %} {% for channel in channels %}
<option value="{{ channel.uid }}">{{ channel.name }}</option> <option value="{{ channel.uid }}">{{ channel.name }}</option>
@@ -80,7 +80,7 @@
</div> </div>
</div> </div>
{% elif searched %} {% elif searched %}
<div class="reader__empty"> <div class="ms-reader__empty">
<p>{{ __("microsub.search.noResults") }}</p> <p>{{ __("microsub.search.noResults") }}</p>
</div> </div>
{% endif %} {% endif %}

View File

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

View File

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