feat: visual polish, focus-point cropping, blurhash placeholders (Release 8)
Card styling: softer 8px radius, subtle box-shadow elevation, hover enhancement. Action buttons: borderless with color-coded hover states via color-mix(). Typography: tighter line-height (4/3), larger avatars (44px), gallery images (220px). Focus-point cropping: convert Mastodon focus.x/y to CSS object-position. Blurhash placeholders: decode DC component to background-color on images. Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Blurhash placeholder backgrounds for gallery images.
|
||||
*
|
||||
* Extracts the average (DC) color from a blurhash string and applies it
|
||||
* as a background-color on images with a data-blurhash attribute.
|
||||
* This provides a meaningful colored placeholder while images load.
|
||||
*
|
||||
* The DC component is encoded in the first 4 characters of the blurhash
|
||||
* after the size byte, as a base-83 integer representing an sRGB color.
|
||||
*/
|
||||
|
||||
const BASE83_CHARS =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
|
||||
|
||||
function decode83(str) {
|
||||
let value = 0;
|
||||
for (const c of str) {
|
||||
const digit = BASE83_CHARS.indexOf(c);
|
||||
if (digit === -1) return 0;
|
||||
value = value * 83 + digit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function decodeDC(value) {
|
||||
return {
|
||||
r: (value >> 16) & 255,
|
||||
g: (value >> 8) & 255,
|
||||
b: value & 255,
|
||||
};
|
||||
}
|
||||
|
||||
function blurhashToColor(hash) {
|
||||
if (!hash || hash.length < 6) return null;
|
||||
const dcValue = decode83(hash.slice(1, 5));
|
||||
const { r, g, b } = decodeDC(dcValue);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
for (const img of document.querySelectorAll("img[data-blurhash]")) {
|
||||
const color = blurhashToColor(img.dataset.blurhash);
|
||||
if (color) {
|
||||
img.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dynamically loaded images (infinite scroll)
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType !== 1) continue;
|
||||
const imgs = node.querySelectorAll
|
||||
? node.querySelectorAll("img[data-blurhash]")
|
||||
: [];
|
||||
for (const img of imgs) {
|
||||
const color = blurhashToColor(img.dataset.blurhash);
|
||||
if (color) {
|
||||
img.style.backgroundColor = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
@@ -15,16 +15,16 @@
|
||||
.ap-link-preview {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: var(--border-radius-small);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-neutral-lighter);
|
||||
background-color: var(--color-offset);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color 0.2s ease;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.ap-link-preview:hover {
|
||||
border-color: var(--color-primary);
|
||||
border-color: var(--color-outline-variant);
|
||||
}
|
||||
|
||||
/* Text content area (left side) */
|
||||
|
||||
+71
-31
@@ -150,10 +150,11 @@
|
||||
.ap-card {
|
||||
background: var(--color-offset);
|
||||
border: var(--border-width-thin) solid var(--color-outline);
|
||||
border-left: var(--border-width-thickest) solid var(--color-outline);
|
||||
border-radius: var(--border-radius-small);
|
||||
border-left: 3px solid var(--color-outline);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
padding: var(--space-m);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
@@ -162,6 +163,7 @@
|
||||
.ap-card:hover {
|
||||
border-color: var(--color-outline-variant);
|
||||
border-left-color: var(--color-outline-variant);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -263,9 +265,9 @@
|
||||
border: var(--border-width-thin) solid var(--color-outline);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
height: 40px;
|
||||
height: 44px;
|
||||
object-fit: cover;
|
||||
width: 40px;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.ap-card__avatar--default {
|
||||
@@ -282,10 +284,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ap-card__author-name {
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -327,7 +331,7 @@
|
||||
.ap-card__timestamp {
|
||||
color: var(--color-on-offset);
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.ap-card__edited {
|
||||
@@ -374,7 +378,7 @@
|
||||
|
||||
.ap-card__content {
|
||||
color: var(--color-on-background);
|
||||
line-height: var(--line-height-prose);
|
||||
line-height: calc(4 / 3 * 1em);
|
||||
margin-bottom: var(--space-s);
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
@@ -504,7 +508,7 @@
|
||||
========================================================================== */
|
||||
|
||||
.ap-card__gallery {
|
||||
border-radius: var(--border-radius-small);
|
||||
border-radius: 6px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
margin-bottom: var(--space-s);
|
||||
@@ -524,9 +528,14 @@
|
||||
.ap-card__gallery img {
|
||||
background: var(--color-offset-variant);
|
||||
display: block;
|
||||
height: 200px;
|
||||
height: 220px;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
transition: filter 0.2s ease;
|
||||
}
|
||||
|
||||
.ap-card__gallery-link:hover img {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
|
||||
.ap-card__gallery-link--more::after {
|
||||
@@ -557,7 +566,7 @@
|
||||
|
||||
.ap-card__gallery--1 img {
|
||||
height: auto;
|
||||
max-height: 400px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* 2 photos — side by side */
|
||||
@@ -761,60 +770,86 @@
|
||||
border-top: var(--border-width-thin) solid var(--color-outline);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-s);
|
||||
gap: 2px;
|
||||
padding-top: var(--space-s);
|
||||
}
|
||||
|
||||
.ap-card__action {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: var(--border-width-thin) solid var(--color-outline);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: var(--color-on-offset);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-size: var(--font-size-s);
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) var(--space-s);
|
||||
gap: 0.3em;
|
||||
min-height: 36px;
|
||||
padding: 0.25em 0.6em;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.ap-card__action:hover {
|
||||
background: var(--color-offset-variant);
|
||||
border-color: var(--color-outline-variant);
|
||||
color: var(--color-on-background);
|
||||
}
|
||||
|
||||
/* Active interaction states — using Indiekit's color palette */
|
||||
/* Color-coded hover states per action type */
|
||||
.ap-card__action--reply:hover {
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ap-card__action--boost:hover {
|
||||
background: color-mix(in srgb, var(--color-green50) 12%, transparent);
|
||||
color: var(--color-green50);
|
||||
}
|
||||
|
||||
.ap-card__action--like:hover {
|
||||
background: color-mix(in srgb, var(--color-red45) 12%, transparent);
|
||||
color: var(--color-red45);
|
||||
}
|
||||
|
||||
.ap-card__action--link:hover {
|
||||
background: var(--color-offset-variant);
|
||||
color: var(--color-on-background);
|
||||
}
|
||||
|
||||
.ap-card__action--save:hover {
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Active interaction states */
|
||||
.ap-card__action--like.ap-card__action--active {
|
||||
background: var(--color-red90);
|
||||
border-color: var(--color-red45);
|
||||
background: color-mix(in srgb, var(--color-red45) 12%, transparent);
|
||||
color: var(--color-red45);
|
||||
}
|
||||
|
||||
.ap-card__action--boost.ap-card__action--active {
|
||||
background: var(--color-green90);
|
||||
border-color: var(--color-green50);
|
||||
background: color-mix(in srgb, var(--color-green50) 12%, transparent);
|
||||
color: var(--color-green50);
|
||||
}
|
||||
|
||||
.ap-card__action--save.ap-card__action--active {
|
||||
background: #4a9eff22;
|
||||
border-color: #4a9eff;
|
||||
color: #4a9eff;
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ap-card__action:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.6;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Interaction counts */
|
||||
.ap-card__count {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-on-offset);
|
||||
margin-left: 0.25rem;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
margin-left: 0.1em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -2536,9 +2571,14 @@
|
||||
|
||||
.ap-quote-embed {
|
||||
border: var(--border-width-thin) solid var(--color-outline);
|
||||
border-radius: var(--border-radius-small);
|
||||
border-radius: 8px;
|
||||
margin-top: var(--space-s);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.ap-quote-embed:hover {
|
||||
border-color: var(--color-outline-variant);
|
||||
}
|
||||
|
||||
.ap-quote-embed--pending {
|
||||
@@ -2553,7 +2593,7 @@
|
||||
}
|
||||
|
||||
.ap-quote-embed__link:hover {
|
||||
background: var(--color-offset);
|
||||
background: color-mix(in srgb, var(--color-offset) 50%, transparent);
|
||||
}
|
||||
|
||||
.ap-quote-embed__author {
|
||||
@@ -2618,8 +2658,8 @@
|
||||
.ap-quote-embed__content {
|
||||
color: var(--color-on-background);
|
||||
font-size: var(--font-size-s);
|
||||
line-height: 1.5;
|
||||
max-height: calc(1.5em * 6);
|
||||
line-height: calc(4 / 3 * 1em);
|
||||
max-height: calc(1.333em * 6);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "2.6.0",
|
||||
"version": "2.6.1",
|
||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||
"keywords": [
|
||||
"indiekit",
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
{# AP link interception for internal navigation #}
|
||||
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-links.js"></script>
|
||||
{# Blurhash placeholder backgrounds for gallery images #}
|
||||
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-blurhash.js"></script>
|
||||
|
||||
{% if readerParent %}
|
||||
<nav class="ap-breadcrumb" aria-label="Breadcrumb">
|
||||
|
||||
@@ -10,10 +10,18 @@
|
||||
{# Support both old string format and new object format #}
|
||||
{% set photoSrc = photo.url if photo.url else photo %}
|
||||
{% set photoAlt = photo.alt if photo.alt else "" %}
|
||||
{% set photoBlurhash = photo.blurhash if photo.blurhash else "" %}
|
||||
{# Focus-point cropping: convert -1..1 range to CSS object-position percentages #}
|
||||
{% set focusStyle = "" %}
|
||||
{% if photo.focus and photo.focus.x != null and photo.focus.y != null %}
|
||||
{% set fpX = ((photo.focus.x + 1) / 2 * 100) %}
|
||||
{% set fpY = ((1 - (photo.focus.y + 1) / 2) * 100) %}
|
||||
{% set focusStyle = "object-position:" + fpX + "% " + fpY + "%;" %}
|
||||
{% endif %}
|
||||
{% if loop.index0 < 4 %}
|
||||
<div class="ap-card__gallery-item" x-data="{ showAlt: false }">
|
||||
<button type="button" @click="idx = {{ loop.index0 }}; lightbox = true" class="ap-card__gallery-link{% if loop.index0 == 3 and extraCount > 0 %} ap-card__gallery-link--more{% endif %}">
|
||||
<img src="{{ photoSrc }}" alt="{{ photoAlt }}" loading="lazy">
|
||||
<img src="{{ photoSrc }}" alt="{{ photoAlt }}" loading="lazy"{% if focusStyle %} style="{{ focusStyle }}"{% endif %}{% if photoBlurhash %} data-blurhash="{{ photoBlurhash }}"{% endif %}>
|
||||
{% if loop.index0 == 3 and extraCount > 0 %}
|
||||
<span class="ap-card__gallery-more">+{{ extraCount }}</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -26,8 +26,14 @@
|
||||
{% endif %}
|
||||
{% if item.quote.photo and item.quote.photo.length > 0 %}
|
||||
{% set qPhoto = item.quote.photo[0] %}
|
||||
{% set qFocusStyle = "" %}
|
||||
{% if qPhoto.focus and qPhoto.focus.x != null and qPhoto.focus.y != null %}
|
||||
{% set qFpX = ((qPhoto.focus.x + 1) / 2 * 100) %}
|
||||
{% set qFpY = ((1 - (qPhoto.focus.y + 1) / 2) * 100) %}
|
||||
{% set qFocusStyle = "object-position:" + qFpX + "% " + qFpY + "%;" %}
|
||||
{% endif %}
|
||||
<div class="ap-quote-embed__media">
|
||||
<img src="{{ qPhoto.url if qPhoto.url else qPhoto }}" alt="{{ qPhoto.alt if qPhoto.alt else '' }}" loading="lazy" class="ap-quote-embed__photo">
|
||||
<img src="{{ qPhoto.url if qPhoto.url else qPhoto }}" alt="{{ qPhoto.alt if qPhoto.alt else '' }}" loading="lazy" class="ap-quote-embed__photo"{% if qFocusStyle %} style="{{ qFocusStyle }}"{% endif %}>
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user