From 9332421890f1c030be93e77ef48d880c57169ad3 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 3 Mar 2026 19:26:38 +0100 Subject: [PATCH] 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 --- assets/reader-blurhash.js | 66 +++++++++++++++++++ assets/reader-links.css | 6 +- assets/reader.css | 102 +++++++++++++++++++++--------- package.json | 2 +- views/layouts/ap-reader.njk | 2 + views/partials/ap-item-media.njk | 10 ++- views/partials/ap-quote-embed.njk | 8 ++- 7 files changed, 159 insertions(+), 37 deletions(-) create mode 100644 assets/reader-blurhash.js diff --git a/assets/reader-blurhash.js b/assets/reader-blurhash.js new file mode 100644 index 0000000..8fbb773 --- /dev/null +++ b/assets/reader-blurhash.js @@ -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 }); +}); diff --git a/assets/reader-links.css b/assets/reader-links.css index 4b1b6aa..5c436d1 100644 --- a/assets/reader-links.css +++ b/assets/reader-links.css @@ -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) */ diff --git a/assets/reader.css b/assets/reader.css index fd708d0..0fcd889 100644 --- a/assets/reader.css +++ b/assets/reader.css @@ -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; } diff --git a/package.json b/package.json index deabf8e..768f2e6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/views/layouts/ap-reader.njk b/views/layouts/ap-reader.njk index 1b57d4c..9086c24 100644 --- a/views/layouts/ap-reader.njk +++ b/views/layouts/ap-reader.njk @@ -19,6 +19,8 @@ {# AP link interception for internal navigation #} + {# Blurhash placeholder backgrounds for gallery images #} + {% if readerParent %}