@rmdes/indiekit-endpoint-activitypub 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Blurhash placeholder backgrounds for gallery images.
3
+ *
4
+ * Extracts the average (DC) color from a blurhash string and applies it
5
+ * as a background-color on images with a data-blurhash attribute.
6
+ * This provides a meaningful colored placeholder while images load.
7
+ *
8
+ * The DC component is encoded in the first 4 characters of the blurhash
9
+ * after the size byte, as a base-83 integer representing an sRGB color.
10
+ */
11
+
12
+ const BASE83_CHARS =
13
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~";
14
+
15
+ function decode83(str) {
16
+ let value = 0;
17
+ for (const c of str) {
18
+ const digit = BASE83_CHARS.indexOf(c);
19
+ if (digit === -1) return 0;
20
+ value = value * 83 + digit;
21
+ }
22
+ return value;
23
+ }
24
+
25
+ function decodeDC(value) {
26
+ return {
27
+ r: (value >> 16) & 255,
28
+ g: (value >> 8) & 255,
29
+ b: value & 255,
30
+ };
31
+ }
32
+
33
+ function blurhashToColor(hash) {
34
+ if (!hash || hash.length < 6) return null;
35
+ const dcValue = decode83(hash.slice(1, 5));
36
+ const { r, g, b } = decodeDC(dcValue);
37
+ return `rgb(${r},${g},${b})`;
38
+ }
39
+
40
+ document.addEventListener("DOMContentLoaded", () => {
41
+ for (const img of document.querySelectorAll("img[data-blurhash]")) {
42
+ const color = blurhashToColor(img.dataset.blurhash);
43
+ if (color) {
44
+ img.style.backgroundColor = color;
45
+ }
46
+ }
47
+
48
+ // Handle dynamically loaded images (infinite scroll)
49
+ const observer = new MutationObserver((mutations) => {
50
+ for (const mutation of mutations) {
51
+ for (const node of mutation.addedNodes) {
52
+ if (node.nodeType !== 1) continue;
53
+ const imgs = node.querySelectorAll
54
+ ? node.querySelectorAll("img[data-blurhash]")
55
+ : [];
56
+ for (const img of imgs) {
57
+ const color = blurhashToColor(img.dataset.blurhash);
58
+ if (color) {
59
+ img.style.backgroundColor = color;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ });
65
+ observer.observe(document.body, { childList: true, subtree: true });
66
+ });
@@ -15,16 +15,16 @@
15
15
  .ap-link-preview {
16
16
  display: flex;
17
17
  overflow: hidden;
18
- border-radius: var(--border-radius-small);
18
+ border-radius: 8px;
19
19
  border: 1px solid var(--color-neutral-lighter);
20
20
  background-color: var(--color-offset);
21
21
  text-decoration: none;
22
22
  color: inherit;
23
- transition: border-color 0.2s ease;
23
+ transition: border-color 0.15s ease;
24
24
  }
25
25
 
26
26
  .ap-link-preview:hover {
27
- border-color: var(--color-primary);
27
+ border-color: var(--color-outline-variant);
28
28
  }
29
29
 
30
30
  /* Text content area (left side) */
package/assets/reader.css CHANGED
@@ -150,10 +150,11 @@
150
150
  .ap-card {
151
151
  background: var(--color-offset);
152
152
  border: var(--border-width-thin) solid var(--color-outline);
153
- border-left: var(--border-width-thickest) solid var(--color-outline);
154
- border-radius: var(--border-radius-small);
153
+ border-left: 3px solid var(--color-outline);
154
+ border-radius: 8px;
155
155
  overflow: hidden;
156
156
  padding: var(--space-m);
157
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
157
158
  transition:
158
159
  box-shadow 0.2s ease,
159
160
  border-color 0.2s ease;
@@ -162,6 +163,7 @@
162
163
  .ap-card:hover {
163
164
  border-color: var(--color-outline-variant);
164
165
  border-left-color: var(--color-outline-variant);
166
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
165
167
  }
166
168
 
167
169
  /* ==========================================================================
@@ -263,9 +265,9 @@
263
265
  border: var(--border-width-thin) solid var(--color-outline);
264
266
  border-radius: 50%;
265
267
  flex-shrink: 0;
266
- height: 40px;
268
+ height: 44px;
267
269
  object-fit: cover;
268
- width: 40px;
270
+ width: 44px;
269
271
  }
270
272
 
271
273
  .ap-card__avatar--default {
@@ -282,10 +284,12 @@
282
284
  display: flex;
283
285
  flex-direction: column;
284
286
  flex: 1;
287
+ gap: 1px;
285
288
  min-width: 0;
286
289
  }
287
290
 
288
291
  .ap-card__author-name {
292
+ font-size: 0.95em;
289
293
  font-weight: 600;
290
294
  overflow: hidden;
291
295
  text-overflow: ellipsis;
@@ -327,7 +331,7 @@
327
331
  .ap-card__timestamp {
328
332
  color: var(--color-on-offset);
329
333
  flex-shrink: 0;
330
- font-size: var(--font-size-xs);
334
+ font-size: var(--font-size-s);
331
335
  }
332
336
 
333
337
  .ap-card__edited {
@@ -374,7 +378,7 @@
374
378
 
375
379
  .ap-card__content {
376
380
  color: var(--color-on-background);
377
- line-height: var(--line-height-prose);
381
+ line-height: calc(4 / 3 * 1em);
378
382
  margin-bottom: var(--space-s);
379
383
  overflow-wrap: break-word;
380
384
  word-break: break-word;
@@ -504,7 +508,7 @@
504
508
  ========================================================================== */
505
509
 
506
510
  .ap-card__gallery {
507
- border-radius: var(--border-radius-small);
511
+ border-radius: 6px;
508
512
  display: grid;
509
513
  gap: 2px;
510
514
  margin-bottom: var(--space-s);
@@ -524,9 +528,14 @@
524
528
  .ap-card__gallery img {
525
529
  background: var(--color-offset-variant);
526
530
  display: block;
527
- height: 200px;
531
+ height: 220px;
528
532
  object-fit: cover;
529
533
  width: 100%;
534
+ transition: filter 0.2s ease;
535
+ }
536
+
537
+ .ap-card__gallery-link:hover img {
538
+ filter: brightness(0.92);
530
539
  }
531
540
 
532
541
  .ap-card__gallery-link--more::after {
@@ -557,7 +566,7 @@
557
566
 
558
567
  .ap-card__gallery--1 img {
559
568
  height: auto;
560
- max-height: 400px;
569
+ max-height: 500px;
561
570
  }
562
571
 
563
572
  /* 2 photos — side by side */
@@ -761,60 +770,86 @@
761
770
  border-top: var(--border-width-thin) solid var(--color-outline);
762
771
  display: flex;
763
772
  flex-wrap: wrap;
764
- gap: var(--space-s);
773
+ gap: 2px;
765
774
  padding-top: var(--space-s);
766
775
  }
767
776
 
768
777
  .ap-card__action {
769
778
  align-items: center;
770
779
  background: transparent;
771
- border: var(--border-width-thin) solid var(--color-outline);
772
- border-radius: var(--border-radius-small);
780
+ border: 0;
781
+ border-radius: 6px;
773
782
  color: var(--color-on-offset);
774
783
  cursor: pointer;
775
784
  display: inline-flex;
776
785
  font-size: var(--font-size-s);
777
- gap: var(--space-xs);
778
- padding: var(--space-xs) var(--space-s);
786
+ gap: 0.3em;
787
+ min-height: 36px;
788
+ padding: 0.25em 0.6em;
779
789
  text-decoration: none;
780
- transition: all 0.2s ease;
790
+ transition:
791
+ background-color 0.15s ease,
792
+ color 0.15s ease;
781
793
  }
782
794
 
783
795
  .ap-card__action:hover {
784
796
  background: var(--color-offset-variant);
785
- border-color: var(--color-outline-variant);
786
797
  color: var(--color-on-background);
787
798
  }
788
799
 
789
- /* Active interaction states using Indiekit's color palette */
800
+ /* Color-coded hover states per action type */
801
+ .ap-card__action--reply:hover {
802
+ background: color-mix(in srgb, var(--color-primary) 12%, transparent);
803
+ color: var(--color-primary);
804
+ }
805
+
806
+ .ap-card__action--boost:hover {
807
+ background: color-mix(in srgb, var(--color-green50) 12%, transparent);
808
+ color: var(--color-green50);
809
+ }
810
+
811
+ .ap-card__action--like:hover {
812
+ background: color-mix(in srgb, var(--color-red45) 12%, transparent);
813
+ color: var(--color-red45);
814
+ }
815
+
816
+ .ap-card__action--link:hover {
817
+ background: var(--color-offset-variant);
818
+ color: var(--color-on-background);
819
+ }
820
+
821
+ .ap-card__action--save:hover {
822
+ background: color-mix(in srgb, var(--color-primary) 12%, transparent);
823
+ color: var(--color-primary);
824
+ }
825
+
826
+ /* Active interaction states */
790
827
  .ap-card__action--like.ap-card__action--active {
791
- background: var(--color-red90);
792
- border-color: var(--color-red45);
828
+ background: color-mix(in srgb, var(--color-red45) 12%, transparent);
793
829
  color: var(--color-red45);
794
830
  }
795
831
 
796
832
  .ap-card__action--boost.ap-card__action--active {
797
- background: var(--color-green90);
798
- border-color: var(--color-green50);
833
+ background: color-mix(in srgb, var(--color-green50) 12%, transparent);
799
834
  color: var(--color-green50);
800
835
  }
801
836
 
802
837
  .ap-card__action--save.ap-card__action--active {
803
- background: #4a9eff22;
804
- border-color: #4a9eff;
805
- color: #4a9eff;
838
+ background: color-mix(in srgb, var(--color-primary) 12%, transparent);
839
+ color: var(--color-primary);
806
840
  }
807
841
 
808
842
  .ap-card__action:disabled {
809
843
  cursor: wait;
810
- opacity: 0.6;
844
+ opacity: 0.5;
811
845
  }
812
846
 
813
847
  /* Interaction counts */
814
848
  .ap-card__count {
815
849
  font-size: var(--font-size-xs);
816
- color: var(--color-on-offset);
817
- margin-left: 0.25rem;
850
+ color: inherit;
851
+ opacity: 0.7;
852
+ margin-left: 0.1em;
818
853
  font-variant-numeric: tabular-nums;
819
854
  }
820
855
 
@@ -2536,9 +2571,14 @@
2536
2571
 
2537
2572
  .ap-quote-embed {
2538
2573
  border: var(--border-width-thin) solid var(--color-outline);
2539
- border-radius: var(--border-radius-small);
2574
+ border-radius: 8px;
2540
2575
  margin-top: var(--space-s);
2541
2576
  overflow: hidden;
2577
+ transition: border-color 0.15s ease;
2578
+ }
2579
+
2580
+ .ap-quote-embed:hover {
2581
+ border-color: var(--color-outline-variant);
2542
2582
  }
2543
2583
 
2544
2584
  .ap-quote-embed--pending {
@@ -2553,7 +2593,7 @@
2553
2593
  }
2554
2594
 
2555
2595
  .ap-quote-embed__link:hover {
2556
- background: var(--color-offset);
2596
+ background: color-mix(in srgb, var(--color-offset) 50%, transparent);
2557
2597
  }
2558
2598
 
2559
2599
  .ap-quote-embed__author {
@@ -2618,8 +2658,8 @@
2618
2658
  .ap-quote-embed__content {
2619
2659
  color: var(--color-on-background);
2620
2660
  font-size: var(--font-size-s);
2621
- line-height: 1.5;
2622
- max-height: calc(1.5em * 6);
2661
+ line-height: calc(4 / 3 * 1em);
2662
+ max-height: calc(1.333em * 6);
2623
2663
  overflow: hidden;
2624
2664
  }
2625
2665
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -19,6 +19,8 @@
19
19
 
20
20
  {# AP link interception for internal navigation #}
21
21
  <script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-links.js"></script>
22
+ {# Blurhash placeholder backgrounds for gallery images #}
23
+ <script defer src="/assets/@rmdes-indiekit-endpoint-activitypub/reader-blurhash.js"></script>
22
24
 
23
25
  {% if readerParent %}
24
26
  <nav class="ap-breadcrumb" aria-label="Breadcrumb">
@@ -10,10 +10,18 @@
10
10
  {# Support both old string format and new object format #}
11
11
  {% set photoSrc = photo.url if photo.url else photo %}
12
12
  {% set photoAlt = photo.alt if photo.alt else "" %}
13
+ {% set photoBlurhash = photo.blurhash if photo.blurhash else "" %}
14
+ {# Focus-point cropping: convert -1..1 range to CSS object-position percentages #}
15
+ {% set focusStyle = "" %}
16
+ {% if photo.focus and photo.focus.x != null and photo.focus.y != null %}
17
+ {% set fpX = ((photo.focus.x + 1) / 2 * 100) %}
18
+ {% set fpY = ((1 - (photo.focus.y + 1) / 2) * 100) %}
19
+ {% set focusStyle = "object-position:" + fpX + "% " + fpY + "%;" %}
20
+ {% endif %}
13
21
  {% if loop.index0 < 4 %}
14
22
  <div class="ap-card__gallery-item" x-data="{ showAlt: false }">
15
23
  <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 %}">
16
- <img src="{{ photoSrc }}" alt="{{ photoAlt }}" loading="lazy">
24
+ <img src="{{ photoSrc }}" alt="{{ photoAlt }}" loading="lazy"{% if focusStyle %} style="{{ focusStyle }}"{% endif %}{% if photoBlurhash %} data-blurhash="{{ photoBlurhash }}"{% endif %}>
17
25
  {% if loop.index0 == 3 and extraCount > 0 %}
18
26
  <span class="ap-card__gallery-more">+{{ extraCount }}</span>
19
27
  {% endif %}
@@ -26,8 +26,14 @@
26
26
  {% endif %}
27
27
  {% if item.quote.photo and item.quote.photo.length > 0 %}
28
28
  {% set qPhoto = item.quote.photo[0] %}
29
+ {% set qFocusStyle = "" %}
30
+ {% if qPhoto.focus and qPhoto.focus.x != null and qPhoto.focus.y != null %}
31
+ {% set qFpX = ((qPhoto.focus.x + 1) / 2 * 100) %}
32
+ {% set qFpY = ((1 - (qPhoto.focus.y + 1) / 2) * 100) %}
33
+ {% set qFocusStyle = "object-position:" + qFpX + "% " + qFpY + "%;" %}
34
+ {% endif %}
29
35
  <div class="ap-quote-embed__media">
30
- <img src="{{ qPhoto.url if qPhoto.url else qPhoto }}" alt="{{ qPhoto.alt if qPhoto.alt else '' }}" loading="lazy" class="ap-quote-embed__photo">
36
+ <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 %}>
31
37
  </div>
32
38
  {% endif %}
33
39
  </a>