@sumaq/site-kit 0.2.0
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 +93 -0
- package/package.json +57 -0
- package/src/astro-modules.d.ts +11 -0
- package/src/blocks/About.astro +94 -0
- package/src/blocks/Audience.astro +66 -0
- package/src/blocks/BlockRenderer.astro +105 -0
- package/src/blocks/Contact.astro +58 -0
- package/src/blocks/ContactDetails.astro +91 -0
- package/src/blocks/CtaBand.astro +53 -0
- package/src/blocks/EmergencyGrid.astro +47 -0
- package/src/blocks/EmergencyHelplines.astro +109 -0
- package/src/blocks/EmergencyPrimary.astro +51 -0
- package/src/blocks/Faq.astro +56 -0
- package/src/blocks/FeatureList.astro +73 -0
- package/src/blocks/Gallery.astro +95 -0
- package/src/blocks/Generic.astro +30 -0
- package/src/blocks/Hero.astro +82 -0
- package/src/blocks/IntroCta.astro +41 -0
- package/src/blocks/LegalDocument.astro +83 -0
- package/src/blocks/LocationsOverview.astro +161 -0
- package/src/blocks/NotFound.astro +33 -0
- package/src/blocks/Notice.astro +49 -0
- package/src/blocks/PageBanner.astro +57 -0
- package/src/blocks/Pricing.astro +97 -0
- package/src/blocks/ProfileDetail.astro +202 -0
- package/src/blocks/Projects.astro +91 -0
- package/src/blocks/Quote.astro +53 -0
- package/src/blocks/Services.astro +46 -0
- package/src/blocks/Skills.astro +65 -0
- package/src/blocks/Stats.astro +54 -0
- package/src/blocks/Steps.astro +76 -0
- package/src/blocks/TeamDirectory.astro +164 -0
- package/src/blocks/TeamTeaser.astro +84 -0
- package/src/blocks/Testimonials.astro +75 -0
- package/src/blocks/Timeline.astro +65 -0
- package/src/components/Actions.astro +67 -0
- package/src/components/Card.astro +104 -0
- package/src/components/Logo.astro +57 -0
- package/src/components/Media.astro +83 -0
- package/src/components/Prose.astro +29 -0
- package/src/components/ScrollToTop.astro +49 -0
- package/src/components/Section.astro +75 -0
- package/src/components/SectionHeader.astro +58 -0
- package/src/components/SectionHeading.astro +76 -0
- package/src/components/Seo.astro +55 -0
- package/src/components/SiteFooter.astro +90 -0
- package/src/components/SiteHeader.astro +97 -0
- package/src/config.ts +59 -0
- package/src/index.ts +75 -0
- package/src/layouts/Base.astro +57 -0
- package/src/lib/collections.ts +101 -0
- package/src/lib/content.ts +51 -0
- package/src/lib/format.ts +17 -0
- package/src/lib/markdown.ts +5 -0
- package/src/profiles/clinic.yaml +35 -0
- package/src/profiles/portfolio.yaml +27 -0
- package/src/profiles/therapist.yaml +31 -0
- package/src/scripts/enhancements.js +459 -0
- package/src/tokens/class-inventory.json +284 -0
- package/src/tokens/index.css +83 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
const initEnhancements = () => {
|
|
2
|
+
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3
|
+
|
|
4
|
+
// 1. Sticky Header scroll behavior
|
|
5
|
+
const header = document.querySelector(".sq-header");
|
|
6
|
+
if (header) {
|
|
7
|
+
const handleScroll = () => {
|
|
8
|
+
header.classList.toggle("is-scrolled", window.scrollY > 15);
|
|
9
|
+
};
|
|
10
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
11
|
+
handleScroll();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 2. Mobile Menu — open/close, focus return, Esc, link close
|
|
15
|
+
const navToggle = document.querySelector(".sq-nav__toggle");
|
|
16
|
+
const siteNav = document.getElementById("site-nav");
|
|
17
|
+
|
|
18
|
+
const setMenuOpen = (isOpen) => {
|
|
19
|
+
if (!navToggle || !siteNav) return;
|
|
20
|
+
siteNav.classList.toggle("is-open", isOpen);
|
|
21
|
+
navToggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
22
|
+
navToggle.setAttribute("aria-label", isOpen ? "Menü schließen" : "Menü öffnen");
|
|
23
|
+
document.body.classList.toggle("nav-open", isOpen);
|
|
24
|
+
|
|
25
|
+
if (isOpen) {
|
|
26
|
+
const firstLink = siteNav.querySelector("a, button");
|
|
27
|
+
if (firstLink) firstLink.focus();
|
|
28
|
+
} else {
|
|
29
|
+
navToggle.focus();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (navToggle && siteNav && header) {
|
|
34
|
+
navToggle.addEventListener("click", (e) => {
|
|
35
|
+
e.stopPropagation();
|
|
36
|
+
setMenuOpen(!siteNav.classList.contains("is-open"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
document.addEventListener("click", (e) => {
|
|
40
|
+
if (!siteNav.classList.contains("is-open")) return;
|
|
41
|
+
if (!header.contains(e.target)) setMenuOpen(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
document.addEventListener("keydown", (e) => {
|
|
45
|
+
if (e.key === "Escape" && siteNav.classList.contains("is-open")) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setMenuOpen(false);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Keep Tab cycling within header+nav while menu is open on small screens
|
|
51
|
+
if (e.key !== "Tab" || !siteNav.classList.contains("is-open")) return;
|
|
52
|
+
if (window.matchMedia("(min-width: 769px)").matches) return;
|
|
53
|
+
|
|
54
|
+
const focusables = [navToggle, ...siteNav.querySelectorAll("a[href], button:not([disabled])")].filter(
|
|
55
|
+
(el) => el && !el.hasAttribute("disabled") && el.offsetParent !== null
|
|
56
|
+
);
|
|
57
|
+
if (!focusables.length) return;
|
|
58
|
+
|
|
59
|
+
const first = focusables[0];
|
|
60
|
+
const last = focusables[focusables.length - 1];
|
|
61
|
+
const active = document.activeElement;
|
|
62
|
+
|
|
63
|
+
if (e.shiftKey && active === first) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
last.focus();
|
|
66
|
+
} else if (!e.shiftKey && active === last) {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
first.focus();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
siteNav.querySelectorAll("a").forEach((link) => {
|
|
73
|
+
link.addEventListener("click", () => {
|
|
74
|
+
if (siteNav.classList.contains("is-open")) setMenuOpen(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Image Gallery Swapping (Standorte page – location thumbs)
|
|
80
|
+
const thumbs = document.querySelectorAll(".sq-location__thumb");
|
|
81
|
+
thumbs.forEach((thumb) => {
|
|
82
|
+
thumb.addEventListener("click", function () {
|
|
83
|
+
const targetId = this.getAttribute("data-target");
|
|
84
|
+
const mainImg = document.getElementById(targetId);
|
|
85
|
+
const newSrc = this.getAttribute("data-src");
|
|
86
|
+
const newAlt = this.getAttribute("data-alt") || "";
|
|
87
|
+
|
|
88
|
+
if (mainImg && newSrc) {
|
|
89
|
+
const swap = () => {
|
|
90
|
+
mainImg.setAttribute("src", newSrc);
|
|
91
|
+
mainImg.setAttribute("alt", newAlt);
|
|
92
|
+
mainImg.style.opacity = 1;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (reduceMotion) {
|
|
96
|
+
swap();
|
|
97
|
+
} else {
|
|
98
|
+
mainImg.style.opacity = 0;
|
|
99
|
+
setTimeout(swap, 150);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const gallery = this.closest(".sq-location__gallery");
|
|
103
|
+
if (gallery) {
|
|
104
|
+
gallery.querySelectorAll(".sq-location__thumb").forEach((t) => t.classList.remove("is-active"));
|
|
105
|
+
}
|
|
106
|
+
this.classList.add("is-active");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 3b. Room gallery dots (standorte detail)
|
|
112
|
+
document.querySelectorAll("[data-room-gallery]").forEach((gallery) => {
|
|
113
|
+
const slides = gallery.querySelectorAll(".sq-room__slides img");
|
|
114
|
+
const dots = gallery.querySelectorAll(".sq-room__dot");
|
|
115
|
+
if (!slides.length || !dots.length) return;
|
|
116
|
+
|
|
117
|
+
const showSlide = (index) => {
|
|
118
|
+
slides.forEach((slide, i) => {
|
|
119
|
+
slide.classList.toggle("is-active", i === index);
|
|
120
|
+
});
|
|
121
|
+
dots.forEach((dot, i) => {
|
|
122
|
+
const isActive = i === index;
|
|
123
|
+
dot.classList.toggle("is-active", isActive);
|
|
124
|
+
dot.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
dots.forEach((dot) => {
|
|
129
|
+
dot.addEventListener("click", () => {
|
|
130
|
+
const index = Number(dot.getAttribute("data-index"));
|
|
131
|
+
if (!Number.isNaN(index)) showSlide(index);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 3c. Location -> room page transition (only when "Räume ansehen" is clicked;
|
|
137
|
+
// every other way of reaching /standorte/[slug] keeps the default transition)
|
|
138
|
+
document.querySelectorAll("[data-rooms-cta]").forEach((cta) => {
|
|
139
|
+
cta.addEventListener("click", () => {
|
|
140
|
+
const id = cta.getAttribute("data-rooms-cta");
|
|
141
|
+
const name = document.querySelector(`[data-location-name="${id}"]`);
|
|
142
|
+
if (name) name.style.viewTransitionName = `location-name-${id}`;
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 4. Team Search + Category Filter
|
|
147
|
+
const searchInput = document.getElementById("team-search");
|
|
148
|
+
const teamGrid = document.getElementById("team-grid");
|
|
149
|
+
if (searchInput && teamGrid) {
|
|
150
|
+
const cards = Array.from(teamGrid.querySelectorAll(".sq-team__card"));
|
|
151
|
+
const counter = document.getElementById("search-counter");
|
|
152
|
+
const noResults = document.getElementById("no-results");
|
|
153
|
+
const filterButtons = Array.from(document.querySelectorAll(".sq-team__filter"));
|
|
154
|
+
const transitionMs = reduceMotion ? 0 : 320;
|
|
155
|
+
let activeFilter = "alle";
|
|
156
|
+
const hideTimers = new WeakMap();
|
|
157
|
+
let debounceTimer = null;
|
|
158
|
+
|
|
159
|
+
if (counter) {
|
|
160
|
+
counter.setAttribute("aria-live", "polite");
|
|
161
|
+
counter.setAttribute("role", "status");
|
|
162
|
+
}
|
|
163
|
+
if (noResults) {
|
|
164
|
+
noResults.setAttribute("role", "status");
|
|
165
|
+
noResults.setAttribute("aria-live", "polite");
|
|
166
|
+
noResults.hidden = true;
|
|
167
|
+
noResults.style.display = "";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Copy lives in the markup (data-counter-*), not here: the kit ships no
|
|
171
|
+
// language. `{count}` is the only placeholder.
|
|
172
|
+
const counterTemplates = {
|
|
173
|
+
all: counter?.getAttribute("data-counter-all") || "{count}",
|
|
174
|
+
results: counter?.getAttribute("data-counter-results") || "{count}",
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const updateCounter = (visibleCount, query) => {
|
|
178
|
+
if (!counter) return;
|
|
179
|
+
const template =
|
|
180
|
+
query === "" && activeFilter === "alle" ? counterTemplates.all : counterTemplates.results;
|
|
181
|
+
counter.innerHTML = template.replace(
|
|
182
|
+
"{count}",
|
|
183
|
+
`<span id="member-count">${visibleCount}</span>`
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const matchesCard = (card, query) => {
|
|
188
|
+
const searchTerms = (card.getAttribute("data-search") || "").toLowerCase();
|
|
189
|
+
const categories = (card.getAttribute("data-categories") || "").toLowerCase();
|
|
190
|
+
const queryOk = !query || searchTerms.includes(query);
|
|
191
|
+
const filterOk = activeFilter === "alle" || categories.split(/\s+/).includes(activeFilter);
|
|
192
|
+
return queryOk && filterOk;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const showCard = (card) => {
|
|
196
|
+
const pending = hideTimers.get(card);
|
|
197
|
+
if (pending) {
|
|
198
|
+
clearTimeout(pending);
|
|
199
|
+
hideTimers.delete(card);
|
|
200
|
+
}
|
|
201
|
+
if (!card.classList.contains("is-hidden") && !card.classList.contains("is-hiding")) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
card.classList.remove("is-hidden", "is-hiding");
|
|
205
|
+
card.removeAttribute("aria-hidden");
|
|
206
|
+
if (transitionMs === 0) return;
|
|
207
|
+
card.classList.add("is-showing");
|
|
208
|
+
void card.offsetWidth;
|
|
209
|
+
requestAnimationFrame(() => {
|
|
210
|
+
card.classList.remove("is-showing");
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const hideCard = (card) => {
|
|
215
|
+
if (card.classList.contains("is-hidden") || card.classList.contains("is-hiding")) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (transitionMs === 0) {
|
|
219
|
+
card.classList.add("is-hidden");
|
|
220
|
+
card.setAttribute("aria-hidden", "true");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
card.classList.add("is-hiding");
|
|
224
|
+
const timer = setTimeout(() => {
|
|
225
|
+
card.classList.add("is-hidden");
|
|
226
|
+
card.classList.remove("is-hiding");
|
|
227
|
+
card.setAttribute("aria-hidden", "true");
|
|
228
|
+
hideTimers.delete(card);
|
|
229
|
+
}, transitionMs);
|
|
230
|
+
hideTimers.set(card, timer);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const applyFilters = () => {
|
|
234
|
+
const query = searchInput.value.toLowerCase().trim();
|
|
235
|
+
let visibleCount = 0;
|
|
236
|
+
|
|
237
|
+
cards.forEach((card) => {
|
|
238
|
+
if (matchesCard(card, query)) {
|
|
239
|
+
showCard(card);
|
|
240
|
+
visibleCount++;
|
|
241
|
+
} else {
|
|
242
|
+
hideCard(card);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
updateCounter(visibleCount, query);
|
|
247
|
+
|
|
248
|
+
if (noResults) {
|
|
249
|
+
const empty = visibleCount === 0;
|
|
250
|
+
noResults.hidden = !empty;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
searchInput.addEventListener("input", () => {
|
|
255
|
+
clearTimeout(debounceTimer);
|
|
256
|
+
debounceTimer = setTimeout(applyFilters, 150);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
filterButtons.forEach((btn) => {
|
|
260
|
+
btn.addEventListener("click", () => {
|
|
261
|
+
activeFilter = btn.getAttribute("data-filter") || "alle";
|
|
262
|
+
filterButtons.forEach((b) => {
|
|
263
|
+
const isActive = b === btn;
|
|
264
|
+
b.classList.toggle("is-active", isActive);
|
|
265
|
+
b.setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
266
|
+
});
|
|
267
|
+
applyFilters();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 4b. Broken image fallback (team / profile)
|
|
273
|
+
document.querySelectorAll("img[src^='media/team/']").forEach((img) => {
|
|
274
|
+
img.addEventListener("error", () => {
|
|
275
|
+
if (img.dataset.fallbackApplied) return;
|
|
276
|
+
img.dataset.fallbackApplied = "1";
|
|
277
|
+
img.src = "media/team/profile_placeholder.CZDfc6EN_1FnOqJ.webp";
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// 4c. Gallery lightbox (<Gallery lightbox />) — one viewer per gallery,
|
|
282
|
+
// keyboard-navigable, restores focus to the trigger on close.
|
|
283
|
+
document.querySelectorAll("[data-lightbox]").forEach((viewer) => {
|
|
284
|
+
const image = viewer.querySelector("[data-lightbox-image]");
|
|
285
|
+
const gallery = document.querySelector("[data-gallery]");
|
|
286
|
+
const triggers = gallery ? Array.from(gallery.querySelectorAll("[data-gallery-index]")) : [];
|
|
287
|
+
if (!image || !triggers.length) return;
|
|
288
|
+
|
|
289
|
+
let current = 0;
|
|
290
|
+
let opener = null;
|
|
291
|
+
|
|
292
|
+
const show = (index) => {
|
|
293
|
+
current = (index + triggers.length) % triggers.length;
|
|
294
|
+
const trigger = triggers[current];
|
|
295
|
+
image.setAttribute("src", trigger.getAttribute("data-src"));
|
|
296
|
+
image.setAttribute("alt", trigger.getAttribute("data-alt") || "");
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const open = (index, trigger) => {
|
|
300
|
+
opener = trigger;
|
|
301
|
+
show(index);
|
|
302
|
+
viewer.hidden = false;
|
|
303
|
+
viewer.classList.add("is-open");
|
|
304
|
+
const close = viewer.querySelector("[data-lightbox-close]");
|
|
305
|
+
if (close) close.focus();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const close = () => {
|
|
309
|
+
viewer.hidden = true;
|
|
310
|
+
viewer.classList.remove("is-open");
|
|
311
|
+
if (opener) opener.focus();
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
triggers.forEach((trigger, index) => {
|
|
315
|
+
trigger.addEventListener("click", () => open(index, trigger));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
viewer.querySelector("[data-lightbox-close]")?.addEventListener("click", close);
|
|
319
|
+
viewer.querySelector("[data-lightbox-prev]")?.addEventListener("click", () => show(current - 1));
|
|
320
|
+
viewer.querySelector("[data-lightbox-next]")?.addEventListener("click", () => show(current + 1));
|
|
321
|
+
viewer.addEventListener("click", (e) => {
|
|
322
|
+
if (e.target === viewer) close();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
document.addEventListener("keydown", (e) => {
|
|
326
|
+
if (viewer.hidden) return;
|
|
327
|
+
if (e.key === "Escape") close();
|
|
328
|
+
else if (e.key === "ArrowLeft") show(current - 1);
|
|
329
|
+
else if (e.key === "ArrowRight") show(current + 1);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// 5. Scroll Reveal Effect using IntersectionObserver
|
|
334
|
+
const revealElements = document.querySelectorAll(
|
|
335
|
+
[
|
|
336
|
+
".reveal",
|
|
337
|
+
".sq-location__card",
|
|
338
|
+
".sq-services__card",
|
|
339
|
+
".sq-audience__card",
|
|
340
|
+
".sq-pricing__card",
|
|
341
|
+
".sq-contact__card",
|
|
342
|
+
".sq-card",
|
|
343
|
+
".sq-steps__item",
|
|
344
|
+
".sq-timeline__item",
|
|
345
|
+
".sq-projects__item",
|
|
346
|
+
".sq-skills__group",
|
|
347
|
+
".sq-testimonials__card",
|
|
348
|
+
".sq-gallery__item",
|
|
349
|
+
].join(", ")
|
|
350
|
+
);
|
|
351
|
+
if (reduceMotion) {
|
|
352
|
+
revealElements.forEach((el) => el.classList.add("is-visible"));
|
|
353
|
+
} else if ("IntersectionObserver" in window) {
|
|
354
|
+
const revealObserver = new IntersectionObserver(
|
|
355
|
+
(entries, observer) => {
|
|
356
|
+
entries.forEach((entry) => {
|
|
357
|
+
if (entry.isIntersecting) {
|
|
358
|
+
entry.target.classList.add("is-visible");
|
|
359
|
+
observer.unobserve(entry.target);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
threshold: 0.05,
|
|
365
|
+
rootMargin: "0px 0px -40px 0px",
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
revealElements.forEach((el) => {
|
|
370
|
+
el.classList.add("reveal");
|
|
371
|
+
revealObserver.observe(el);
|
|
372
|
+
});
|
|
373
|
+
} else {
|
|
374
|
+
revealElements.forEach((el) => {
|
|
375
|
+
el.classList.add("is-visible");
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 6. Nav scroll-spy — highlight in-page anchor links (services/pricing/contact) as their section scrolls into view
|
|
380
|
+
const navHashLinks = Array.from(document.querySelectorAll(".sq-nav__list a[href*='#']"));
|
|
381
|
+
const sectionLinkMap = navHashLinks
|
|
382
|
+
.map((link) => {
|
|
383
|
+
const id = link.getAttribute("href").split("#")[1];
|
|
384
|
+
const section = id ? document.getElementById(id) : null;
|
|
385
|
+
return section ? { link, section } : null;
|
|
386
|
+
})
|
|
387
|
+
.filter(Boolean);
|
|
388
|
+
|
|
389
|
+
if (sectionLinkMap.length && "IntersectionObserver" in window) {
|
|
390
|
+
const homeLink = document.querySelector(".sq-nav__list a[href='/']");
|
|
391
|
+
const intersecting = new Set();
|
|
392
|
+
|
|
393
|
+
const updateActiveLink = () => {
|
|
394
|
+
navHashLinks.forEach((link) => link.removeAttribute("aria-current"));
|
|
395
|
+
if (homeLink) homeLink.removeAttribute("aria-current");
|
|
396
|
+
const active = sectionLinkMap.find(({ section }) => intersecting.has(section));
|
|
397
|
+
if (active) {
|
|
398
|
+
active.link.setAttribute("aria-current", "page");
|
|
399
|
+
} else if (homeLink) {
|
|
400
|
+
homeLink.setAttribute("aria-current", "page");
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const spyObserver = new IntersectionObserver(
|
|
405
|
+
(entries) => {
|
|
406
|
+
entries.forEach((entry) => {
|
|
407
|
+
if (entry.isIntersecting) intersecting.add(entry.target);
|
|
408
|
+
else intersecting.delete(entry.target);
|
|
409
|
+
});
|
|
410
|
+
updateActiveLink();
|
|
411
|
+
},
|
|
412
|
+
{ rootMargin: "-45% 0px -50% 0px", threshold: 0 }
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
sectionLinkMap.forEach(({ section }) => spyObserver.observe(section));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 7. Back to top — restore focus to skip target
|
|
419
|
+
const scrollToTop = (targetId) => {
|
|
420
|
+
const top = document.getElementById(targetId) || document.body;
|
|
421
|
+
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
|
|
422
|
+
if (history.replaceState) {
|
|
423
|
+
history.replaceState(null, "", `#${targetId}`);
|
|
424
|
+
}
|
|
425
|
+
if (typeof top.focus === "function") {
|
|
426
|
+
if (!top.hasAttribute("tabindex")) top.setAttribute("tabindex", "-1");
|
|
427
|
+
top.focus({ preventScroll: true });
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
document.querySelectorAll("a.sq-to-top").forEach((link) => {
|
|
432
|
+
link.addEventListener("click", (e) => {
|
|
433
|
+
e.preventDefault();
|
|
434
|
+
scrollToTop("top");
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// 7b. Floating back-to-top control (<ScrollToTop />) — appears past its
|
|
439
|
+
// own threshold, so a short page never shows it.
|
|
440
|
+
document.querySelectorAll("[data-scroll-top]").forEach((button) => {
|
|
441
|
+
const targetId = button.getAttribute("data-scroll-target") || "top";
|
|
442
|
+
const threshold = Number(button.getAttribute("data-scroll-threshold")) || 300;
|
|
443
|
+
|
|
444
|
+
const sync = () => {
|
|
445
|
+
const visible = window.scrollY > threshold;
|
|
446
|
+
button.hidden = !visible;
|
|
447
|
+
button.classList.toggle("is-visible", visible);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
button.addEventListener("click", () => scrollToTop(targetId));
|
|
451
|
+
window.addEventListener("scroll", sync, { passive: true });
|
|
452
|
+
sync();
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// astro:page-load fires after the initial page load and after every
|
|
457
|
+
// client-side navigation done by <ClientRouter />, so re-run setup each time
|
|
458
|
+
// instead of relying on DOMContentLoaded (which only fires once).
|
|
459
|
+
document.addEventListener("astro:page-load", initEnhancements);
|