stropress 0.0.1
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/dist/index.d.ts +2 -0
- package/dist/index.js +307 -0
- package/dist/theme-default/package.json +20 -0
- package/dist/theme-default/postcss.config.mjs +5 -0
- package/dist/theme-default/src/components/BaseHead.astro +53 -0
- package/dist/theme-default/src/components/DocToc.astro +111 -0
- package/dist/theme-default/src/components/GithubIcon.astro +35 -0
- package/dist/theme-default/src/components/HomePage.astro +198 -0
- package/dist/theme-default/src/components/Icon.astro +78 -0
- package/dist/theme-default/src/components/LocaleSelect.astro +187 -0
- package/dist/theme-default/src/components/NavBar.astro +231 -0
- package/dist/theme-default/src/components/SearchInput.astro +84 -0
- package/dist/theme-default/src/components/SearchModal.astro +209 -0
- package/dist/theme-default/src/components/Sidebar.astro +101 -0
- package/dist/theme-default/src/content/docs/guide/configuration.mdx +195 -0
- package/dist/theme-default/src/content/docs/guide/getting-started.md +98 -0
- package/dist/theme-default/src/content/docs/guide/search.mdx +41 -0
- package/dist/theme-default/src/content/docs/index.css +0 -0
- package/dist/theme-default/src/content/docs/index.md +6 -0
- package/dist/theme-default/src/content/docs/zh/guide/configuration.mdx +149 -0
- package/dist/theme-default/src/content/docs/zh/guide/getting-started.md +31 -0
- package/dist/theme-default/src/content/docs/zh/guide/search.mdx +23 -0
- package/dist/theme-default/src/content/docs/zh/index.astro +75 -0
- package/dist/theme-default/src/content/docs/zh/index.md +6 -0
- package/dist/theme-default/src/content.config.ts +14 -0
- package/dist/theme-default/src/env.d.ts +15 -0
- package/dist/theme-default/src/layouts/DocsLayout.astro +278 -0
- package/dist/theme-default/src/lib/config.ts +195 -0
- package/dist/theme-default/src/lib/custom-home.ts +40 -0
- package/dist/theme-default/src/lib/custom-style.ts +11 -0
- package/dist/theme-default/src/lib/og.ts +275 -0
- package/dist/theme-default/src/pages/[...slug]/index.astro +83 -0
- package/dist/theme-default/src/pages/index.astro +47 -0
- package/dist/theme-default/src/pages/og/[...slug].png.ts +70 -0
- package/dist/theme-default/src/scripts/code-copy.ts +54 -0
- package/dist/theme-default/src/scripts/doc-toc.ts +109 -0
- package/dist/theme-default/src/scripts/search.ts +329 -0
- package/dist/theme-default/src/styles/global.css +70 -0
- package/dist/theme-default/src/styles/markdown.css +141 -0
- package/dist/theme-default/tsconfig.json +10 -0
- package/package.json +32 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { getCollection, type CollectionEntry } from "astro:content";
|
|
3
|
+
import { getResolvedSiteConfig } from "../../lib/config";
|
|
4
|
+
import { renderOgPng } from "../../lib/og";
|
|
5
|
+
|
|
6
|
+
interface OgRouteProps {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
siteTitle: string;
|
|
10
|
+
routePath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const prerender = true;
|
|
14
|
+
|
|
15
|
+
export const getStaticPaths = async () => {
|
|
16
|
+
const docs = await getCollection("docs");
|
|
17
|
+
|
|
18
|
+
return docs.map((entry) => {
|
|
19
|
+
const routePath = getRoutePathFromEntry(entry);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
params: {
|
|
23
|
+
slug: routePathToOgSlug(routePath),
|
|
24
|
+
},
|
|
25
|
+
props: {
|
|
26
|
+
title: entry.data.title,
|
|
27
|
+
description: entry.data.description || "",
|
|
28
|
+
siteTitle: getResolvedSiteConfig(routePath).siteTitle,
|
|
29
|
+
routePath,
|
|
30
|
+
} satisfies OgRouteProps,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const GET: APIRoute = async ({ props }) => {
|
|
36
|
+
const data = props as OgRouteProps;
|
|
37
|
+
const png = await renderOgPng({
|
|
38
|
+
title: data.title,
|
|
39
|
+
description: data.description,
|
|
40
|
+
siteTitle: data.siteTitle,
|
|
41
|
+
routePath: data.routePath,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return new Response(Buffer.from(png), {
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "image/png",
|
|
47
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getRoutePathFromEntry = (entry: CollectionEntry<"docs">) => {
|
|
53
|
+
if (entry.id === "index") {
|
|
54
|
+
return "/";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (entry.id.endsWith("/index")) {
|
|
58
|
+
return `/${entry.id.slice(0, -"/index".length)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `/${entry.id}`;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const routePathToOgSlug = (routePath: string) => {
|
|
65
|
+
if (routePath === "/") {
|
|
66
|
+
return "index";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return routePath.slice(1);
|
|
70
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import copy from "copy-to-clipboard";
|
|
2
|
+
|
|
3
|
+
const COPY_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
|
4
|
+
const COPIED_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 15 2 2 4-4"/><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
|
|
5
|
+
|
|
6
|
+
const attachCopyButtons = () => {
|
|
7
|
+
const blocks = document.querySelectorAll(".markdown-content pre > code");
|
|
8
|
+
|
|
9
|
+
for (const code of blocks) {
|
|
10
|
+
const pre = code.parentElement;
|
|
11
|
+
if (!(pre instanceof HTMLElement) || pre.dataset.copyReady === "true") {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pre.dataset.copyReady = "true";
|
|
16
|
+
pre.classList.add("code-block");
|
|
17
|
+
|
|
18
|
+
const button = document.createElement("button");
|
|
19
|
+
button.type = "button";
|
|
20
|
+
button.className = "code-copy-button";
|
|
21
|
+
button.innerHTML = COPY_SVG;
|
|
22
|
+
button.setAttribute("aria-label", "Copy code to clipboard");
|
|
23
|
+
|
|
24
|
+
button.addEventListener("click", () => {
|
|
25
|
+
const source = code.textContent ?? "";
|
|
26
|
+
if (!source) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
copy(source);
|
|
31
|
+
button.innerHTML = COPIED_SVG;
|
|
32
|
+
button.classList.add("is-copied");
|
|
33
|
+
|
|
34
|
+
window.setTimeout(() => {
|
|
35
|
+
button.innerHTML = COPY_SVG;
|
|
36
|
+
button.classList.remove("is-copied");
|
|
37
|
+
}, 1200);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
pre.appendChild(button);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const setupCodeCopyButtons = () => {
|
|
45
|
+
if (document.readyState === "loading") {
|
|
46
|
+
document.addEventListener("DOMContentLoaded", attachCopyButtons, {
|
|
47
|
+
once: true,
|
|
48
|
+
});
|
|
49
|
+
} else {
|
|
50
|
+
attachCopyButtons();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
document.addEventListener("astro:page-load", attachCopyButtons);
|
|
54
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
type TocItem = HTMLElement & { dataset: DOMStringMap };
|
|
2
|
+
|
|
3
|
+
let initialized = false;
|
|
4
|
+
let cleanup: (() => void) | null = null;
|
|
5
|
+
|
|
6
|
+
const activateFromViewport = (items: TocItem[], headings: HTMLElement[]) => {
|
|
7
|
+
if (headings.length === 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const offset = 140;
|
|
12
|
+
let currentId = headings[0].id;
|
|
13
|
+
|
|
14
|
+
for (const heading of headings) {
|
|
15
|
+
if (heading.getBoundingClientRect().top - offset <= 0) {
|
|
16
|
+
currentId = heading.id;
|
|
17
|
+
} else {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
const isActive = item.dataset.targetId === currentId;
|
|
24
|
+
item.dataset.state = isActive ? "active" : "inactive";
|
|
25
|
+
item
|
|
26
|
+
.querySelector<HTMLAnchorElement>(".doc-toc-link")
|
|
27
|
+
?.setAttribute("aria-current", isActive ? "location" : "false");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const mountToc = () => {
|
|
32
|
+
cleanup?.();
|
|
33
|
+
|
|
34
|
+
const items = Array.from(document.querySelectorAll<TocItem>(".doc-toc-item"));
|
|
35
|
+
if (items.length === 0) {
|
|
36
|
+
cleanup = null;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const headings = items
|
|
41
|
+
.map((item) => item.dataset.targetId)
|
|
42
|
+
.filter((id): id is string => Boolean(id))
|
|
43
|
+
.map((id) => document.getElementById(id))
|
|
44
|
+
.filter(
|
|
45
|
+
(heading): heading is HTMLElement => heading instanceof HTMLElement,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (headings.length === 0) {
|
|
49
|
+
cleanup = null;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let rafId = 0;
|
|
54
|
+
const onScroll = () => {
|
|
55
|
+
if (rafId) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
rafId = window.requestAnimationFrame(() => {
|
|
60
|
+
rafId = 0;
|
|
61
|
+
activateFromViewport(items, headings);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onHashChange = () => {
|
|
66
|
+
const hash = window.location.hash;
|
|
67
|
+
if (!hash) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const id = decodeURIComponent(hash.slice(1));
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
const isActive = item.dataset.targetId === id;
|
|
74
|
+
item.dataset.state = isActive ? "active" : "inactive";
|
|
75
|
+
item
|
|
76
|
+
.querySelector<HTMLAnchorElement>(".doc-toc-link")
|
|
77
|
+
?.setAttribute("aria-current", isActive ? "location" : "false");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
82
|
+
window.addEventListener("resize", onScroll);
|
|
83
|
+
window.addEventListener("hashchange", onHashChange);
|
|
84
|
+
|
|
85
|
+
onHashChange();
|
|
86
|
+
activateFromViewport(items, headings);
|
|
87
|
+
|
|
88
|
+
cleanup = () => {
|
|
89
|
+
if (rafId) {
|
|
90
|
+
window.cancelAnimationFrame(rafId);
|
|
91
|
+
rafId = 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
window.removeEventListener("scroll", onScroll);
|
|
95
|
+
window.removeEventListener("resize", onScroll);
|
|
96
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const setupDocToc = () => {
|
|
101
|
+
mountToc();
|
|
102
|
+
|
|
103
|
+
if (initialized) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
initialized = true;
|
|
108
|
+
document.addEventListener("astro:page-load", mountToc);
|
|
109
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import MiniSearch from "minisearch";
|
|
2
|
+
|
|
3
|
+
export interface SearchDocument {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
body: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type ResultDocument = SearchDocument & {
|
|
12
|
+
score: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let initialized = false;
|
|
16
|
+
let cleanup: (() => void) | null = null;
|
|
17
|
+
|
|
18
|
+
const normalizeText = (value: string) =>
|
|
19
|
+
value
|
|
20
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
21
|
+
.replace(/`[^`]+`/g, " ")
|
|
22
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
|
23
|
+
.replace(/\[[^\]]+\]\([^)]*\)/g, "$1")
|
|
24
|
+
.replace(/[#>*_~\-]/g, " ")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
|
|
28
|
+
const sliceSummary = (value: string, maxLength = 120) => {
|
|
29
|
+
if (value.length <= maxLength) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return `${value.slice(0, maxLength - 1).trimEnd()}...`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const renderResultItem = (doc: ResultDocument) => {
|
|
37
|
+
const item = document.createElement("li");
|
|
38
|
+
item.className = "site-search-result-item";
|
|
39
|
+
|
|
40
|
+
const link = document.createElement("a");
|
|
41
|
+
link.className = "site-search-result-link";
|
|
42
|
+
link.href = doc.url;
|
|
43
|
+
|
|
44
|
+
const title = document.createElement("span");
|
|
45
|
+
title.className = "site-search-result-title";
|
|
46
|
+
title.textContent = doc.title;
|
|
47
|
+
|
|
48
|
+
const description = document.createElement("span");
|
|
49
|
+
description.className = "site-search-result-desc";
|
|
50
|
+
description.textContent = doc.description || sliceSummary(doc.body);
|
|
51
|
+
|
|
52
|
+
link.append(title, description);
|
|
53
|
+
item.append(link);
|
|
54
|
+
return item;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const toSafeResult = (
|
|
58
|
+
source: Partial<SearchDocument> & { score?: number },
|
|
59
|
+
fallbackId: string,
|
|
60
|
+
): ResultDocument => ({
|
|
61
|
+
id: source.id || fallbackId,
|
|
62
|
+
title: source.title || "Untitled",
|
|
63
|
+
description: source.description || "",
|
|
64
|
+
body: source.body || "",
|
|
65
|
+
url: source.url || "#",
|
|
66
|
+
score: source.score || 0,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const buildSearch = (documents: SearchDocument[]) => {
|
|
70
|
+
const docs = documents.map((doc) => ({
|
|
71
|
+
...doc,
|
|
72
|
+
title: normalizeText(doc.title),
|
|
73
|
+
description: normalizeText(doc.description),
|
|
74
|
+
body: normalizeText(doc.body),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const miniSearch = new MiniSearch<SearchDocument>({
|
|
78
|
+
fields: ["title", "description", "body"],
|
|
79
|
+
storeFields: ["id", "title", "description", "body", "url"],
|
|
80
|
+
searchOptions: {
|
|
81
|
+
prefix: true,
|
|
82
|
+
fuzzy: 0.2,
|
|
83
|
+
boost: {
|
|
84
|
+
title: 3,
|
|
85
|
+
description: 2,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
miniSearch.addAll(docs);
|
|
91
|
+
return miniSearch;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const searchLocal = (
|
|
95
|
+
index: MiniSearch<SearchDocument>,
|
|
96
|
+
sourceDocuments: SearchDocument[],
|
|
97
|
+
query: string,
|
|
98
|
+
): ResultDocument[] => {
|
|
99
|
+
const docById = new Map(sourceDocuments.map((doc) => [doc.id, doc]));
|
|
100
|
+
|
|
101
|
+
return index
|
|
102
|
+
.search(query, {
|
|
103
|
+
combineWith: "AND",
|
|
104
|
+
prefix: true,
|
|
105
|
+
fuzzy: 0.2,
|
|
106
|
+
})
|
|
107
|
+
.map((hit, i) => {
|
|
108
|
+
const id = String(hit.id);
|
|
109
|
+
const source = docById.get(id);
|
|
110
|
+
|
|
111
|
+
return toSafeResult(
|
|
112
|
+
{
|
|
113
|
+
id,
|
|
114
|
+
title: source?.title,
|
|
115
|
+
description: source?.description,
|
|
116
|
+
body: source?.body,
|
|
117
|
+
url: source?.url,
|
|
118
|
+
score: hit.score,
|
|
119
|
+
},
|
|
120
|
+
`local-${i}`,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const mountSearch = (documents: SearchDocument[]) => {
|
|
126
|
+
cleanup?.();
|
|
127
|
+
|
|
128
|
+
const modal = document.querySelector<HTMLElement>("[data-search-modal]");
|
|
129
|
+
const triggerButton = document.querySelector<HTMLButtonElement>(
|
|
130
|
+
"[data-search-trigger]",
|
|
131
|
+
);
|
|
132
|
+
const openButtons = Array.from(
|
|
133
|
+
document.querySelectorAll<HTMLElement>("[data-search-open]"),
|
|
134
|
+
);
|
|
135
|
+
const closeButtons = Array.from(
|
|
136
|
+
document.querySelectorAll<HTMLElement>("[data-search-close]"),
|
|
137
|
+
);
|
|
138
|
+
const input = document.querySelector<HTMLInputElement>("[data-search-input]");
|
|
139
|
+
const results = document.querySelector<HTMLUListElement>(
|
|
140
|
+
"[data-search-results]",
|
|
141
|
+
);
|
|
142
|
+
const empty = document.querySelector<HTMLElement>("[data-search-empty]");
|
|
143
|
+
|
|
144
|
+
if (!modal || !triggerButton || !input || !results || !empty) {
|
|
145
|
+
cleanup = null;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const index = buildSearch(documents);
|
|
150
|
+
let selectedIndex = -1;
|
|
151
|
+
let searchToken = 0;
|
|
152
|
+
|
|
153
|
+
const setModalOpen = (open: boolean) => {
|
|
154
|
+
modal.dataset.state = open ? "open" : "closed";
|
|
155
|
+
modal.setAttribute("aria-hidden", open ? "false" : "true");
|
|
156
|
+
document.body.style.overflow = open ? "hidden" : "";
|
|
157
|
+
|
|
158
|
+
if (open) {
|
|
159
|
+
window.setTimeout(() => {
|
|
160
|
+
input.focus();
|
|
161
|
+
input.select();
|
|
162
|
+
}, 0);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
input.value = "";
|
|
167
|
+
selectedIndex = -1;
|
|
168
|
+
results.innerHTML = "";
|
|
169
|
+
empty.hidden = true;
|
|
170
|
+
triggerButton.blur();
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const paintResults = async (query: string) => {
|
|
174
|
+
results.innerHTML = "";
|
|
175
|
+
selectedIndex = -1;
|
|
176
|
+
const token = ++searchToken;
|
|
177
|
+
|
|
178
|
+
const normalizedQuery = query.trim();
|
|
179
|
+
if (!normalizedQuery) {
|
|
180
|
+
empty.hidden = false;
|
|
181
|
+
empty.textContent = "Start typing to search docs.";
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let matches: ResultDocument[] = [];
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
matches = searchLocal(index, documents, normalizedQuery);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error("[stropress] Search query failed:", error);
|
|
191
|
+
matches = searchLocal(index, documents, normalizedQuery);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (token !== searchToken) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (matches.length === 0) {
|
|
199
|
+
empty.hidden = false;
|
|
200
|
+
empty.textContent = "No results found.";
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
empty.hidden = true;
|
|
205
|
+
matches.slice(0, 8).forEach((match) => {
|
|
206
|
+
results.append(renderResultItem(match));
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const moveHighlight = (direction: 1 | -1) => {
|
|
211
|
+
const items = Array.from(
|
|
212
|
+
results.querySelectorAll<HTMLLIElement>(".site-search-result-item"),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if (items.length === 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
selectedIndex += direction;
|
|
220
|
+
if (selectedIndex < 0) {
|
|
221
|
+
selectedIndex = items.length - 1;
|
|
222
|
+
}
|
|
223
|
+
if (selectedIndex >= items.length) {
|
|
224
|
+
selectedIndex = 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
items.forEach((item, indexItem) => {
|
|
228
|
+
item.dataset.state = indexItem === selectedIndex ? "active" : "inactive";
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
items[selectedIndex]?.scrollIntoView({ block: "nearest" });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const openSelectedResult = () => {
|
|
235
|
+
const items = Array.from(
|
|
236
|
+
results.querySelectorAll<HTMLLIElement>(".site-search-result-item"),
|
|
237
|
+
);
|
|
238
|
+
const activeItem = items[selectedIndex];
|
|
239
|
+
activeItem?.querySelector<HTMLAnchorElement>("a")?.click();
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const onTriggerClick = () => setModalOpen(true);
|
|
243
|
+
const onCloseClick = () => setModalOpen(false);
|
|
244
|
+
const onInput = () => {
|
|
245
|
+
paintResults(input.value);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const onInputKeydown = (event: KeyboardEvent) => {
|
|
249
|
+
if (event.key === "ArrowDown") {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
moveHighlight(1);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (event.key === "ArrowUp") {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
moveHighlight(-1);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (event.key === "Enter") {
|
|
262
|
+
const hasSelection = selectedIndex >= 0;
|
|
263
|
+
if (hasSelection) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
openSelectedResult();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const onGlobalKeydown = (event: KeyboardEvent) => {
|
|
271
|
+
const commandPressed = event.metaKey || event.ctrlKey;
|
|
272
|
+
|
|
273
|
+
if (commandPressed && event.key.toLowerCase() === "k") {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
setModalOpen(true);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (event.key === "Escape") {
|
|
280
|
+
if (modal.dataset.state === "open") {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
setModalOpen(false);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
triggerButton.addEventListener("click", onTriggerClick);
|
|
288
|
+
input.addEventListener("input", onInput);
|
|
289
|
+
input.addEventListener("keydown", onInputKeydown);
|
|
290
|
+
document.addEventListener("keydown", onGlobalKeydown);
|
|
291
|
+
|
|
292
|
+
openButtons.forEach((button) => {
|
|
293
|
+
button.addEventListener("click", onTriggerClick);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
closeButtons.forEach((button) => {
|
|
297
|
+
button.addEventListener("click", onCloseClick);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
cleanup = () => {
|
|
301
|
+
triggerButton.removeEventListener("click", onTriggerClick);
|
|
302
|
+
input.removeEventListener("input", onInput);
|
|
303
|
+
input.removeEventListener("keydown", onInputKeydown);
|
|
304
|
+
document.removeEventListener("keydown", onGlobalKeydown);
|
|
305
|
+
|
|
306
|
+
openButtons.forEach((button) => {
|
|
307
|
+
button.removeEventListener("click", onTriggerClick);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
closeButtons.forEach((button) => {
|
|
311
|
+
button.removeEventListener("click", onCloseClick);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
document.body.style.overflow = "";
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export const setupSearch = (documents: SearchDocument[]) => {
|
|
319
|
+
mountSearch(documents);
|
|
320
|
+
|
|
321
|
+
if (initialized) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
initialized = true;
|
|
326
|
+
document.addEventListener("astro:page-load", () => {
|
|
327
|
+
mountSearch(documents);
|
|
328
|
+
});
|
|
329
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
@import "@fontsource/inter/400.css";
|
|
2
|
+
@import "@fontsource/inter/700.css";
|
|
3
|
+
@import "@fontsource/jetbrains-mono/400.css";
|
|
4
|
+
@import "@fontsource/jetbrains-mono/700.css";
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
color-scheme: light;
|
|
8
|
+
--background-color: 255 255 255;
|
|
9
|
+
--foreground-color: 17 17 17;
|
|
10
|
+
--primary-color: 10 10 10;
|
|
11
|
+
--primary-foreground-color: 255 255 255;
|
|
12
|
+
--secondary-color: rgba(255, 255, 255, 0.9);
|
|
13
|
+
--muted-color: 102 102 102;
|
|
14
|
+
--accent-color: 242 242 242;
|
|
15
|
+
--border-color: 224 224 224;
|
|
16
|
+
|
|
17
|
+
--color-alert-note: 37 99 235;
|
|
18
|
+
--color-alert-tip: 5 150 105;
|
|
19
|
+
--color-alert-warning: 217 119 6;
|
|
20
|
+
--color-alert-important: 147 51 234;
|
|
21
|
+
--color-alert-caution: 220 38 38;
|
|
22
|
+
|
|
23
|
+
--font-sans:
|
|
24
|
+
"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
25
|
+
"Segoe UI", Helvetica, Arial, sans-serif;
|
|
26
|
+
--font-mono:
|
|
27
|
+
"JetBrains Mono", ui-monospace, SF Mono, SF Mono-Regular, Consolas,
|
|
28
|
+
"Liberation Mono", Menlo, Courier, monospace;
|
|
29
|
+
--border-radius: 0.5rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (prefers-color-scheme: dark) {
|
|
33
|
+
:root {
|
|
34
|
+
color-scheme: dark;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
* {
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
html,
|
|
43
|
+
body {
|
|
44
|
+
overscroll-behavior: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
body {
|
|
48
|
+
margin: 0;
|
|
49
|
+
min-height: 100vh;
|
|
50
|
+
color: rgba(var(--foreground-color) / 1);
|
|
51
|
+
background-color: rgba(var(--background-color) / 1);
|
|
52
|
+
font-family: var(--font-sans);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
a {
|
|
56
|
+
color: inherit;
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sr-only {
|
|
61
|
+
position: absolute;
|
|
62
|
+
width: 1px;
|
|
63
|
+
height: 1px;
|
|
64
|
+
padding: 0;
|
|
65
|
+
margin: -1px;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
clip: rect(0, 0, 0, 0);
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
border: 0;
|
|
70
|
+
}
|