@hagicode/hagilight-core 0.8.1-dev.85.1.ebda464 → 0.8.1-dev.87.1.57ea49d
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.
- package/PromotoBanner.astro +4 -0
- package/README.md +2 -0
- package/dist/promotions.d.ts +5 -0
- package/dist/promotions.js +22 -0
- package/dist/promoto-banner.d.ts +2 -1
- package/dist/promoto-banner.js +41 -3
- package/package.json +1 -1
package/PromotoBanner.astro
CHANGED
|
@@ -196,6 +196,10 @@ const { fallback, locale = Astro.currentLocale ?? 'en' } = Astro.props;
|
|
|
196
196
|
object-fit: cover;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
.hagilight-promoto__media picture {
|
|
200
|
+
display: contents;
|
|
201
|
+
}
|
|
202
|
+
|
|
199
203
|
.hagilight-promoto__cta {
|
|
200
204
|
grid-area: cta;
|
|
201
205
|
display: inline-flex;
|
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ const links = resolveSiteLinks('en-US');
|
|
|
39
39
|
|
|
40
40
|
The shared banner (`PromotoBanner`, `loadActivePromotions`) displays only promotions whose `platforms` array in Index's `/promote.json` includes `web`, in addition to being enabled, inside their activation window, and matched with `/promote_content.json`. This follows the `platforms` flag documented in the Index README. An entry whose `platforms` is missing, empty, or not an array of strings is not displayed, other values such as `hagicode` are ignored, and matching is exact (`Web` does not match). When no promotion remains, the banner shows its site fallback or stays hidden, as before.
|
|
41
41
|
|
|
42
|
+
A promotion's `image` may also carry `sources`, the alternative image files that Index publishes in the order AVIF, WebP, then the original. The banner renders them as a `<picture>` in that order, with the original as its `<img>`, and skips any entry whose `type` is not an `image/*` media type or whose URL is unsafe. Each `src` is resolved against `https://index.hagicode.com` when the promotion documents load. If the browser fails to load a tier, the banner removes it and the next tier is used. A card without `sources` renders the plain `<img>` as before, and `sources` never affect the dismissal signature, so a dismissed banner stays dismissed when only its image chain changes.
|
|
43
|
+
|
|
42
44
|
## Google Analytics click events
|
|
43
45
|
|
|
44
46
|
`GoogleAnalytics` also reports clicks on the key links and download buttons that the shared components render. There is nothing to configure: events follow the same rules as page views, so they are sent only on production pages where Google Analytics is enabled, never on the Starlight 404 page, and silently skipped when `gtag` is missing (development builds, blocked scripts).
|
package/dist/promotions.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
type FetchLike = typeof fetch;
|
|
2
|
+
export interface PromotionImageSource {
|
|
3
|
+
type: string;
|
|
4
|
+
src: string;
|
|
5
|
+
}
|
|
2
6
|
export interface PromotionImage {
|
|
3
7
|
src: string;
|
|
4
8
|
alt: string;
|
|
5
9
|
variant?: string;
|
|
6
10
|
width?: number;
|
|
7
11
|
height?: number;
|
|
12
|
+
sources?: PromotionImageSource[];
|
|
8
13
|
}
|
|
9
14
|
export interface PromotionCard {
|
|
10
15
|
id: string;
|
package/dist/promotions.js
CHANGED
|
@@ -15,6 +15,26 @@ function parseLocalizedMap(value) {
|
|
|
15
15
|
const entries = Object.entries(value).filter((entry) => nonEmptyString(entry[0]) && nonEmptyString(entry[1]));
|
|
16
16
|
return entries.length ? Object.fromEntries(entries.map(([key, text]) => [key.trim(), text.trim()])) : null;
|
|
17
17
|
}
|
|
18
|
+
function parseImageSources(value) {
|
|
19
|
+
if (!Array.isArray(value))
|
|
20
|
+
return [];
|
|
21
|
+
return value.flatMap((item) => {
|
|
22
|
+
if (!isRecord(item) || !nonEmptyString(item.type) || !nonEmptyString(item.src))
|
|
23
|
+
return [];
|
|
24
|
+
const type = item.type.trim().toLowerCase();
|
|
25
|
+
if (!type.startsWith('image/'))
|
|
26
|
+
return [];
|
|
27
|
+
try {
|
|
28
|
+
const url = new URL(item.src.trim(), INDEX_ORIGIN);
|
|
29
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
30
|
+
return [];
|
|
31
|
+
return [{ type, src: url.toString() }];
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
18
38
|
function parseImage(value, record) {
|
|
19
39
|
const image = isRecord(value) ? value : {};
|
|
20
40
|
const src = nonEmptyString(value)
|
|
@@ -46,6 +66,7 @@ function parseImage(value, record) {
|
|
|
46
66
|
catch {
|
|
47
67
|
return undefined;
|
|
48
68
|
}
|
|
69
|
+
const sources = parseImageSources(image.sources);
|
|
49
70
|
return {
|
|
50
71
|
src: normalizedSrc,
|
|
51
72
|
alt: nonEmptyString(image.alt)
|
|
@@ -56,6 +77,7 @@ function parseImage(value, record) {
|
|
|
56
77
|
variant: nonEmptyString(image.variant) ? image.variant.trim() : undefined,
|
|
57
78
|
width,
|
|
58
79
|
height,
|
|
80
|
+
...(sources.length ? { sources } : {}),
|
|
59
81
|
};
|
|
60
82
|
}
|
|
61
83
|
function isSafeLink(value) {
|
package/dist/promoto-banner.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type PromotionCard } from './promotions.js';
|
|
1
|
+
import { type PromotionCard, type PromotionImage, type PromotionImageSource } from './promotions.js';
|
|
2
2
|
export type PromotionVisibility = 'dismissed' | 'footer-hidden' | 'hidden' | 'ready';
|
|
3
3
|
export declare function selectPromotionCards(remote: PromotionCard[], fallback?: PromotionCard | null): PromotionCard[];
|
|
4
|
+
export declare function getPromotionImageSources(image: PromotionImage | undefined): PromotionImageSource[];
|
|
4
5
|
export declare function getPromotionSetSignature(cards: PromotionCard[]): string | null;
|
|
5
6
|
export declare function getPromotionVisibility(cards: PromotionCard[], signature: string | null, dismissedSignature: string | null, footerVisible: boolean): PromotionVisibility;
|
|
6
7
|
export declare function getNextPromotionIndex(index: number, count: number, direction: -1 | 1): number;
|
package/dist/promoto-banner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { gaEventAttributes } from './analytics-events.js';
|
|
2
|
-
import { loadActivePromotions } from './promotions.js';
|
|
2
|
+
import { loadActivePromotions, } from './promotions.js';
|
|
3
3
|
const ELEMENT_NAME = 'hagilight-promoto-banner';
|
|
4
4
|
const DISMISSED_SIGNATURE_KEY = 'hagilight:promoto-banner:dismissed-signature';
|
|
5
5
|
const ROTATION_INTERVAL_MS = 8000;
|
|
@@ -37,6 +37,28 @@ export function selectPromotionCards(remote, fallback) {
|
|
|
37
37
|
return eligibleRemote;
|
|
38
38
|
return fallback && isUsableCard(fallback) ? [fallback] : [];
|
|
39
39
|
}
|
|
40
|
+
export function getPromotionImageSources(image) {
|
|
41
|
+
if (!image || !Array.isArray(image.sources))
|
|
42
|
+
return [];
|
|
43
|
+
return image.sources.filter((source) => source.type.toLowerCase().startsWith('image/')
|
|
44
|
+
&& isSafeUrl(source.src)
|
|
45
|
+
&& source.src !== image.src);
|
|
46
|
+
}
|
|
47
|
+
function sourceUrl(source) {
|
|
48
|
+
return new URL(source.srcset, document.baseURI).href;
|
|
49
|
+
}
|
|
50
|
+
function advancePromotionImageSource(image) {
|
|
51
|
+
const picture = image.parentElement;
|
|
52
|
+
if (!(picture instanceof HTMLPictureElement))
|
|
53
|
+
return;
|
|
54
|
+
const failed = Array.from(picture.querySelectorAll('source'))
|
|
55
|
+
.find((source) => sourceUrl(source) === image.currentSrc);
|
|
56
|
+
if (!failed)
|
|
57
|
+
return;
|
|
58
|
+
failed.remove();
|
|
59
|
+
// Re-assigning src makes the browser re-run source selection over the remaining tiers.
|
|
60
|
+
image.src = image.src;
|
|
61
|
+
}
|
|
40
62
|
export function getPromotionSetSignature(cards) {
|
|
41
63
|
if (!cards.length)
|
|
42
64
|
return null;
|
|
@@ -241,7 +263,6 @@ export function definePromotoBannerElement() {
|
|
|
241
263
|
media.className = 'hagilight-promoto__media';
|
|
242
264
|
const image = document.createElement('img');
|
|
243
265
|
image.className = 'hagilight-promoto__image';
|
|
244
|
-
image.src = card.image.src;
|
|
245
266
|
image.alt = card.image.alt || card.title;
|
|
246
267
|
image.loading = index === this.currentIndex ? 'eager' : 'lazy';
|
|
247
268
|
image.decoding = 'async';
|
|
@@ -249,7 +270,24 @@ export function definePromotoBannerElement() {
|
|
|
249
270
|
image.width = card.image.width;
|
|
250
271
|
if (card.image.height)
|
|
251
272
|
image.height = card.image.height;
|
|
252
|
-
|
|
273
|
+
const sources = getPromotionImageSources(card.image);
|
|
274
|
+
if (sources.length) {
|
|
275
|
+
const picture = document.createElement('picture');
|
|
276
|
+
picture.className = 'hagilight-promoto__picture';
|
|
277
|
+
for (const source of sources) {
|
|
278
|
+
const element = document.createElement('source');
|
|
279
|
+
element.type = source.type;
|
|
280
|
+
element.srcset = source.src;
|
|
281
|
+
picture.append(element);
|
|
282
|
+
}
|
|
283
|
+
picture.append(image);
|
|
284
|
+
image.addEventListener('error', () => advancePromotionImageSource(image));
|
|
285
|
+
media.append(picture);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
media.append(image);
|
|
289
|
+
}
|
|
290
|
+
image.src = card.image.src;
|
|
253
291
|
slide.append(media);
|
|
254
292
|
}
|
|
255
293
|
slide.append(link);
|
package/package.json
CHANGED