@getquick/content 0.3.1 → 0.3.2

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/README.md CHANGED
@@ -290,3 +290,84 @@ content runtime, framework or WordPress client. Passing
290
290
  at delivery and stores gq-config's Legal Contract and GA4 settings in each
291
291
  language's chrome; `runtime.consentConfiguration(chrome)` returns the page's
292
292
  consent configuration. See the repository's `docs/consent.md`.
293
+
294
+ ## Design presentation and composition
295
+
296
+ Sites import the canonical presentation and theme APIs from `@getquick/content`.
297
+ The package bundles its pinned Design renderer and contract; Sites do not install
298
+ those packages themselves. Apply `gqd-content` to body and chrome regions and emit
299
+ `designThemeStyleSheet(theme)` followed by `presentationStyleSheet(theme)` once
300
+ per response, using the delivered `theme` from entry/home/chrome. Resolve defaults
301
+ with `resolveDesignTheme` for older stored publications. A changed theme requires
302
+ regenerating both stylesheets, including its media thresholds.
303
+
304
+ `presentationHtml(name, shell, presentation, { attributes, model? })` decorates an
305
+ existing root shell. Pass the complete block attributes for native Group layout
306
+ context. `presentationVariables` takes the same attributes context. Both delegate
307
+ validation and responsive rules to the bundled renderer; saved variables survive
308
+ when parsed attributes are absent. An optional extended `model` must also be
309
+ passed to the corresponding canonical stylesheet generator.
310
+
311
+ `renderDesignPattern` restores native text/list structures, media and layout
312
+ wrappers, resolved Navigation and Query composition. Site `options.blocks`
313
+ renderers retain precedence. Complete saved media HTML stays authoritative;
314
+ source-only Image nodes use `renderDesignImage`. The public low-level HTML
315
+ adapters also support parsed Site adapters without a second presentation parser.
316
+
317
+ Carousel uses `renderDesignCarousel(attributes, orderedChildren, shell?)`, where
318
+ children carry `{ name, attributes, html }` and each `html` is the complete rendered
319
+ child. Supply exactly one navigation child, even though WordPress saves its HTML
320
+ as null. `renderDesignCarouselItem` accepts its rendered foreground children and
321
+ an optional saved figure shell. Emit `designCarouselStyle()` once. Video Hero
322
+ uses `renderDesignVideoHero` and the canonical `videoHeroStyleSheet()`; deferred
323
+ iframes remain `data-src` until consent and motion preferences allow playback.
324
+ Navigation uses `navigationStyleSheet(theme)` for interaction geometry.
325
+
326
+ Browser lifecycle is isolated in `@getquick/content/design/client`, exporting
327
+ `mountDesignCarousels`, `mountVideoHero` and `mountDesignNavigation`. Each returns
328
+ cleanup; mount again after navigation replaces the DOM. Importing the root does
329
+ not require browser globals. The configured runtime gates generated Hero media
330
+ before composing already-delivered children, so consent hooks are not rewritten
331
+ a second time.
332
+
333
+ For a producer-resolved Query, preload outside the synchronous renderer:
334
+
335
+ ```ts
336
+ const loaded = await runtime.preloadDesignQueryPosts(pattern, siteOrigin);
337
+ const html = runtime.renderDesignPattern(pattern, {
338
+ siteOrigin,
339
+ resolvePost: loaded.resolvePost,
340
+ });
341
+ ```
342
+
343
+ This uses the existing `publishedEntry(item.uri)` read for standard Post/Page
344
+ items. Its optional `databaseId` and canonical `designBlocks` become a typed
345
+ `DesignPostContext` through `designPostContext`. Older publications without the
346
+ neutral tree cannot supply that context. Custom post types require an explicit
347
+ authorized preloaded record through `preloadDesignQueryPosts(pattern, load)`;
348
+ this API adds no custom post type GraphQL field or query selection engine.
349
+ Delivered item order, count and duplicates are preserved without applying Query
350
+ arguments again. Synced references add no layout wrapper, and Post Content
351
+ recursively renders the matching preloaded neutral blocks.
352
+
353
+ Native decorative Cover backgrounds use the same explicit Site lifecycle:
354
+
355
+ ```ts
356
+ import { mountDesignCoverVideos } from "@getquick/content/design/client";
357
+ const cleanup = mountDesignCoverVideos(document);
358
+ // Invoke cleanup before removing/replacing the rendered page; remount afterward.
359
+ ```
360
+
361
+ The configured publication delivery and `presentationHtml("core/cover", ...)`
362
+ remove eager `autoplay` from direct native Cover background videos. The initializer
363
+ plays when reduced motion is off, pauses when enabled, resumes when disabled,
364
+ and pauses/removes its preference listener on cleanup. It does not own foreground
365
+ or ordinary content videos. If a Site renders raw Cover HTML outside these paths,
366
+ apply `enhanceDesignCoverVideosHtml(html)` before SSR. Sources, posters and ordered
367
+ foreground remain authoritative; no legacy appearance properties are inferred.
368
+
369
+ `mountDesignNavigation` also enhances native Polylang dropdowns inside
370
+ `.wp-block-polylang-language-switcher`, including ordinary published HTML whose
371
+ inline controller was removed during delivery. It navigates to the delivered
372
+ selected option's HTTP(S) URL; it does not resolve languages or evaluate inline
373
+ scripts. Cleanup removes that handler and restores the original selection.
@@ -1,6 +1,6 @@
1
1
  import { S as SERVICE_CATALOG, T as copyLanguage, c as grantedCategories, d as promptReason, f as readConsentCookie, x as OPTIONAL_CATEGORIES } from "./state-BYWc5TUB.js";
2
- import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-p9SyGNZI.js";
3
- import { t as CONSENT_UI_VERSION } from "./version-ChwS3fUu.js";
2
+ import { a as CONSENT_COPY, c as legalRevisions, r as escapeHtml } from "./embeds-CJJemAmE.js";
3
+ import { t as CONSENT_UI_VERSION } from "./version-DkpQm85L.js";
4
4
  import { t as CONSENT_CONFIG_ID } from "./client-config-B3LNSOxl.js";
5
5
  import { t as CONSENT_PATH } from "./paths-Cmzzof3v.js";
6
6
  //#region frontend/src/consent/astro.ts
@@ -121,13 +121,26 @@ function loadEmbed(placeholder, id) {
121
121
  if (src.protocol !== "https:" || embedService(src) !== id) return;
122
122
  const iframe = document.createElement("iframe");
123
123
  for (const [name, value] of Object.entries(embed.attrs)) if (EMBED_ATTRIBUTES.has(name.toLowerCase()) && typeof value === "string") iframe.setAttribute(name, value);
124
- iframe.src = src.href;
125
124
  const original = placeholder.cloneNode(true);
126
- placeholder.replaceChildren(iframe);
127
- placeholder.setAttribute("data-gq-loaded", "");
128
- placeholder.removeAttribute("role");
129
- placeholder.removeAttribute("aria-label");
130
- originals.set(placeholder, original);
125
+ if (placeholder.hasAttribute("data-gq-decorative") && iframe.classList.contains("gqd-video-hero__video")) {
126
+ iframe.dataset.src = src.href;
127
+ for (const name of [
128
+ "data-gq-consent-embed",
129
+ "data-gq-category",
130
+ "data-gq-embed",
131
+ "data-gq-decorative"
132
+ ]) if (placeholder.hasAttribute(name)) iframe.setAttribute(name, placeholder.getAttribute(name));
133
+ iframe.setAttribute("data-gq-loaded", "");
134
+ placeholder.replaceWith(iframe);
135
+ originals.set(iframe, original);
136
+ } else {
137
+ iframe.src = src.href;
138
+ placeholder.replaceChildren(iframe);
139
+ placeholder.setAttribute("data-gq-loaded", "");
140
+ placeholder.removeAttribute("role");
141
+ placeholder.removeAttribute("aria-label");
142
+ originals.set(placeholder, original);
143
+ }
131
144
  }
132
145
  const originals = /* @__PURE__ */ new WeakMap();
133
146
  function unloadEmbed(placeholder) {
package/dist/consent.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
2
2
  import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
3
- import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-p9SyGNZI.js";
4
- import { t as CONSENT_UI_VERSION } from "./version-ChwS3fUu.js";
3
+ import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-CJJemAmE.js";
4
+ import { t as CONSENT_UI_VERSION } from "./version-DkpQm85L.js";
5
5
  export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };
@@ -0,0 +1,19 @@
1
+ //#region frontend/src/carousel-client.d.ts
2
+ /** Mount each Carousel once. Sites invoke on page load and call cleanup before
3
+ * client navigation replaces the page. DOM APIs are read only on invocation. */
4
+ export declare function mountDesignCarousels(root?: ParentNode): () => void;
5
+ //#endregion
6
+ /** Enhance only decorative media; nested content keeps its native interaction. */
7
+ export declare function mountVideoHero(hero: HTMLElement): () => void;
8
+ //#endregion
9
+ //#region frontend/src/navigation-client.d.ts
10
+ /** Mount the adapter's navigation markup once; cleanup restores server state.
11
+ * Native links retain ordinary Tab order (this is navigation, not an ARIA menu).
12
+ * Install navigationStyleSheet alongside the canonical presentation stylesheet. */
13
+ export declare function mountDesignNavigation(root?: ParentNode): () => void;
14
+ //#endregion
15
+ //#region frontend/src/cover-video-client.d.ts
16
+ /** Mount decorative native Cover backgrounds in neutral, saved or delivered HTML.
17
+ * No DOM access occurs until invocation. The Site owns mounting and cleanup. */
18
+ export declare function mountDesignCoverVideos(root?: ParentNode): () => void;
19
+ //#endregion
@@ -0,0 +1,519 @@
1
+ import { t as videoHeroFrameGeometry } from "./video-hero-D8AcMR4q.js";
2
+ //#region frontend/src/carousel-client.ts
3
+ /** Mount each Carousel once. Sites invoke on page load and call cleanup before
4
+ * client navigation replaces the page. DOM APIs are read only on invocation. */
5
+ function mountDesignCarousels(root) {
6
+ const scope = root ?? (typeof document === "undefined" ? void 0 : document);
7
+ if (!scope) return () => {};
8
+ const carousels = [...scope.querySelectorAll("[data-gqd-carousel]")];
9
+ if ("matches" in scope && typeof scope.matches === "function" && scope.matches("[data-gqd-carousel]")) carousels.unshift(scope);
10
+ const cleanups = [];
11
+ for (const carousel of carousels) {
12
+ if (carousel.hasAttribute("data-gqd-mounted")) continue;
13
+ const slides = [...carousel.children].filter((child) => child instanceof HTMLElement && child.classList.contains("wp-block-getquick-design-slider"));
14
+ if (!slides.length) continue;
15
+ carousel.setAttribute("data-gqd-mounted", "");
16
+ const view = carousel.ownerDocument.defaultView;
17
+ if (!view) continue;
18
+ const motion = view.matchMedia("(prefers-reduced-motion: reduce)");
19
+ const buttons = [...carousel.querySelectorAll("button[data-gqd-go]")].filter((node) => node.closest("[data-gqd-carousel]") === carousel);
20
+ const previous = slides.map((slide) => ({
21
+ slide,
22
+ hidden: slide.hidden,
23
+ inert: slide.inert,
24
+ role: slide.getAttribute("role"),
25
+ label: slide.getAttribute("aria-label"),
26
+ description: slide.getAttribute("aria-roledescription")
27
+ }));
28
+ let index = 0;
29
+ let timer;
30
+ let hovered = false;
31
+ let focused = false;
32
+ let stopped = false;
33
+ const listeners = [];
34
+ const listen = (target, name, handler) => {
35
+ target.addEventListener(name, handler);
36
+ listeners.push(() => target.removeEventListener(name, handler));
37
+ };
38
+ const cancel = () => {
39
+ if (timer !== void 0) clearTimeout(timer);
40
+ timer = void 0;
41
+ };
42
+ const schedule = () => {
43
+ cancel();
44
+ if (stopped || carousel.dataset.gqdAutoplay !== "true" || slides.length < 2 || motion.matches || hovered || focused || carousel.ownerDocument.hidden) return;
45
+ const seconds = Number(carousel.dataset.gqdDuration);
46
+ if (!Number.isInteger(seconds) || seconds < 1 || seconds > 120) return;
47
+ timer = setTimeout(() => {
48
+ if (motion.matches || hovered || focused || carousel.ownerDocument.hidden) {
49
+ schedule();
50
+ return;
51
+ }
52
+ show(index + 1);
53
+ }, seconds * 1e3);
54
+ };
55
+ const show = (next) => {
56
+ index = (next + slides.length) % slides.length;
57
+ slides.forEach((slide, position) => {
58
+ slide.dataset.gqdSlide = "";
59
+ slide.hidden = position !== index;
60
+ slide.inert = position !== index;
61
+ slide.setAttribute("role", "group");
62
+ slide.setAttribute("aria-roledescription", "slide");
63
+ slide.setAttribute("aria-label", `${position + 1} of ${slides.length}`);
64
+ });
65
+ buttons.forEach((button) => {
66
+ if (Number(button.dataset.gqdGo) === index) button.setAttribute("aria-current", "true");
67
+ else button.removeAttribute("aria-current");
68
+ });
69
+ schedule();
70
+ };
71
+ listen(carousel, "click", (event) => {
72
+ const target = event.target instanceof Element ? event.target.closest("button") : null;
73
+ if (!target || target.closest("[data-gqd-carousel]") !== carousel) return;
74
+ if (target.hasAttribute("data-gqd-next")) show(index + 1);
75
+ else if (target.hasAttribute("data-gqd-previous")) show(index - 1);
76
+ else if (target.dataset.gqdGo !== void 0) show(Number(target.dataset.gqdGo));
77
+ });
78
+ listen(carousel, "keydown", (event) => {
79
+ if (!(event instanceof KeyboardEvent) || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
80
+ const target = event.target instanceof Element ? event.target : null;
81
+ if (!target || target.closest("[data-gqd-carousel]") !== carousel || target.closest("input,textarea,select,[contenteditable=true]")) return;
82
+ const control = target.closest("button[data-gqd-next],button[data-gqd-previous],button[data-gqd-go]");
83
+ if (!control || control.closest(".wp-block-getquick-design-carousel-navigation")?.parentElement !== carousel) return;
84
+ if (![
85
+ "ArrowLeft",
86
+ "ArrowRight",
87
+ "Home",
88
+ "End"
89
+ ].includes(event.key)) return;
90
+ event.preventDefault();
91
+ const rtl = view.getComputedStyle(carousel).direction === "rtl";
92
+ show(event.key === "Home" ? 0 : event.key === "End" ? slides.length - 1 : index + (event.key === "ArrowRight" !== rtl ? 1 : -1));
93
+ });
94
+ listen(carousel, "mouseenter", () => {
95
+ hovered = true;
96
+ cancel();
97
+ });
98
+ listen(carousel, "mouseleave", () => {
99
+ hovered = false;
100
+ schedule();
101
+ });
102
+ listen(carousel, "focusin", () => {
103
+ focused = true;
104
+ cancel();
105
+ });
106
+ listen(carousel, "focusout", (event) => {
107
+ focused = event instanceof FocusEvent && event.relatedTarget instanceof Node && carousel.contains(event.relatedTarget);
108
+ schedule();
109
+ });
110
+ listen(motion, "change", schedule);
111
+ listen(carousel.ownerDocument, "visibilitychange", schedule);
112
+ show(0);
113
+ cleanups.push(() => {
114
+ stopped = true;
115
+ cancel();
116
+ listeners.forEach((remove) => remove());
117
+ carousel.removeAttribute("data-gqd-mounted");
118
+ for (const { slide, hidden, inert, role, label, description } of previous) {
119
+ slide.hidden = hidden;
120
+ slide.inert = inert;
121
+ slide.removeAttribute("data-gqd-slide");
122
+ for (const [name, value] of [
123
+ ["role", role],
124
+ ["aria-label", label],
125
+ ["aria-roledescription", description]
126
+ ]) if (value === null) slide.removeAttribute(name);
127
+ else slide.setAttribute(name, value);
128
+ }
129
+ });
130
+ }
131
+ return () => cleanups.forEach((cleanup) => cleanup());
132
+ }
133
+ //#endregion
134
+ /** Enhance only decorative media; nested content keeps its native interaction. */
135
+ function mountVideoHero(hero) {
136
+ const media = hero.querySelector(":scope > .gqd-video-hero__media");
137
+ if (!media) return () => {};
138
+ let frame = null;
139
+ let video = null;
140
+ const sources = /* @__PURE__ */ new WeakMap();
141
+ const motion = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
142
+ const update = () => {
143
+ const nextFrame = media.querySelector("iframe");
144
+ const nextVideo = media.querySelector("video");
145
+ if (frame !== nextFrame) frame?.removeAttribute("src");
146
+ if (video !== nextVideo) video?.pause();
147
+ frame = nextFrame;
148
+ video = nextVideo;
149
+ if (video) {
150
+ if (motion.matches) video.pause();
151
+ else video.play().catch(() => {});
152
+ }
153
+ if (!frame) return;
154
+ const source = frame.dataset.src || frame.getAttribute("src") || sources.get(frame);
155
+ if (source) sources.set(frame, source);
156
+ if (motion.matches) {
157
+ frame.removeAttribute("src");
158
+ frame.hidden = true;
159
+ return;
160
+ }
161
+ frame.hidden = false;
162
+ if (source && frame.getAttribute("src") !== source) frame.setAttribute("src", source);
163
+ const style = globalThis.getComputedStyle(frame);
164
+ const [x, y] = style.objectPosition.split(" ").map((v) => Number.parseFloat(v) / 100);
165
+ const geometry = videoHeroFrameGeometry({
166
+ width: media.clientWidth,
167
+ height: media.clientHeight
168
+ }, style.objectFit, {
169
+ x: Number.isFinite(x) ? x : .5,
170
+ y: Number.isFinite(y) ? y : .5
171
+ });
172
+ for (const [property, value] of Object.entries(geometry)) frame.style.setProperty(property, `${value}px`);
173
+ };
174
+ const resize = new globalThis.ResizeObserver(update);
175
+ resize.observe(media);
176
+ const attributes = new globalThis.MutationObserver(update);
177
+ attributes.observe(hero, {
178
+ attributes: true,
179
+ attributeFilter: ["style", "class"]
180
+ });
181
+ const children = new globalThis.MutationObserver(update);
182
+ children.observe(media, {
183
+ childList: true,
184
+ subtree: true
185
+ });
186
+ globalThis.addEventListener("resize", update);
187
+ motion.addEventListener("change", update);
188
+ update();
189
+ return () => {
190
+ resize.disconnect();
191
+ attributes.disconnect();
192
+ children.disconnect();
193
+ globalThis.removeEventListener("resize", update);
194
+ motion.removeEventListener("change", update);
195
+ video?.pause();
196
+ frame?.removeAttribute("src");
197
+ };
198
+ }
199
+ //#endregion
200
+ //#region frontend/src/navigation-client.ts
201
+ const mounted$1 = /* @__PURE__ */ new WeakSet();
202
+ const inertLocks = /* @__PURE__ */ new WeakMap();
203
+ function inertBackground(panel) {
204
+ const locked = [];
205
+ for (let branch = panel; branch.parentElement; branch = branch.parentElement) {
206
+ for (const sibling of branch.parentElement.children) {
207
+ if (sibling === branch || !("inert" in sibling)) continue;
208
+ const node = sibling;
209
+ const lock = inertLocks.get(node) ?? {
210
+ count: 0,
211
+ original: node.inert
212
+ };
213
+ lock.count += 1;
214
+ inertLocks.set(node, lock);
215
+ node.inert = true;
216
+ locked.push(node);
217
+ }
218
+ if (branch.parentElement === panel.ownerDocument.body) break;
219
+ }
220
+ return () => {
221
+ for (const node of locked) {
222
+ const lock = inertLocks.get(node);
223
+ if (!lock || --lock.count > 0) continue;
224
+ node.inert = lock.original;
225
+ inertLocks.delete(node);
226
+ }
227
+ };
228
+ }
229
+ /** Mount the adapter's navigation markup once; cleanup restores server state.
230
+ * Native links retain ordinary Tab order (this is navigation, not an ARIA menu).
231
+ * Install navigationStyleSheet alongside the canonical presentation stylesheet. */
232
+ function mountDesignNavigation(root) {
233
+ const scope = root ?? (typeof document === "undefined" ? void 0 : document);
234
+ if (!scope) return () => {};
235
+ const cleanups = [];
236
+ const languageSelects = [...scope.querySelectorAll(".wp-block-polylang-language-switcher select.pll-switcher-select")];
237
+ if ("matches" in scope && typeof scope.matches === "function" && scope.matches(".wp-block-polylang-language-switcher select.pll-switcher-select")) languageSelects.unshift(scope);
238
+ for (const select of languageSelects) {
239
+ const view = select.ownerDocument.defaultView;
240
+ if (!view || mounted$1.has(select)) continue;
241
+ mounted$1.add(select);
242
+ const original = select.selectedIndex;
243
+ const change = (event) => {
244
+ event.stopImmediatePropagation();
245
+ const option = select.selectedOptions[0];
246
+ const value = option?.value.trim();
247
+ if (!option || option.disabled || !value || value.includes("\\")) return;
248
+ for (let index = 0; index < value.length; index++) if (value.charCodeAt(index) <= 32 || value.charCodeAt(index) === 127) return;
249
+ try {
250
+ const url = new URL(value, select.ownerDocument.baseURI);
251
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return;
252
+ view.location.assign(url.href);
253
+ } catch {}
254
+ };
255
+ select.addEventListener("change", change, true);
256
+ cleanups.push(() => {
257
+ select.removeEventListener("change", change, true);
258
+ select.selectedIndex = original;
259
+ mounted$1.delete(select);
260
+ });
261
+ }
262
+ const languageMenus = [...scope.querySelectorAll("details[data-gqd-language-menu]")];
263
+ if ("matches" in scope && typeof scope.matches === "function" && scope.matches("details[data-gqd-language-menu]")) languageMenus.unshift(scope);
264
+ for (const menu of languageMenus) {
265
+ if (mounted$1.has(menu)) continue;
266
+ mounted$1.add(menu);
267
+ const original = menu.open;
268
+ const escape = (event) => {
269
+ if (event.key === "Escape" && menu.open) {
270
+ event.preventDefault();
271
+ menu.open = false;
272
+ menu.querySelector("summary")?.focus();
273
+ }
274
+ };
275
+ menu.addEventListener("keydown", escape);
276
+ cleanups.push(() => {
277
+ menu.removeEventListener("keydown", escape);
278
+ menu.open = original;
279
+ mounted$1.delete(menu);
280
+ });
281
+ }
282
+ const roots = [...scope.querySelectorAll("[data-gqd-navigation]")];
283
+ if ("matches" in scope && typeof scope.matches === "function" && scope.matches("[data-gqd-navigation]")) roots.unshift(scope);
284
+ for (const nav of roots) {
285
+ if (mounted$1.has(nav)) continue;
286
+ const view = nav.ownerDocument.defaultView;
287
+ const panel = nav.querySelector("[data-gqd-navigation-panel]");
288
+ const open = nav.querySelector("[data-gqd-navigation-open]");
289
+ const close = nav.querySelector("[data-gqd-navigation-close]");
290
+ if (!view || !panel || !open || !close) continue;
291
+ mounted$1.add(nav);
292
+ const buttons = [...nav.querySelectorAll("[data-gqd-submenu]")];
293
+ const lists = new Map(buttons.map((button) => [button, [...nav.querySelectorAll("[data-gqd-submenu-list]")].find((list) => list.id === button.getAttribute("aria-controls"))]));
294
+ const saved = /* @__PURE__ */ new Map();
295
+ const remember = (node, names) => saved.set(node, new Map(names.map((name) => [name, node.getAttribute(name)])));
296
+ remember(nav, ["data-gqd-navigation-enhanced", "data-gqd-overlay-active"]);
297
+ remember(panel, [
298
+ "hidden",
299
+ "role",
300
+ "aria-modal",
301
+ "aria-label",
302
+ "style"
303
+ ]);
304
+ remember(open, ["aria-expanded"]);
305
+ buttons.forEach((button) => remember(button, ["aria-expanded"]));
306
+ for (const list of lists.values()) if (list) remember(list, ["hidden", "style"]);
307
+ const restore = (node, names) => {
308
+ for (const name of names) {
309
+ const value = saved.get(node)?.get(name);
310
+ if (value == null) node.removeAttribute(name);
311
+ else node.setAttribute(name, value);
312
+ }
313
+ };
314
+ const currentUrl = new URL(nav.ownerDocument.URL);
315
+ const path = (value) => value.replace(/\/+$/, "") || "/";
316
+ for (const anchor of nav.querySelectorAll("a[href]")) {
317
+ if (anchor.hasAttribute("aria-current")) continue;
318
+ try {
319
+ const target = new URL(anchor.getAttribute("href"), nav.ownerDocument.baseURI);
320
+ if (!target.hash && target.origin === currentUrl.origin && path(target.pathname) === path(currentUrl.pathname) && (!target.search || target.search === currentUrl.search)) {
321
+ remember(anchor, ["aria-current"]);
322
+ anchor.setAttribute("aria-current", "page");
323
+ }
324
+ } catch {}
325
+ }
326
+ const listeners = [];
327
+ const listen = (node, name, callback) => {
328
+ node.addEventListener(name, callback);
329
+ listeners.push(() => node.removeEventListener(name, callback));
330
+ };
331
+ let overlay = false;
332
+ let expanded = false;
333
+ let unlock;
334
+ const element = (target) => target && "nodeType" in target && target.nodeType === 1 ? target : null;
335
+ const setHidden = (node, hidden) => {
336
+ node.hidden = hidden;
337
+ restore(node, ["style"]);
338
+ if (hidden) node.style.setProperty("display", "none", "important");
339
+ };
340
+ const setSubmenu = (button, state) => {
341
+ const list = lists.get(button);
342
+ button.setAttribute("aria-expanded", String(state));
343
+ if (list) setHidden(list, !state);
344
+ if (!state && list) {
345
+ for (const nested of buttons) if (list.contains(nested)) {
346
+ nested.setAttribute("aria-expanded", "false");
347
+ const child = lists.get(nested);
348
+ if (child) setHidden(child, true);
349
+ }
350
+ }
351
+ };
352
+ const focusables = (node) => [...node.querySelectorAll("a[href],button:not([disabled]),summary,[tabindex=\"0\"]")].filter((candidate) => !candidate.closest("[hidden]") && view.getComputedStyle(candidate).visibility !== "hidden" && candidate.getClientRects().length > 0);
353
+ const setOverlay = (state, returnFocus = false) => {
354
+ expanded = state && overlay;
355
+ open.setAttribute("aria-expanded", String(expanded));
356
+ setHidden(panel, overlay && !expanded);
357
+ if (expanded) {
358
+ unlock ??= inertBackground(panel);
359
+ panel.setAttribute("role", "dialog");
360
+ panel.setAttribute("aria-modal", "true");
361
+ panel.setAttribute("aria-label", nav.getAttribute("aria-label") || "Navigation");
362
+ focusables(panel)[0]?.focus();
363
+ } else {
364
+ unlock?.();
365
+ unlock = void 0;
366
+ restore(panel, [
367
+ "role",
368
+ "aria-modal",
369
+ "aria-label"
370
+ ]);
371
+ if (returnFocus) open.focus();
372
+ }
373
+ };
374
+ const threshold = view.getComputedStyle(nav).getPropertyValue("--gqd-navigation-breakpoint").trim() || "64rem";
375
+ const mobile = view.matchMedia(`(width < ${threshold})`);
376
+ const sync = () => {
377
+ const previous = overlay;
378
+ const focused = nav.ownerDocument.activeElement;
379
+ const panelFocused = focused !== null && panel.contains(focused);
380
+ overlay = nav.dataset.gqdOverlay === "always" || nav.dataset.gqdOverlay !== "never" && mobile.matches;
381
+ nav.toggleAttribute("data-gqd-overlay-active", overlay);
382
+ setOverlay(previous === overlay && expanded);
383
+ if (previous !== overlay) {
384
+ if (overlay && panelFocused) open.focus();
385
+ else if (!overlay && (focused === open || focused === close)) focusables(panel)[0]?.focus();
386
+ }
387
+ };
388
+ nav.setAttribute("data-gqd-navigation-enhanced", "");
389
+ for (const button of buttons) setSubmenu(button, nav.dataset.gqdSubmenuVisibility === "always");
390
+ sync();
391
+ listen(mobile, "change", sync);
392
+ listen(nav.ownerDocument, "focusin", (event) => {
393
+ const target = element(event.target);
394
+ if (expanded && target && !panel.contains(target)) focusables(panel)[0]?.focus();
395
+ });
396
+ listen(open, "click", () => setOverlay(true));
397
+ listen(close, "click", () => {
398
+ setOverlay(false, true);
399
+ });
400
+ for (const button of buttons) {
401
+ listen(button, "click", () => {
402
+ if (nav.dataset.gqdSubmenuVisibility !== "always") setSubmenu(button, button.getAttribute("aria-expanded") !== "true");
403
+ });
404
+ const row = button.closest("li") ?? button.parentElement;
405
+ const hover = () => {
406
+ if (nav.dataset.gqdSubmenuVisibility === "hover" && !overlay) setSubmenu(button, true);
407
+ };
408
+ listen(row, "pointerenter", hover);
409
+ listen(row, "pointerleave", () => {
410
+ if (nav.dataset.gqdSubmenuVisibility === "hover" && !row.contains(nav.ownerDocument.activeElement)) setSubmenu(button, false);
411
+ });
412
+ listen(row, "focusin", (event) => {
413
+ if (event.target !== button) hover();
414
+ });
415
+ listen(row, "focusout", (event) => {
416
+ const next = event.relatedTarget;
417
+ if (!next || !row.contains(next)) {
418
+ if (nav.dataset.gqdSubmenuVisibility !== "always") setSubmenu(button, false);
419
+ }
420
+ });
421
+ }
422
+ listen(nav, "keydown", (raw) => {
423
+ const event = raw;
424
+ const target = element(event.target);
425
+ if (!target || event.altKey || event.ctrlKey || event.metaKey) return;
426
+ const rtl = view.getComputedStyle(nav).direction === "rtl";
427
+ const button = target.closest("[data-gqd-submenu]");
428
+ if (button && ["ArrowDown", rtl ? "ArrowLeft" : "ArrowRight"].includes(event.key)) {
429
+ event.preventDefault();
430
+ setSubmenu(button, true);
431
+ const list = lists.get(button);
432
+ if (list) focusables(list)[0]?.focus();
433
+ return;
434
+ }
435
+ if (event.key === "Escape" || event.key === (rtl ? "ArrowRight" : "ArrowLeft")) {
436
+ const list = target.closest("[data-gqd-submenu-list]");
437
+ const row = target.closest("li");
438
+ const owner = (event.key === "Escape" && row ? buttons.find((candidate) => candidate.closest("li") === row && candidate.getAttribute("aria-expanded") === "true") : null) ?? (button?.getAttribute("aria-expanded") === "true" ? button : list ? buttons.find((candidate) => lists.get(candidate) === list) : null);
439
+ if (owner && owner.getAttribute("aria-expanded") === "true" && nav.dataset.gqdSubmenuVisibility !== "always") {
440
+ event.preventDefault();
441
+ event.stopPropagation();
442
+ setSubmenu(owner, false);
443
+ owner.focus();
444
+ return;
445
+ }
446
+ if (event.key === "Escape" && expanded) {
447
+ event.preventDefault();
448
+ setOverlay(false, true);
449
+ }
450
+ }
451
+ if (event.key === "Tab" && expanded) {
452
+ const candidates = focusables(panel);
453
+ const first = candidates[0], last = candidates.at(-1);
454
+ if (event.shiftKey && target === first) {
455
+ event.preventDefault();
456
+ last?.focus();
457
+ } else if (!event.shiftKey && target === last) {
458
+ event.preventDefault();
459
+ first?.focus();
460
+ }
461
+ }
462
+ });
463
+ listen(nav.ownerDocument, "pointerdown", (event) => {
464
+ const target = element(event.target);
465
+ if (target && !nav.contains(target) && nav.dataset.gqdSubmenuVisibility !== "always") buttons.forEach((button) => setSubmenu(button, false));
466
+ });
467
+ cleanups.push(() => {
468
+ listeners.forEach((remove) => remove());
469
+ unlock?.();
470
+ for (const [node, attributes] of saved) restore(node, [...attributes.keys()]);
471
+ mounted$1.delete(nav);
472
+ });
473
+ }
474
+ let disposed = false;
475
+ return () => {
476
+ if (disposed) return;
477
+ disposed = true;
478
+ cleanups.forEach((cleanup) => cleanup());
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region frontend/src/cover-video-client.ts
483
+ const mounted = /* @__PURE__ */ new WeakSet();
484
+ /** Mount decorative native Cover backgrounds in neutral, saved or delivered HTML.
485
+ * No DOM access occurs until invocation. The Site owns mounting and cleanup. */
486
+ function mountDesignCoverVideos(root) {
487
+ const scope = root ?? document;
488
+ const cleanups = Array.from(scope.querySelectorAll(".wp-block-cover > video.wp-block-cover__video-background")).map((video) => {
489
+ const view = video.ownerDocument.defaultView;
490
+ if (!view || mounted.has(video)) return () => {};
491
+ mounted.add(video);
492
+ const motion = view.matchMedia("(prefers-reduced-motion: reduce)");
493
+ let active = true;
494
+ video.autoplay = false;
495
+ video.removeAttribute("autoplay");
496
+ video.muted = true;
497
+ const update = () => {
498
+ if (!active || motion.matches) {
499
+ video.pause();
500
+ return;
501
+ }
502
+ video.play().then(() => {
503
+ if (active && motion.matches || !active && !mounted.has(video)) video.pause();
504
+ }, () => {});
505
+ };
506
+ motion.addEventListener("change", update);
507
+ update();
508
+ return () => {
509
+ if (!active) return;
510
+ active = false;
511
+ motion.removeEventListener("change", update);
512
+ mounted.delete(video);
513
+ video.pause();
514
+ };
515
+ });
516
+ return () => cleanups.forEach((cleanup) => cleanup());
517
+ }
518
+ //#endregion
519
+ export { mountDesignCarousels, mountDesignCoverVideos, mountDesignNavigation, mountVideoHero };