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 {
|
.ap-link-preview {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: 8px;
|
||||||
border: 1px solid var(--color-neutral-lighter);
|
border: 1px solid var(--color-neutral-lighter);
|
||||||
background-color: var(--color-offset);
|
background-color: var(--color-offset);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: border-color 0.2s ease;
|
transition: border-color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-link-preview:hover {
|
.ap-link-preview:hover {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Text content area (left side) */
|
/* Text content area (left side) */
|
||||||
|
|||||||
+71
-31
@@ -150,10 +150,11 @@
|
|||||||
.ap-card {
|
.ap-card {
|
||||||
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: var(--border-width-thickest) solid var(--color-outline);
|
border-left: 3px solid var(--color-outline);
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: var(--space-m);
|
padding: var(--space-m);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
transition:
|
transition:
|
||||||
box-shadow 0.2s ease,
|
box-shadow 0.2s ease,
|
||||||
border-color 0.2s ease;
|
border-color 0.2s ease;
|
||||||
@@ -162,6 +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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
@@ -263,9 +265,9 @@
|
|||||||
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;
|
flex-shrink: 0;
|
||||||
height: 40px;
|
height: 44px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 40px;
|
width: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__avatar--default {
|
.ap-card__avatar--default {
|
||||||
@@ -282,10 +284,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
gap: 1px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__author-name {
|
.ap-card__author-name {
|
||||||
|
font-size: 0.95em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -327,7 +331,7 @@
|
|||||||
.ap-card__timestamp {
|
.ap-card__timestamp {
|
||||||
color: var(--color-on-offset);
|
color: var(--color-on-offset);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__edited {
|
.ap-card__edited {
|
||||||
@@ -374,7 +378,7 @@
|
|||||||
|
|
||||||
.ap-card__content {
|
.ap-card__content {
|
||||||
color: var(--color-on-background);
|
color: var(--color-on-background);
|
||||||
line-height: var(--line-height-prose);
|
line-height: calc(4 / 3 * 1em);
|
||||||
margin-bottom: var(--space-s);
|
margin-bottom: var(--space-s);
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -504,7 +508,7 @@
|
|||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
.ap-card__gallery {
|
.ap-card__gallery {
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: 6px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-bottom: var(--space-s);
|
margin-bottom: var(--space-s);
|
||||||
@@ -524,9 +528,14 @@
|
|||||||
.ap-card__gallery img {
|
.ap-card__gallery img {
|
||||||
background: var(--color-offset-variant);
|
background: var(--color-offset-variant);
|
||||||
display: block;
|
display: block;
|
||||||
height: 200px;
|
height: 220px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
transition: filter 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ap-card__gallery-link:hover img {
|
||||||
|
filter: brightness(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__gallery-link--more::after {
|
.ap-card__gallery-link--more::after {
|
||||||
@@ -557,7 +566,7 @@
|
|||||||
|
|
||||||
.ap-card__gallery--1 img {
|
.ap-card__gallery--1 img {
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 400px;
|
max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2 photos — side by side */
|
/* 2 photos — side by side */
|
||||||
@@ -761,60 +770,86 @@
|
|||||||
border-top: var(--border-width-thin) solid var(--color-outline);
|
border-top: var(--border-width-thin) solid var(--color-outline);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--space-s);
|
gap: 2px;
|
||||||
padding-top: var(--space-s);
|
padding-top: var(--space-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__action {
|
.ap-card__action {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: var(--border-width-thin) solid var(--color-outline);
|
border: 0;
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: 6px;
|
||||||
color: var(--color-on-offset);
|
color: var(--color-on-offset);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
gap: var(--space-xs);
|
gap: 0.3em;
|
||||||
padding: var(--space-xs) var(--space-s);
|
min-height: 36px;
|
||||||
|
padding: 0.25em 0.6em;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.2s ease;
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__action:hover {
|
.ap-card__action:hover {
|
||||||
background: var(--color-offset-variant);
|
background: var(--color-offset-variant);
|
||||||
border-color: var(--color-outline-variant);
|
|
||||||
color: var(--color-on-background);
|
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 {
|
.ap-card__action--like.ap-card__action--active {
|
||||||
background: var(--color-red90);
|
background: color-mix(in srgb, var(--color-red45) 12%, transparent);
|
||||||
border-color: var(--color-red45);
|
|
||||||
color: var(--color-red45);
|
color: var(--color-red45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__action--boost.ap-card__action--active {
|
.ap-card__action--boost.ap-card__action--active {
|
||||||
background: var(--color-green90);
|
background: color-mix(in srgb, var(--color-green50) 12%, transparent);
|
||||||
border-color: var(--color-green50);
|
|
||||||
color: var(--color-green50);
|
color: var(--color-green50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__action--save.ap-card__action--active {
|
.ap-card__action--save.ap-card__action--active {
|
||||||
background: #4a9eff22;
|
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||||
border-color: #4a9eff;
|
color: var(--color-primary);
|
||||||
color: #4a9eff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-card__action:disabled {
|
.ap-card__action:disabled {
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
opacity: 0.6;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interaction counts */
|
/* Interaction counts */
|
||||||
.ap-card__count {
|
.ap-card__count {
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
color: var(--color-on-offset);
|
color: inherit;
|
||||||
margin-left: 0.25rem;
|
opacity: 0.7;
|
||||||
|
margin-left: 0.1em;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2536,9 +2571,14 @@
|
|||||||
|
|
||||||
.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: var(--border-radius-small);
|
border-radius: 8px;
|
||||||
margin-top: var(--space-s);
|
margin-top: var(--space-s);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ap-quote-embed:hover {
|
||||||
|
border-color: var(--color-outline-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-quote-embed--pending {
|
.ap-quote-embed--pending {
|
||||||
@@ -2553,7 +2593,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ap-quote-embed__link:hover {
|
.ap-quote-embed__link:hover {
|
||||||
background: var(--color-offset);
|
background: color-mix(in srgb, var(--color-offset) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ap-quote-embed__author {
|
.ap-quote-embed__author {
|
||||||
@@ -2618,8 +2658,8 @@
|
|||||||
.ap-quote-embed__content {
|
.ap-quote-embed__content {
|
||||||
color: var(--color-on-background);
|
color: var(--color-on-background);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
line-height: 1.5;
|
line-height: calc(4 / 3 * 1em);
|
||||||
max-height: calc(1.5em * 6);
|
max-height: calc(1.333em * 6);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"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.",
|
"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",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
{# AP link interception for internal navigation #}
|
{# AP link interception for internal navigation #}
|
||||||
<script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-links.js"></script>
|
<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 %}
|
{% if readerParent %}
|
||||||
<nav class="ap-breadcrumb" aria-label="Breadcrumb">
|
<nav class="ap-breadcrumb" aria-label="Breadcrumb">
|
||||||
|
|||||||
@@ -10,10 +10,18 @@
|
|||||||
{# Support both old string format and new object format #}
|
{# Support both old string format and new object format #}
|
||||||
{% set photoSrc = photo.url if photo.url else photo %}
|
{% set photoSrc = photo.url if photo.url else photo %}
|
||||||
{% set photoAlt = photo.alt if photo.alt else "" %}
|
{% 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 %}
|
{% if loop.index0 < 4 %}
|
||||||
<div class="ap-card__gallery-item" x-data="{ showAlt: false }">
|
<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 %}">
|
<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 %}
|
{% if loop.index0 == 3 and extraCount > 0 %}
|
||||||
<span class="ap-card__gallery-more">+{{ extraCount }}</span>
|
<span class="ap-card__gallery-more">+{{ extraCount }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -26,8 +26,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if item.quote.photo and item.quote.photo.length > 0 %}
|
{% if item.quote.photo and item.quote.photo.length > 0 %}
|
||||||
{% set qPhoto = item.quote.photo[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">
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user