/* Site-wide search. One index built from SX_DATA + SX_STUDIES, a trigger injected into the header beside the CTA, and an overlay opened by click, "/" or cmd-K. */ const SRCH_D = window.SX_DATA; const SX_INDEX = (function () { const D = SRCH_D, S = window.SX_STUDIES || {}, out = []; const add = (kind, title, sub, href, kw) => out.push({ kind, title, sub, href, kw: (title + " " + (sub || "") + " " + (kw || "")).toLowerCase() }); D.solutions.forEach((s) => add("solution", s.name, s.summary, "/solutions#" + s.id, [s.group, s.practice, s.tag].join(" "))); D.projects.forEach((p) => { const study = !!S[p.slug] || p.slug === "lesco"; const href = study ? (p.slug === "lesco" ? "/lesco" : "/case?p=" + p.slug) : "/work#" + p.slug; add(study ? "case study" : "project", p.title, p.client + " · " + p.year, href, [p.short, p.sector, p.practice, p.country, p.summary, p.brief].filter(Boolean).join(" ")); }); D.practices.forEach((p) => add("practice", p.title, p.what || p.note, p.slug === "document-intelligence" ? "/document-intelligence" : "/practices#" + p.slug, [p.group, p.note, p.body].filter(Boolean).join(" "))); D.industries.forEach((i) => add("sector", i.name, i.summary, "/industries#" + i.id, i.tag)); D.stack.forEach((g) => g.items.forEach((t) => add("technology", t, g.title, "/technology#" + g.id, g.description))); D.faqs.forEach((q) => add("question", q.question, q.answer.slice(0, 110), "/faq", q.answer)); [ ["page", "Work", "Every documented programme, filterable", "/work"], ["page", "Solutions", "Systems you can order, with the runs behind them", "/solutions"], ["page", "Practices", "The ten things we do", "/practices"], ["page", "Technology stack", "Every technology, by layer", "/technology"], ["page", "Industries", "Sectors we have delivered into", "/industries"], ["page", "About", "Who you work with, and how we engage", "/about"], ["page", "Book a call", "Tell us what is not working", "/about#start"], ].forEach((p) => add(p[0], p[1], p[2], p[3])); return out; })(); const SX_SUGGEST = ["Utility CIS & billing", "MetersBoard", "Document intelligence", "AI agents", "Work"]; function sxSearch(q) { const query = q.trim().toLowerCase(); if (!query) return SX_INDEX.filter((e) => SX_SUGGEST.includes(e.title)).slice(0, 6); const toks = query.split(/\s+/); return SX_INDEX.map((e) => { const t = e.title.toLowerCase(); if (!toks.every((tk) => e.kw.includes(tk))) return null; let s = 10; if (t.startsWith(query)) s = 100; else if (t.includes(query)) s = 70; else if (toks.every((tk) => t.includes(tk))) s = 50; else if (e.kw.includes(query)) s = 30; if (e.kind === "case study") s += 6; if (e.kind === "solution") s += 4; return { ...e, s }; }).filter(Boolean).sort((a, b) => b.s - a.s).slice(0, 12); } function SiteSearch({ mobile }) { const [host, setHost] = React.useState(null); const [open, setOpen] = React.useState(false); const [q, setQ] = React.useState(""); const [sel, setSel] = React.useState(0); const input = React.useRef(null); const results = React.useMemo(() => sxSearch(q), [q]); React.useEffect(() => { const cta = Array.from(document.querySelectorAll("header a")).find((a) => a.textContent.trim() === "Book a call"); if (!cta || !cta.parentNode) return; const el = document.createElement("div"); el.style.display = "flex"; cta.parentNode.insertBefore(el, cta.parentNode.firstChild); setHost(el); return () => el.remove(); }, [mobile]); React.useEffect(() => { const onKey = (e) => { if (!open && (e.key === "/" || ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k")) && !/^(input|textarea)$/i.test(document.activeElement?.tagName || "")) { e.preventDefault(); setOpen(true); } else if (open && e.key === "Escape") { setOpen(false); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open]); React.useEffect(() => { if (open) { setSel(0); setTimeout(() => input.current?.focus(), 20); } }, [open]); React.useEffect(() => { setSel(0); }, [q]); const onFieldKey = (e) => { if (e.key === "ArrowDown") { e.preventDefault(); setSel((i) => Math.min(i + 1, results.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSel((i) => Math.max(i - 1, 0)); } else if (e.key === "Enter" && results[sel]) { e.preventDefault(); location.href = results[sel].href; } }; const trigger = ( ); return ( <> {host && ReactDOM.createPortal(trigger, host)} {open && (
{ if (e.target === e.currentTarget) setOpen(false); }}>
setQ(e.target.value)} onKeyDown={onFieldKey} placeholder="Search solutions, projects, practices, technologies" aria-label="Search" />
{results.length > 0 ? ( ) : (

Nothing matched “{q}”.

Try billing, ocr, agent, thingsboard, or a country.

)}
{q.trim() ? results.length + " result" + (results.length === 1 ? "" : "s") : "suggested"} ↑↓ move · ⏎ open · esc close
)} ); } function SearchGlyph() { return ( ); } Object.assign(window, { SiteSearch, SX_INDEX, sxSearch, SearchGlyph });