ferst-core 0.4.2 → 0.4.3
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.
|
@@ -76,48 +76,31 @@ const safe = isSafeEmbedUrl(finalEmbedUrl);
|
|
|
76
76
|
</section>
|
|
77
77
|
|
|
78
78
|
<script>
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
import { hasConsent, setConsent, onConsentChange } from '../lib/consent-client';
|
|
84
|
-
|
|
85
|
-
const embeds = document.querySelectorAll<HTMLElement>('.b-calendar__embed');
|
|
79
|
+
// Shared consent-gated embed loader (one implementation for calendar/map/
|
|
80
|
+
// locations). Accepting the cookie banner or the footer panel auto-loads every
|
|
81
|
+
// embed; the per-embed button is a fallback granting the same shared consent.
|
|
82
|
+
import { initEmbedConsent, makeEmbedFrame } from '../lib/embed-consent';
|
|
86
83
|
|
|
87
84
|
// Google Calendar embeds render light-only — `bgcolor` recolours just the
|
|
88
85
|
// surround, never the grid, so matching it to a dark surface still left a white
|
|
89
|
-
// calendar
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function load(el: HTMLElement) {
|
|
99
|
-
if (el.dataset.loaded === 'true') return;
|
|
100
|
-
const url = el.dataset.embedUrl;
|
|
101
|
-
const frameSlot = el.querySelector<HTMLElement>('.b-calendar__frame');
|
|
102
|
-
if (!url || !frameSlot) return;
|
|
103
|
-
const frame = document.createElement('iframe');
|
|
104
|
-
frame.src = calendarUrl(url);
|
|
105
|
-
frame.title = el.dataset.title || 'Calendar';
|
|
106
|
-
frame.loading = 'lazy';
|
|
107
|
-
frame.style.width = '100%';
|
|
108
|
-
frame.style.height = (el.dataset.height || '600') + 'px';
|
|
109
|
-
frame.style.border = '0';
|
|
110
|
-
frameSlot.replaceChildren(frame);
|
|
111
|
-
el.dataset.loaded = 'true';
|
|
112
|
-
const consent = el.querySelector<HTMLElement>('.b-calendar__consent');
|
|
113
|
-
if (consent) consent.hidden = true;
|
|
114
|
-
}
|
|
115
|
-
const loadAll = () => embeds.forEach(load);
|
|
86
|
+
// calendar in the dark page. We force a deterministic WHITE surround here
|
|
87
|
+
// (independent of the theme at load) and, in dark mode, invert the whole frame
|
|
88
|
+
// in CSS (below) — keyed on [data-theme] so it also flips live on a toggle.
|
|
89
|
+
const withWhiteSurround = (url: string) =>
|
|
90
|
+
/google\.com\/calendar|calendar\.google\.com/.test(url)
|
|
91
|
+
? url + (url.includes('?') ? '&' : '?') + 'bgcolor=%23ffffff'
|
|
92
|
+
: url;
|
|
116
93
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
94
|
+
initEmbedConsent({
|
|
95
|
+
root: '.b-calendar__embed',
|
|
96
|
+
frame: '.b-calendar__frame',
|
|
97
|
+
consent: '.b-calendar__consent',
|
|
98
|
+
button: '.b-calendar__load',
|
|
99
|
+
buildFrame: (el, url) =>
|
|
100
|
+
makeEmbedFrame(withWhiteSurround(url), {
|
|
101
|
+
title: el.dataset.title || 'Calendar',
|
|
102
|
+
height: (el.dataset.height || '600') + 'px',
|
|
103
|
+
}),
|
|
121
104
|
});
|
|
122
105
|
</script>
|
|
123
106
|
|
|
@@ -65,34 +65,18 @@ const { title, intro, columns = 2, provider = 'Google Maps', items = [] } = Astr
|
|
|
65
65
|
</section>
|
|
66
66
|
|
|
67
67
|
<script>
|
|
68
|
-
// Shared
|
|
69
|
-
|
|
68
|
+
// Shared consent-gated embed loader (see lib/embed-consent). Accepting the cookie
|
|
69
|
+
// banner or the footer panel auto-loads every map tile here; the per-tile button
|
|
70
|
+
// is a fallback that grants the same shared consent.
|
|
71
|
+
import { initEmbedConsent, makeEmbedFrame } from '../../lib/embed-consent';
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
frame.src = url;
|
|
79
|
-
frame.title = el.dataset.title || 'Map';
|
|
80
|
-
frame.loading = 'lazy';
|
|
81
|
-
frame.referrerPolicy = 'no-referrer-when-downgrade';
|
|
82
|
-
frame.style.width = '100%';
|
|
83
|
-
frame.style.height = '100%';
|
|
84
|
-
frame.style.border = '0';
|
|
85
|
-
slot.replaceChildren(frame);
|
|
86
|
-
el.dataset.loaded = 'true';
|
|
87
|
-
const consent = el.querySelector<HTMLElement>('.b-loc-map__consent');
|
|
88
|
-
if (consent) consent.hidden = true;
|
|
89
|
-
}
|
|
90
|
-
const loadAll = () => maps.forEach(load);
|
|
91
|
-
|
|
92
|
-
if (hasConsent()) loadAll();
|
|
93
|
-
onConsentChange((allowed) => { if (allowed) loadAll(); });
|
|
94
|
-
maps.forEach((el) => {
|
|
95
|
-
el.querySelector<HTMLButtonElement>('.b-loc-map__load')?.addEventListener('click', () => setConsent(true));
|
|
73
|
+
initEmbedConsent({
|
|
74
|
+
root: '.b-loc-map',
|
|
75
|
+
frame: '.b-loc-map__frame',
|
|
76
|
+
consent: '.b-loc-map__consent',
|
|
77
|
+
button: '.b-loc-map__load',
|
|
78
|
+
buildFrame: (el, url) =>
|
|
79
|
+
makeEmbedFrame(url, { title: el.dataset.title || 'Map', height: '100%', referrer: true }),
|
|
96
80
|
});
|
|
97
81
|
</script>
|
|
98
82
|
|
|
@@ -71,37 +71,22 @@ const safe = isSafeEmbedUrl(embedUrl);
|
|
|
71
71
|
</section>
|
|
72
72
|
|
|
73
73
|
<script>
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
import {
|
|
74
|
+
// Shared consent-gated embed loader (see lib/embed-consent). Accepting the cookie
|
|
75
|
+
// banner or the footer panel auto-loads every embed; the per-embed button is a
|
|
76
|
+
// fallback that grants the same shared consent.
|
|
77
|
+
import { initEmbedConsent, makeEmbedFrame } from '../../lib/embed-consent';
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
frame.referrerPolicy = 'no-referrer-when-downgrade';
|
|
91
|
-
frame.style.width = '100%';
|
|
92
|
-
frame.style.height = (el.dataset.height || '450') + 'px';
|
|
93
|
-
frame.style.border = '0';
|
|
94
|
-
frameSlot.replaceChildren(frame);
|
|
95
|
-
el.dataset.loaded = 'true';
|
|
96
|
-
const consent = el.querySelector<HTMLElement>('.b-map__consent');
|
|
97
|
-
if (consent) consent.hidden = true;
|
|
98
|
-
}
|
|
99
|
-
const loadAll = () => embeds.forEach(load);
|
|
100
|
-
|
|
101
|
-
if (hasConsent()) loadAll();
|
|
102
|
-
onConsentChange((allowed) => { if (allowed) loadAll(); });
|
|
103
|
-
embeds.forEach((el) => {
|
|
104
|
-
el.querySelector<HTMLButtonElement>('.b-map__load')?.addEventListener('click', () => setConsent(true));
|
|
79
|
+
initEmbedConsent({
|
|
80
|
+
root: '.b-map__embed',
|
|
81
|
+
frame: '.b-map__frame',
|
|
82
|
+
consent: '.b-map__consent',
|
|
83
|
+
button: '.b-map__load',
|
|
84
|
+
buildFrame: (el, url) =>
|
|
85
|
+
makeEmbedFrame(url, {
|
|
86
|
+
title: el.dataset.title || 'Map',
|
|
87
|
+
height: (el.dataset.height || '450') + 'px',
|
|
88
|
+
referrer: true,
|
|
89
|
+
}),
|
|
105
90
|
});
|
|
106
91
|
</script>
|
|
107
92
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared client-side loader for consent-gated third-party embeds (Google Calendar,
|
|
3
|
+
* Google Maps, the Locations tiles). One implementation, so every embed gates,
|
|
4
|
+
* loads and reacts to consent identically — instead of the same ~25-line script
|
|
5
|
+
* copied into each component (where a fix, or a bug, lands in only one copy).
|
|
6
|
+
*
|
|
7
|
+
* Behaviour: nothing loads until the visitor consents. Accepting the site cookie
|
|
8
|
+
* banner (or the footer preferences panel) grants the shared consent and
|
|
9
|
+
* auto-loads EVERY registered embed on the page; the per-embed button is a
|
|
10
|
+
* fallback that grants that same shared consent. Consent is remembered per
|
|
11
|
+
* visitor. Import from a component `<script>` (it touches the DOM).
|
|
12
|
+
*
|
|
13
|
+
* Each embed root element carries the data the loader needs:
|
|
14
|
+
* data-embed-url the https URL to load (required)
|
|
15
|
+
* data-title the iframe title (optional)
|
|
16
|
+
* data-height pixel height, for fixed-height embeds (optional)
|
|
17
|
+
* and contains a `frame` slot (filled with the iframe), a `consent` card (hidden
|
|
18
|
+
* once loaded) and a `button` that grants consent.
|
|
19
|
+
*/
|
|
20
|
+
import { hasConsent, setConsent, onConsentChange } from './consent-client';
|
|
21
|
+
|
|
22
|
+
export interface EmbedConsentConfig {
|
|
23
|
+
/** Selector for each embed root (one per embed on the page). */
|
|
24
|
+
root: string;
|
|
25
|
+
/** Selector, within a root, of the empty slot the iframe is injected into. */
|
|
26
|
+
frame: string;
|
|
27
|
+
/** Selector, within a root, of the consent card to hide once loaded. */
|
|
28
|
+
consent: string;
|
|
29
|
+
/** Selector, within a root, of the button that grants consent. */
|
|
30
|
+
button: string;
|
|
31
|
+
/** Build the iframe for one root — lets each embed set its own src / size / attrs. */
|
|
32
|
+
buildFrame: (el: HTMLElement, url: string) => HTMLIFrameElement;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Wire up every consent-gated embed matching `cfg.root`. Call once per component. */
|
|
36
|
+
export function initEmbedConsent(cfg: EmbedConsentConfig): void {
|
|
37
|
+
const roots = document.querySelectorAll<HTMLElement>(cfg.root);
|
|
38
|
+
if (!roots.length) return;
|
|
39
|
+
|
|
40
|
+
function load(el: HTMLElement): void {
|
|
41
|
+
if (el.dataset.loaded === 'true') return;
|
|
42
|
+
const url = el.dataset.embedUrl;
|
|
43
|
+
const slot = el.querySelector<HTMLElement>(cfg.frame);
|
|
44
|
+
if (!url || !slot) return;
|
|
45
|
+
slot.replaceChildren(cfg.buildFrame(el, url));
|
|
46
|
+
el.dataset.loaded = 'true';
|
|
47
|
+
const card = el.querySelector<HTMLElement>(cfg.consent);
|
|
48
|
+
if (card) card.hidden = true;
|
|
49
|
+
}
|
|
50
|
+
const loadAll = (): void => roots.forEach(load);
|
|
51
|
+
|
|
52
|
+
if (hasConsent()) loadAll();
|
|
53
|
+
onConsentChange((allowed) => { if (allowed) loadAll(); });
|
|
54
|
+
roots.forEach((el) => {
|
|
55
|
+
el.querySelector<HTMLButtonElement>(cfg.button)?.addEventListener('click', () => setConsent(true));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build a standard lazy embed iframe (100% width, 0 border). */
|
|
60
|
+
export function makeEmbedFrame(
|
|
61
|
+
src: string,
|
|
62
|
+
opts: { title?: string; height?: string; referrer?: boolean } = {},
|
|
63
|
+
): HTMLIFrameElement {
|
|
64
|
+
const f = document.createElement('iframe');
|
|
65
|
+
f.src = src;
|
|
66
|
+
if (opts.title) f.title = opts.title;
|
|
67
|
+
f.loading = 'lazy';
|
|
68
|
+
if (opts.referrer) f.referrerPolicy = 'no-referrer-when-downgrade';
|
|
69
|
+
f.style.width = '100%';
|
|
70
|
+
f.style.height = opts.height ?? '100%';
|
|
71
|
+
f.style.border = '0';
|
|
72
|
+
return f;
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ferst-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ferst Core — the shared, brand-agnostic client-site system: Astro components, layouts, content schemas, the layered config resolver, and a neutral styling architecture that every client overrides.",
|
|
6
6
|
"license": "BUSL-1.1",
|