---
import { Icon } from "@astrojs/starlight/components";
import project from "@astrojs/starlight";
const pagefindTranslations = {
placeholder: Astro.locals.t("search.label"),
...Object.fromEntries(
Object.entries(Astro.locals.t.all())
.filter(([key]) => key.startsWith("pagefind."))
.map(([key, value]) => [key.replace("pagefind.", ""), value])
),
};
const dataAttributes: DOMStringMap = {
"data-translations": JSON.stringify(pagefindTranslations),
};
// @ts-ignore - Accessing trailingSlash from Starlight integration
if ((project as any)?.trailingSlash === "never")
dataAttributes["data-strip-trailing-slash"] = "";
---
<site-search class={Astro.props.class} {...dataAttributes}>
<button
data-open-modal
disabled
aria-label={Astro.locals.t("search.label")}
aria-keyshortcuts="Control+K"
>
<Icon name="magnifier" size="1.3rem" />
<span class="sl-block" aria-hidden="true"
>{Astro.locals.t("search.label")}</span
>
<kbd class="sl-flex">
<kbd>{Astro.locals.t("search.ctrlKey")}</kbd><kbd>K</kbd>
</kbd>
</button>
<dialog style="padding:0" aria-label={Astro.locals.t("search.label")}>
<div class="dialog-frame sl-flex">
{
/* TODO: Make the layout of this button flexible to accommodate different word lengths. Currently hard-coded for English: “Cancel” */
}
<button data-close-modal class="sl-flex md:sl-hidden">
{Astro.locals.t("search.cancelLabel")}
</button>
{
import.meta.env.DEV ? (
<div
style="margin: auto; text-align: center; white-space: pre-line;"
dir="ltr"
>
<p>{Astro.locals.t("search.devWarning")}</p>
</div>
) : (
<div class="search-container">
<div id="starlight__search" />
</div>
)
}
</div>
</dialog>
</site-search>
{
/**
* This is intentionally inlined to avoid briefly showing an invalid shortcut.
* Purposely using the deprecated `navigator.platform` property to detect Apple devices, as the
* user agent is spoofed by some browsers when opening the devtools.
*/
}
<script is:inline>
(() => {
const openBtn = document.querySelector("button[data-open-modal]");
const shortcut = openBtn?.querySelector("kbd");
if (!openBtn || !(shortcut instanceof HTMLElement)) return;
const platformKey = shortcut.querySelector("kbd");
if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
platformKey.textContent = "⌘";
openBtn.setAttribute("aria-keyshortcuts", "Meta+K");
}
shortcut.style.display = "";
})();
</script>
<script>
import { pagefindUserConfig } from "virtual:starlight/pagefind-config";
class SiteSearch extends HTMLElement {
constructor() {
super();
const openBtn = this.querySelector<HTMLButtonElement>(
"button[data-open-modal]"
)!;
const closeBtn = this.querySelector<HTMLButtonElement>(
"button[data-close-modal]"
)!;
const dialog = this.querySelector("dialog")!;
const dialogFrame = this.querySelector(".dialog-frame")!;
const onClick = (event: MouseEvent) => {
const isLink = "href" in (event.target || {});
if (
isLink ||
(document.body.contains(event.target as Node) &&
!dialogFrame.contains(event.target as Node))
) {
closeModal();
}
};
const openModal = (event?: MouseEvent) => {
dialog.showModal();
document.body.toggleAttribute("data-search-modal-open", true);
this.querySelector("input")?.focus();
event?.stopPropagation();
window.addEventListener("click", onClick);
};
const closeModal = () => dialog.close();
openBtn.addEventListener("click", openModal);
openBtn.disabled = false;
closeBtn.addEventListener("click", closeModal);
dialog.addEventListener("close", () => {
document.body.toggleAttribute("data-search-modal-open", false);
window.removeEventListener("click", onClick);
});
window.addEventListener("keydown", (e) => {
if ((e.metaKey === true || e.ctrlKey === true) && e.key === "k") {
dialog.open ? closeModal() : openModal();
e.preventDefault();
}
});
let translations = {};
try {
translations = JSON.parse(this.dataset.translations || "{}");
} catch {}
const shouldStrip = this.dataset.stripTrailingSlash !== undefined;
const stripTrailingSlash = (path: string) =>
path.replace(/(.)\/(#.*)?$/, "$1$2");
const formatURL = shouldStrip
? stripTrailingSlash
: (path: string) => path;
window.addEventListener("DOMContentLoaded", () => {
if (import.meta.env.DEV) return;
const onIdle =
window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
onIdle(async () => {
const { PagefindUI } = await import("@pagefind/default-ui");
new PagefindUI({
...pagefindUserConfig,
element: "#starlight__search",
baseUrl: import.meta.env.BASE_URL,
bundlePath:
import.meta.env.BASE_URL.replace(/\/$/, "") + "/pagefind/",
showImages: false,
translations,
showSubResults: true,
processResult: (result: {
url: string;
sub_results: Array<{ url: string }>;
}) => {
result.url = formatURL(result.url);
result.sub_results = result.sub_results.map((sub_result) => {
sub_result.url = formatURL(sub_result.url);
return sub_result;
});
},
});
});
});
}
}
customElements.define("site-search", SiteSearch);
</script>
<style>
@layer starlight.core {
site-search {
display: contents;
}
button[data-open-modal] {
display: flex;
align-items: center;
gap: 0.5rem;
background-color: color-mix(
in srgb,
var(--sl-color-white) 7%,
transparent
);
border: 1px solid
color-mix(in srgb, var(--sl-color-white) 10%, transparent);
border-radius: 0.5rem;
padding-inline-start: 1rem;
padding-inline-end: 0.8rem;
cursor: pointer;
height: 3rem;
font-size: var(--sl-text-sm);
width: 100%;
}
button[data-open-modal] kbd {
margin-inline-start: auto;
}
@media (min-width: 50rem) {
button[data-open-modal] {
color: var(--sl-color-gray-2);
font-size: var(--sl-text-sm);
width: 100%;
max-width: 100%;
}
button[data-open-modal]:hover {
border-color: var(--sl-color-gray-2);
color: var(--sl-color-white);
}
button[data-open-modal] > :last-child {
margin-inline-start: auto;
}
}
button > kbd {
border-radius: 0.25rem;
font-size: var(--sl-text-sm);
padding-inline: 0.375rem;
background-color: var(--sl-color-gray-6);
}
kbd {
font-family: var(--__sl-font);
}
dialog {
margin: 0;
background-color: var(--sl-color-gray-6);
border: 1px solid var(--sl-color-gray-5);
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
box-shadow: var(--sl-shadow-lg);
}
dialog[open] {
display: flex;
}
dialog::backdrop {
background-color: var(--sl-color-backdrop-overlay);
-webkit-backdrop-filter: blur(0.25rem);
backdrop-filter: blur(0.25rem);
}
.dialog-frame {
position: relative;
overflow: auto;
flex-direction: column;
flex-grow: 1;
gap: 1rem;
padding: 1rem;
}
button[data-close-modal] {
position: absolute;
z-index: 1;
align-items: center;
align-self: flex-end;
height: calc(64px * var(--pagefind-ui-scale));
padding: 0.25rem;
border: 0;
background: transparent;
cursor: pointer;
color: var(--sl-color-text-accent);
}
#starlight__search {
--pagefind-ui-primary: var(--sl-color-text);
--pagefind-ui-text: var(--sl-color-gray-2);
--pagefind-ui-font: var(--__sl-font);
--pagefind-ui-background: var(--sl-color-black);
--pagefind-ui-border: var(--sl-color-gray-5);
--pagefind-ui-border-width: 1px;
--pagefind-ui-tag: var(--sl-color-gray-5);
--sl-search-cancel-space: 5rem;
}
:root[data-theme="light"] #starlight__search {
--pagefind-ui-tag: var(--sl-color-gray-6);
}
@media (min-width: 50rem) {
#starlight__search {
--sl-search-cancel-space: 0px;
}
dialog {
margin: 4rem auto auto;
border-radius: 0.5rem;
width: 90%;
max-width: 40rem;
height: max-content;
min-height: 15rem;
max-height: calc(100% - 8rem);
}
.dialog-frame {
padding: 1.5rem;
}
}
}
</style>
<style is:global>
@import url("@pagefind/default-ui/css/ui.css") layer(starlight.core);
@layer starlight.core {
[data-search-modal-open] {
overflow: hidden;
}
#starlight__search {
--sl-search-result-spacing: calc(1.25rem * var(--pagefind-ui-scale));
--sl-search-result-pad-inline-start: calc(
3.75rem * var(--pagefind-ui-scale)
);
--sl-search-result-pad-inline-end: calc(
1.25rem * var(--pagefind-ui-scale)
);
--sl-search-result-pad-block: calc(0.9375rem * var(--pagefind-ui-scale));
--sl-search-result-nested-pad-block: calc(
0.625rem * var(--pagefind-ui-scale)
);
--sl-search-corners: calc(0.3125rem * var(--pagefind-ui-scale));
--sl-search-page-icon-size: calc(1.875rem * var(--pagefind-ui-scale));
--sl-search-page-icon-inline-start: calc(
(
var(--sl-search-result-pad-inline-start) -
var(--sl-search-page-icon-size)
) / 2
);
--sl-search-tree-diagram-size: calc(2.5rem * var(--pagefind-ui-scale));
--sl-search-tree-diagram-inline-start: calc(
(
var(--sl-search-result-pad-inline-start) -
var(--sl-search-tree-diagram-size)
) / 2
);
}
#starlight__search .pagefind-ui__form::before {
--pagefind-ui-text: var(--sl-color-gray-1);
opacity: 1;
}
#starlight__search .pagefind-ui__search-input {
color: var(--sl-color-white);
font-weight: 400;
width: calc(100% - var(--sl-search-cancel-space));
}
#starlight__search input:focus {
--pagefind-ui-border: var(--sl-color-accent);
}
#starlight__search .pagefind-ui__search-clear {
inset-inline-end: var(--sl-search-cancel-space);
width: calc(60px * var(--pagefind-ui-scale));
padding: 0;
background-color: transparent;
overflow: hidden;
}
#starlight__search .pagefind-ui__search-clear:focus {
outline: 1px solid var(--sl-color-accent);
}
#starlight__search .pagefind-ui__search-clear::before {
content: "";
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E")
center / 50% no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E")
center / 50% no-repeat;
background-color: var(--sl-color-text-accent);
display: block;
width: 100%;
height: 100%;
}
#starlight__search .pagefind-ui__results > * + * {
margin-top: var(--sl-search-result-spacing);
}
#starlight__search .pagefind-ui__result {
border: 0;
padding: 0;
}
#starlight__search .pagefind-ui__result-nested {
position: relative;
padding: var(--sl-search-result-nested-pad-block)
var(--sl-search-result-pad-inline-end);
padding-inline-start: var(--sl-search-result-pad-inline-start);
}
#starlight__search
.pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)),
#starlight__search .pagefind-ui__result-nested {
position: relative;
background-color: var(--sl-color-black);
}
#starlight__search
.pagefind-ui__result-title:not(
:where(.pagefind-ui__result-nested *)
):hover,
#starlight__search
.pagefind-ui__result-title:not(
:where(.pagefind-ui__result-nested *)
):focus-within,
#starlight__search .pagefind-ui__result-nested:hover,
#starlight__search .pagefind-ui__result-nested:focus-within {
outline: 1px solid var(--sl-color-accent-high);
}
#starlight__search
.pagefind-ui__result-title:not(
:where(.pagefind-ui__result-nested *)
):focus-within,
#starlight__search .pagefind-ui__result-nested:focus-within {
background-color: var(--sl-color-accent-low);
}
#starlight__search .pagefind-ui__result-thumb,
#starlight__search .pagefind-ui__result-inner {
margin-top: 0;
}
#starlight__search .pagefind-ui__result-inner > :first-child {
border-radius: var(--sl-search-corners) var(--sl-search-corners) 0 0;
}
#starlight__search .pagefind-ui__result-inner > :last-child {
border-radius: 0 0 var(--sl-search-corners) var(--sl-search-corners);
}
#starlight__search .pagefind-ui__result-inner > .pagefind-ui__result-title {
padding: var(--sl-search-result-pad-block)
var(--sl-search-result-pad-inline-end);
padding-inline-start: var(--sl-search-result-pad-inline-start);
}
#starlight__search
.pagefind-ui__result-inner
> .pagefind-ui__result-title::before {
content: "";
position: absolute;
inset-block: 0;
inset-inline-start: var(--sl-search-page-icon-inline-start);
width: var(--sl-search-page-icon-size);
background: var(--sl-color-gray-3);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E")
center no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 24 24'%3E%3Cpath d='M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3V8l-6-6a1 1 0 0 0-1 0H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V9Zm-6-4 3 3h-2a1 1 0 0 1-1-1V5Zm4 14a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z'/%3E%3C/svg%3E")
center no-repeat;
}
#starlight__search .pagefind-ui__result-inner {
align-items: stretch;
gap: 1px;
}
#starlight__search .pagefind-ui__result-link {
position: unset;
--pagefind-ui-text: var(--sl-color-white);
font-weight: 600;
}
#starlight__search .pagefind-ui__result-link:hover {
text-decoration: none;
}
#starlight__search
.pagefind-ui__result-nested
.pagefind-ui__result-link::before {
content: unset;
}
#starlight__search .pagefind-ui__result-nested::before {
content: "";
position: absolute;
inset-block: 0;
inset-inline-start: var(--sl-search-tree-diagram-inline-start);
width: var(--sl-search-tree-diagram-size);
background: var(--sl-color-gray-4);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E")
0% 0% / 100% no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' viewBox='0 0 16 1000' preserveAspectRatio='xMinYMin slice'%3E%3Cpath d='M8 0v1000m6-988H8'/%3E%3C/svg%3E")
0% 0% / 100% no-repeat;
}
#starlight__search .pagefind-ui__result-nested:last-of-type::before {
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' viewBox='0 0 16 16'%3E%3Cpath d='M8 0v12m6 0H8'/%3E%3C/svg%3E");
}
[dir="rtl"] .pagefind-ui__result-title::before,
[dir="rtl"] .pagefind-ui__result-nested::before {
transform: matrix(-1, 0, 0, 1, 0, 0);
}
#starlight__search .pagefind-ui__result-link::after {
content: "";
position: absolute;
inset: 0;
}
#starlight__search .pagefind-ui__result-excerpt {
font-size: calc(1rem * var(--pagefind-ui-scale));
overflow-wrap: anywhere;
}
#starlight__search mark {
color: var(--sl-color-gray-2);
background-color: transparent;
font-weight: 600;
}
#starlight__search .pagefind-ui__filter-value::before {
border-color: var(--sl-color-text-invert);
}
#starlight__search .pagefind-ui__result-tags {
background-color: var(--sl-color-black);
margin-top: 0;
padding: var(--sl-search-result-nested-pad-block)
var(--sl-search-result-pad-inline-end);
}
}
</style>