chore: phase 2 convention alignment — onerror/onclick removal, CSS stacking avatar fallback (v2.8.1)

- Replace inline onerror handlers with CSS stacking + event delegation for avatar fallback
- Replace inline onclick with event delegation for profile link removal
- Replace hardcoded border values with design tokens in reader-links.css
- Add data-avatar-fallback pattern: fallback initials always visible, img layered on top

Confab-Link: http://localhost:8080/sessions/bb4a6ec4-b711-48cd-b3d7-942ec2a9851d
This commit is contained in:
Ricardo
2026-03-13 12:32:14 +01:00
parent 1c2fb321bc
commit bf386e0c41
9 changed files with 88 additions and 57 deletions
+3 -3
View File
@@ -15,8 +15,8 @@
.ap-link-preview { .ap-link-preview {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: var(--border-radius-small);
border: 1px solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
background-color: var(--color-offset); background-color: var(--color-offset);
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
@@ -121,7 +121,7 @@
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin: var(--space-l) 0 var(--space-s); margin: var(--space-l) 0 var(--space-s);
padding-bottom: var(--space-xs); padding-bottom: var(--space-xs);
border-bottom: 1px solid var(--color-outline); border-bottom: var(--border-width-thin) solid var(--color-outline);
} }
.ap-post-detail__main { .ap-post-detail__main {
+61 -29
View File
@@ -32,7 +32,7 @@
.ap-breadcrumb__current { .ap-breadcrumb__current {
color: var(--color-on-background); color: var(--color-on-background);
font-weight: var(--font-weight-bold); font-weight: 600;
} }
/* ========================================================================== /* ==========================================================================
@@ -151,10 +151,10 @@
background: var(--color-offset); background: var(--color-offset);
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-left: 3px solid var(--color-outline); border-left: 3px solid var(--color-outline);
border-radius: 8px; border-radius: var(--border-radius-small);
overflow: hidden; overflow: hidden;
padding: var(--space-m); padding: var(--space-m);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); box-shadow: 0 1px 2px hsl(var(--tint-neutral) 10% / 0.04);
transition: transition:
box-shadow 0.2s ease, box-shadow 0.2s ease,
border-color 0.2s ease; border-color 0.2s ease;
@@ -163,7 +163,7 @@
.ap-card:hover { .ap-card:hover {
border-color: var(--color-outline-variant); border-color: var(--color-outline-variant);
border-left-color: var(--color-outline-variant); border-left-color: var(--color-outline-variant);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 8px hsl(var(--tint-neutral) 10% / 0.08);
} }
/* ========================================================================== /* ==========================================================================
@@ -261,15 +261,27 @@
margin-bottom: var(--space-s); margin-bottom: var(--space-s);
} }
.ap-card__avatar-wrap {
flex-shrink: 0;
height: 44px;
position: relative;
width: 44px;
}
.ap-card__avatar { .ap-card__avatar {
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-radius: 50%; border-radius: 50%;
flex-shrink: 0;
height: 44px; height: 44px;
object-fit: cover; object-fit: cover;
width: 44px; width: 44px;
} }
.ap-card__avatar-wrap > img {
position: absolute;
inset: 0;
z-index: 1;
}
.ap-card__avatar--default { .ap-card__avatar--default {
align-items: center; align-items: center;
background: var(--color-offset-variant); background: var(--color-offset-variant);
@@ -411,7 +423,7 @@
.ap-card__content code { .ap-card__content code {
background: var(--color-offset-variant); background: var(--color-offset-variant);
border-radius: 3px; border-radius: var(--border-radius-small);
font-size: 0.9em; font-size: 0.9em;
padding: 1px 4px; padding: 1px 4px;
} }
@@ -508,7 +520,7 @@
========================================================================== */ ========================================================================== */
.ap-card__gallery { .ap-card__gallery {
border-radius: 6px; border-radius: var(--border-radius-small);
display: grid; display: grid;
gap: 2px; gap: 2px;
margin-bottom: var(--space-s); margin-bottom: var(--space-s);
@@ -607,7 +619,7 @@
.ap-lightbox { .ap-lightbox {
align-items: center; align-items: center;
background: rgba(0, 0, 0, 0.92); background: hsl(var(--tint-neutral) 10% / 0.92);
display: flex; display: flex;
inset: 0; inset: 0;
justify-content: center; justify-content: center;
@@ -707,7 +719,7 @@
} }
.ap-link-preview__title { .ap-link-preview__title {
font-weight: var(--font-weight-bold); font-weight: 600;
font-size: var(--font-size-s); font-size: var(--font-size-s);
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
@@ -861,7 +873,7 @@
align-items: center; align-items: center;
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 6px; border-radius: var(--border-radius-small);
color: var(--color-on-offset); color: var(--color-on-offset);
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
@@ -1188,6 +1200,11 @@
position: relative; position: relative;
} }
.ap-notification__avatar-wrap {
height: 40px;
width: 40px;
}
.ap-notification__avatar { .ap-notification__avatar {
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-radius: 50%; border-radius: 50%;
@@ -1196,6 +1213,12 @@
width: 40px; width: 40px;
} }
.ap-notification__avatar-wrap > img {
position: absolute;
inset: 0;
z-index: 1;
}
.ap-notification__avatar--default { .ap-notification__avatar--default {
align-items: center; align-items: center;
background: var(--color-offset-variant); background: var(--color-offset-variant);
@@ -1319,7 +1342,16 @@
} }
.ap-profile__avatar-wrap { .ap-profile__avatar-wrap {
height: 80px;
margin-bottom: var(--space-s); margin-bottom: var(--space-s);
position: relative;
width: 80px;
}
.ap-profile__avatar-wrap > img {
position: absolute;
inset: 0;
z-index: 1;
} }
.ap-profile__avatar { .ap-profile__avatar {
@@ -1901,7 +1933,7 @@
.ap-tag-header__title { .ap-tag-header__title {
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold); font-weight: 600;
margin: 0 0 var(--space-xs); margin: 0 0 var(--space-xs);
} }
@@ -2028,7 +2060,7 @@
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
box-sizing: border-box; box-sizing: border-box;
font-size: var(--font-size-base); font-size: var(--font-size-m);
min-width: 0; min-width: 0;
padding: var(--space-xs) var(--space-s); padding: var(--space-xs) var(--space-s);
width: 100%; width: 100%;
@@ -2094,7 +2126,7 @@
background: var(--color-background); background: var(--color-background);
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px hsl(var(--tint-neutral) 10% / 0.15);
left: 0; left: 0;
max-height: 320px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
@@ -2171,7 +2203,7 @@
background: var(--color-background); background: var(--color-background);
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px hsl(var(--tint-neutral) 10% / 0.15);
left: 0; left: 0;
max-height: 320px; max-height: 320px;
overflow-y: auto; overflow-y: auto;
@@ -2281,7 +2313,7 @@
} }
.ap-explore-tabs-nav::after { .ap-explore-tabs-nav::after {
background: linear-gradient(to right, transparent, var(--color-background, #fff) 80%); background: linear-gradient(to right, transparent, var(--color-background) 80%);
content: ""; content: "";
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
@@ -2350,7 +2382,7 @@
/* Scope badges on instance tabs */ /* Scope badges on instance tabs */
.ap-tab__badge { .ap-tab__badge {
border-radius: 3px; border-radius: var(--border-radius-small);
font-size: 0.65em; font-size: 0.65em;
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em; letter-spacing: 0.02em;
@@ -2522,7 +2554,7 @@
.ap-explore-tab-error__retry { .ap-explore-tab-error__retry {
background: none; background: none;
border: 1px solid var(--color-primary-on-background); border: var(--border-width-thin) solid var(--color-primary-on-background);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
color: var(--color-primary-on-background); color: var(--color-primary-on-background);
cursor: pointer; cursor: pointer;
@@ -2602,7 +2634,7 @@
.ap-unread-toggle--active { .ap-unread-toggle--active {
background: color-mix(in srgb, var(--color-primary) 12%, transparent); background: color-mix(in srgb, var(--color-primary) 12%, transparent);
font-weight: var(--font-weight-bold); font-weight: 600;
} }
/* ========================================================================== /* ==========================================================================
@@ -2611,7 +2643,7 @@
.ap-quote-embed { .ap-quote-embed {
border: var(--border-width-thin) solid var(--color-outline); border: var(--border-width-thin) solid var(--color-outline);
border-radius: 8px; border-radius: var(--border-radius-small);
margin-top: var(--space-s); margin-top: var(--space-s);
overflow: hidden; overflow: hidden;
transition: border-color 0.15s ease; transition: border-color 0.15s ease;
@@ -2657,7 +2689,7 @@
color: var(--color-on-offset); color: var(--color-on-offset);
display: inline-flex; display: inline-flex;
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
font-weight: var(--font-weight-bold); font-weight: 600;
justify-content: center; justify-content: center;
} }
@@ -2668,7 +2700,7 @@
.ap-quote-embed__name { .ap-quote-embed__name {
font-size: var(--font-size-s); font-size: var(--font-size-s);
font-weight: var(--font-weight-bold); font-weight: 600;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -2691,7 +2723,7 @@
.ap-quote-embed__title { .ap-quote-embed__title {
font-size: var(--font-size-s); font-size: var(--font-size-s);
font-weight: var(--font-weight-bold); font-weight: 600;
margin: 0 0 var(--space-xs); margin: 0 0 var(--space-xs);
} }
@@ -2757,8 +2789,8 @@
position: absolute; position: absolute;
bottom: 0.5rem; bottom: 0.5rem;
left: 0.5rem; left: 0.5rem;
background: rgba(0, 0, 0, 0.7); background: hsl(var(--tint-neutral) 10% / 0.7);
color: white; color: var(--color-neutral99);
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 700; font-weight: 700;
padding: 0.15rem 0.35rem; padding: 0.15rem 0.35rem;
@@ -2771,7 +2803,7 @@
} }
.ap-media__alt-badge:hover { .ap-media__alt-badge:hover {
background: rgba(0, 0, 0, 0.9); background: hsl(var(--tint-neutral) 10% / 0.9);
} }
.ap-media__alt-text { .ap-media__alt-text {
@@ -2779,8 +2811,8 @@
bottom: 2.2rem; bottom: 2.2rem;
left: 0.5rem; left: 0.5rem;
right: 0.5rem; right: 0.5rem;
background: rgba(0, 0, 0, 0.85); background: hsl(var(--tint-neutral) 10% / 0.85);
color: white; color: var(--color-neutral99);
font-size: var(--font-size-s); font-size: var(--font-size-s);
padding: 0.5rem; padding: 0.5rem;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
@@ -2870,11 +2902,11 @@
/* --- Card shadows: use light tint instead of black --- */ /* --- Card shadows: use light tint instead of black --- */
.ap-card { .ap-card {
box-shadow: 0 1px 2px rgba(255, 255, 255, 0.04); box-shadow: 0 1px 2px hsl(var(--tint-neutral) 90% / 0.04);
} }
.ap-card:hover { .ap-card:hover {
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.06); box-shadow: 0 2px 8px hsl(var(--tint-neutral) 90% / 0.06);
} }
/* --- Tab badge federated: soften purple --- */ /* --- Tab badge federated: soften purple --- */
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "2.8.0", "version": "2.8.1",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
+7 -3
View File
@@ -91,7 +91,7 @@
<label class="label" for="link_value_{{ loop.index }}">{{ __("activitypub.profile.linkValueLabel") }}</label> <label class="label" for="link_value_{{ loop.index }}">{{ __("activitypub.profile.linkValueLabel") }}</label>
<input class="input" type="url" id="link_value_{{ loop.index }}" name="link_value[]" value="{{ att.value }}" placeholder="https://example.com"> <input class="input" type="url" id="link_value_{{ loop.index }}" name="link_value[]" value="{{ att.value }}" placeholder="https://example.com">
</div> </div>
<button type="button" class="button button--small" onclick="this.closest('.profile-link-row').remove()" style="margin-block-end: 4px;">{{ __("activitypub.profile.removeLink") }}</button> <button type="button" class="button button--small profile-link-remove" style="margin-block-end: 4px;">{{ __("activitypub.profile.removeLink") }}</button>
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -129,6 +129,11 @@
<script> <script>
(function() { (function() {
document.getElementById('profile-links').addEventListener('click', function(e) {
var btn = e.target.closest('.profile-link-remove');
if (btn) btn.closest('.profile-link-row').remove();
});
var linkCount = {{ (profile.attachments.length if profile.attachments) or 0 }}; var linkCount = {{ (profile.attachments.length if profile.attachments) or 0 }};
document.getElementById('add-link-btn').addEventListener('click', function() { document.getElementById('add-link-btn').addEventListener('click', function() {
linkCount++; linkCount++;
@@ -167,10 +172,9 @@
var removeBtn = document.createElement('button'); var removeBtn = document.createElement('button');
removeBtn.type = 'button'; removeBtn.type = 'button';
removeBtn.className = 'button button--small'; removeBtn.className = 'button button--small profile-link-remove';
removeBtn.style.cssText = 'margin-block-end: 4px;'; removeBtn.style.cssText = 'margin-block-end: 4px;';
removeBtn.textContent = 'Remove'; removeBtn.textContent = 'Remove';
removeBtn.addEventListener('click', function() { row.remove(); });
row.appendChild(nameDiv); row.appendChild(nameDiv);
row.appendChild(valueDiv); row.appendChild(valueDiv);
+1 -2
View File
@@ -47,8 +47,7 @@
:class="{ 'ap-lookup-autocomplete__item--highlighted': index === highlighted }" :class="{ 'ap-lookup-autocomplete__item--highlighted': index === highlighted }"
@click="selectItem(item)" @click="selectItem(item)"
@mouseenter="highlighted = index"> @mouseenter="highlighted = index">
<img :src="item.avatar" :alt="item.name" class="ap-lookup-autocomplete__avatar" <img :src="item.avatar" :alt="item.name" class="ap-lookup-autocomplete__avatar">
onerror="this.style.display='none'">
<span class="ap-lookup-autocomplete__info"> <span class="ap-lookup-autocomplete__info">
<span class="ap-lookup-autocomplete__name" x-text="item.name"></span> <span class="ap-lookup-autocomplete__name" x-text="item.name"></span>
<span class="ap-lookup-autocomplete__handle" x-text="item.handle"></span> <span class="ap-lookup-autocomplete__handle" x-text="item.handle"></span>
+3 -5
View File
@@ -38,13 +38,11 @@
{# Profile info #} {# Profile info #}
<div class="ap-profile__info"> <div class="ap-profile__info">
<div class="ap-profile__avatar-wrap"> <div class="ap-profile__avatar-wrap" data-avatar-fallback>
{% if icon %} {% if icon %}
<img src="{{ icon }}" alt="{{ name }}" class="ap-profile__avatar" <img src="{{ icon }}" alt="{{ name }}" class="ap-profile__avatar">
onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'ap-profile__avatar ap-profile__avatar--placeholder',textContent:'{{ name[0] }}'}))">
{% else %}
<div class="ap-profile__avatar ap-profile__avatar--placeholder">{{ name[0] }}</div>
{% endif %} {% endif %}
<div class="ap-profile__avatar ap-profile__avatar--placeholder">{{ name[0] }}</div>
</div> </div>
<div class="ap-profile__details"> <div class="ap-profile__details">
+4 -2
View File
@@ -10,8 +10,10 @@
{# Relative timestamps — converts absolute dates to "5m", "3h", "2d" etc. #} {# Relative timestamps — converts absolute dates to "5m", "3h", "2d" etc. #}
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-relative-time.js"></script> <script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-relative-time.js"></script>
{# Alpine.js for client-side reactivity (CW toggles, interaction buttons, infinite scroll) #} {# Avatar fallback — remove broken images to reveal initials fallback underneath #}
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.9/dist/cdn.min.js"></script> <script>document.addEventListener("error",function(e){var t=e.target;if(t.tagName==="IMG"&&t.closest("[data-avatar-fallback]"))t.remove()},true)</script>
{# Alpine.js loaded by default.njk — AP scripts register via alpine:init before it initializes #}
{# Reader stylesheet — loaded in body is fine for modern browsers #} {# Reader stylesheet — loaded in body is fine for modern browsers #}
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css"> <link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css">
+5 -6
View File
@@ -39,13 +39,12 @@
{# Author header #} {# Author header #}
<header class="ap-card__author"> <header class="ap-card__author">
{% if item.author.photo %} <div class="ap-card__avatar-wrap" data-avatar-fallback>
<img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar" loading="lazy" crossorigin="anonymous" {% if item.author.photo %}
onerror="this.style.display='none';this.nextElementSibling.style.display=''"> <img src="{{ item.author.photo }}" alt="{{ item.author.name }}" class="ap-card__avatar" loading="lazy" crossorigin="anonymous">
<span class="ap-card__avatar ap-card__avatar--default" style="display:none" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span> {% endif %}
{% else %}
<span class="ap-card__avatar ap-card__avatar--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span> <span class="ap-card__avatar ap-card__avatar--default" aria-hidden="true">{{ item.author.name[0] | upper if item.author.name else "?" }}</span>
{% endif %} </div>
<div class="ap-card__author-info"> <div class="ap-card__author-info">
<div class="ap-card__author-name"> <div class="ap-card__author-name">
{% if item.author.url %} {% if item.author.url %}
+3 -6
View File
@@ -9,14 +9,11 @@
</form> </form>
{# Actor avatar with type badge #} {# Actor avatar with type badge #}
<div class="ap-notification__avatar-wrap"> <div class="ap-notification__avatar-wrap" data-avatar-fallback>
{% if item.actorPhoto %} {% if item.actorPhoto %}
<img src="{{ item.actorPhoto }}" alt="{{ item.actorName }}" class="ap-notification__avatar" loading="lazy" crossorigin="anonymous" <img src="{{ item.actorPhoto }}" alt="{{ item.actorName }}" class="ap-notification__avatar" loading="lazy" crossorigin="anonymous">
onerror="this.style.display='none';this.nextElementSibling.style.display=''">
<span class="ap-notification__avatar ap-notification__avatar--default" style="display:none" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
{% else %}
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
{% endif %} {% endif %}
<span class="ap-notification__avatar ap-notification__avatar--default" aria-hidden="true">{{ item.actorName[0] | upper if item.actorName else "?" }}</span>
<span class="ap-notification__type-badge"> <span class="ap-notification__type-badge">
{% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}@{% endif %} {% if item.type == "like" %}❤{% elif item.type == "boost" %}🔁{% elif item.type == "follow" %}👤{% elif item.type == "reply" %}💬{% elif item.type == "mention" %}@{% endif %}
</span> </span>