b8cf7cfbcb
Adds About, Contact, Services (index + 8) and Industries (index + 7), generated from structured JSON converted out of the .docx copy decks by scripts/convert-docs.py. Shared header with dropdowns, footer, contact form (mailto until a backend exists), FAQ/breadcrumb JSON-LD, sitemap and robots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
1.4 KiB
TypeScript
51 lines
1.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
|
|
const SELECTOR = "[data-reveal], [data-reveal-group]";
|
|
|
|
/**
|
|
* Marks `[data-reveal]` / `[data-reveal-group]` elements with `data-in` as they scroll into view.
|
|
* Render it as the last child of a page: its effect then runs only after the page's markup has
|
|
* hydrated, so touching those elements can't cause a hydration mismatch.
|
|
*/
|
|
export default function RevealObserver() {
|
|
useEffect(() => {
|
|
const io = new IntersectionObserver(
|
|
(entries) => {
|
|
for (const entry of entries) {
|
|
if (entry.isIntersecting) {
|
|
(entry.target as HTMLElement).dataset.in = "";
|
|
io.unobserve(entry.target);
|
|
}
|
|
}
|
|
},
|
|
{ rootMargin: "0px 0px -12% 0px", threshold: 0.12 },
|
|
);
|
|
const watch = (root: ParentNode) =>
|
|
root.querySelectorAll<HTMLElement>(SELECTOR).forEach((el) => {
|
|
if (!("in" in el.dataset)) io.observe(el);
|
|
});
|
|
|
|
watch(document);
|
|
const mo = new MutationObserver((records) => {
|
|
for (const r of records) {
|
|
r.addedNodes.forEach((n) => {
|
|
if (n instanceof HTMLElement) {
|
|
if (n.matches(SELECTOR) && !("in" in n.dataset)) io.observe(n);
|
|
watch(n);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
mo.observe(document.body, { childList: true, subtree: true });
|
|
|
|
return () => {
|
|
io.disconnect();
|
|
mo.disconnect();
|
|
};
|
|
}, []);
|
|
|
|
return null;
|
|
}
|