@terra-symposium/docs 0.1.73 → 0.1.75
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 +1 -1
- package/bin/terra-docs.mjs +151 -115
- package/lib-dist/components/StaticWorkspace.d.ts.map +1 -1
- package/lib-dist/components/StaticWorkspace.js +4 -4
- package/lib-dist/components/StaticWorkspace.js.map +1 -1
- package/lib-dist/components/terra/Workspace.d.ts.map +1 -1
- package/lib-dist/components/terra/Workspace.js +1 -1
- package/lib-dist/components/terra/Workspace.js.map +1 -1
- package/lib-dist/components/terra/pages/docs/_editable.d.ts +2 -1
- package/lib-dist/components/terra/pages/docs/_editable.d.ts.map +1 -1
- package/lib-dist/components/terra/pages/docs/_editable.js +2 -2
- package/lib-dist/components/terra/pages/docs/_editable.js.map +1 -1
- package/lib-dist/components/terra/pages/docs/_main.d.ts +2 -1
- package/lib-dist/components/terra/pages/docs/_main.d.ts.map +1 -1
- package/lib-dist/components/terra/pages/docs/_main.js +5 -5
- package/lib-dist/components/terra/pages/docs/_main.js.map +1 -1
- package/lib-dist/components/terra/pages/docs/_sidebar.d.ts.map +1 -1
- package/lib-dist/components/terra/pages/docs/_sidebar.js +5 -2
- package/lib-dist/components/terra/pages/docs/_sidebar.js.map +1 -1
- package/lib-dist/components/terra/pages/docs/_toolbar.d.ts.map +1 -1
- package/lib-dist/components/terra/pages/docs/_toolbar.js +1 -4
- package/lib-dist/components/terra/pages/docs/_toolbar.js.map +1 -1
- package/lib-dist/components/terra/ui.d.ts +3 -1
- package/lib-dist/components/terra/ui.d.ts.map +1 -1
- package/lib-dist/components/terra/ui.js +11 -3
- package/lib-dist/components/terra/ui.js.map +1 -1
- package/lib-dist/lib/host.d.ts +2 -0
- package/lib-dist/lib/host.d.ts.map +1 -1
- package/lib-dist/lib/workspace-types.d.ts.map +1 -1
- package/lib-dist/style.css +1 -1
- package/package.json +2 -1
- package/site/astro.config.mjs +45 -43
- package/site/base-path-remark-plugin.mjs +30 -23
- package/site/docs-content-plugin.mjs +68 -57
- package/site/src/components/DocsSiteSearch.tsx +106 -91
- package/site/src/lib/catalog.ts +110 -96
- package/site/src/styles/search.css +114 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terra-symposium/docs",
|
|
3
|
-
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"description": "Terra Docs workspace and Markdown site generator.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"@types/sanitize-html": "^2.16.2",
|
|
74
74
|
"@vitejs/plugin-react": "6.1.2",
|
|
75
75
|
"eslint": "10.13.0",
|
|
76
|
+
"globals": "^16.5.0",
|
|
76
77
|
"husky": "^9.1.7",
|
|
77
78
|
"postcss": "8.5.31",
|
|
78
79
|
"postcss-import": "15.1.0",
|
package/site/astro.config.mjs
CHANGED
|
@@ -13,59 +13,61 @@ const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
|
13
13
|
const requireFromPackage = createRequire(import.meta.url);
|
|
14
14
|
const projectRoot = process.env.TERRA_DOCS_ROOT ?? process.cwd();
|
|
15
15
|
const publicDir = resolve(projectRoot, '.terra-docs/public');
|
|
16
|
-
const outDir = resolve(projectRoot, process.env.TERRA_DOCS_OUT ?? 'docs
|
|
16
|
+
const outDir = resolve(projectRoot, process.env.TERRA_DOCS_OUT ?? 'dist/docs');
|
|
17
17
|
const site = process.env.TERRA_DOCS_SITE;
|
|
18
18
|
const base = process.env.TERRA_DOCS_BASE ?? '/';
|
|
19
19
|
const projectManifest = (() => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(resolve(projectRoot, 'package.json'), 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
25
|
})();
|
|
26
26
|
|
|
27
27
|
function packageExportAliases() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
if (!projectManifest.name || !projectManifest.exports) return [];
|
|
29
|
+
const escapeRegex = (value) => value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
30
|
+
return Object.entries(projectManifest.exports).flatMap(([key, value]) => {
|
|
31
|
+
const target = typeof value === 'string' ? value : (value?.import ?? value?.default);
|
|
32
|
+
if (!target?.startsWith('./')) return [];
|
|
33
|
+
const prefix = projectManifest.name + (key === '.' ? '' : key.slice(1));
|
|
34
|
+
if (key.includes('*')) {
|
|
35
|
+
const matcher = new RegExp(`^${escapeRegex(prefix).replace('*', '(.+)')}$`);
|
|
36
|
+
return [{ find: matcher, replacement: resolve(projectRoot, target.replace('*', '$1')) }];
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
{ find: new RegExp(`^${escapeRegex(prefix)}$`), replacement: resolve(projectRoot, target) },
|
|
40
|
+
];
|
|
41
|
+
});
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
const packageDependencyResolver = {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
name: 'terra-docs-package-dependencies',
|
|
46
|
+
enforce: 'pre',
|
|
47
|
+
resolveId(source) {
|
|
48
|
+
if (!/^(?:astro|@astrojs\/(?:markdown-remark|mdx|react))(?:\/|$)/.test(source)) return null;
|
|
49
|
+
try {
|
|
50
|
+
return requireFromPackage.resolve(source);
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
export default defineConfig({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
58
|
+
root: projectRoot,
|
|
59
|
+
srcDir: resolve(packageRoot, 'site/src'),
|
|
60
|
+
publicDir,
|
|
61
|
+
outDir,
|
|
62
|
+
...(site ? { site } : {}),
|
|
63
|
+
base,
|
|
64
|
+
devToolbar: { enabled: false },
|
|
65
|
+
build: { format: 'directory' },
|
|
66
|
+
integrations: [react({ experimentalDisableStreaming: true }), mdx()],
|
|
67
|
+
markdown: { processor: unified({ remarkPlugins: [[basePathRemarkPlugin, { base }]] }) },
|
|
68
|
+
vite: {
|
|
69
|
+
plugins: [packageDependencyResolver, docsContentPlugin({ root: projectRoot, base })],
|
|
70
|
+
resolve: { alias: packageExportAliases() },
|
|
71
|
+
server: { fs: { allow: [projectRoot, packageRoot] } },
|
|
72
|
+
},
|
|
71
73
|
});
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
function addBasePath(value, base) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//') || base === '/')
|
|
3
|
+
return value;
|
|
4
|
+
const basePath = `/${base.replace(/^\/+|\/+$/g, '')}`;
|
|
5
|
+
if (value === basePath) return `${basePath}/`;
|
|
6
|
+
if (value.startsWith(`${basePath}/`)) return value;
|
|
7
|
+
return `${basePath}${value}`;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
function addBaseToHtml(source, base) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
return source.replace(
|
|
12
|
+
/\b(href|src)=(['"])(\/[^'"#?]*)([^'"]*)\2/gi,
|
|
13
|
+
(_match, attribute, quote, pathname, suffix) =>
|
|
14
|
+
`${attribute}=${quote}${addBasePath(`${pathname}${suffix}`, base)}${quote}`,
|
|
15
|
+
);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export function basePathRemarkPlugin({ base }) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
return (tree) => {
|
|
20
|
+
const visit = (node) => {
|
|
21
|
+
if (['link', 'image', 'definition'].includes(node.type) && typeof node.url === 'string') {
|
|
22
|
+
node.url = addBasePath(node.url, base);
|
|
23
|
+
}
|
|
24
|
+
if (node.type === 'html' && typeof node.value === 'string') {
|
|
25
|
+
node.value = addBaseToHtml(node.value, base);
|
|
26
|
+
}
|
|
27
|
+
if (
|
|
28
|
+
node.type === 'mdxJsxAttribute' &&
|
|
29
|
+
['href', 'src'].includes(node.name) &&
|
|
30
|
+
typeof node.value === 'string'
|
|
31
|
+
) {
|
|
32
|
+
node.value = addBasePath(node.value, base);
|
|
33
|
+
}
|
|
34
|
+
for (const child of node.children ?? []) visit(child);
|
|
35
|
+
};
|
|
36
|
+
visit(tree);
|
|
37
|
+
};
|
|
31
38
|
}
|
|
@@ -4,70 +4,81 @@ import { extname, relative, resolve, sep } from 'node:path';
|
|
|
4
4
|
const markdownExtensions = new Set(['.md', '.mdx']);
|
|
5
5
|
|
|
6
6
|
function publicRoute(file, docsRoot, base) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const path = relative(docsRoot, file)
|
|
8
|
+
.split(sep)
|
|
9
|
+
.join('/')
|
|
10
|
+
.replace(/\.(?:md|mdx)$/i, '');
|
|
11
|
+
const route = path.replace(/(?:^|\/)index$/i, '').replace(/^\/+|\/+$/g, '');
|
|
12
|
+
return `${base}${route ? `${route}/` : ''}`;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
function rewriteTarget(rawTarget, sourceFile, docsRoot, base) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
16
|
+
const target = rawTarget.trim();
|
|
17
|
+
if (!target || /^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(target)) return rawTarget;
|
|
18
|
+
const match = /^(<[^>]+>|[^\s]+)([\s\S]*)$/.exec(target);
|
|
19
|
+
if (!match) return rawTarget;
|
|
20
|
+
const bracketed = match[1].startsWith('<') && match[1].endsWith('>');
|
|
21
|
+
const pathname = bracketed ? match[1].slice(1, -1) : match[1];
|
|
22
|
+
const suffix = match[2];
|
|
23
|
+
const [filePath, ...hashParts] = pathname.split('#');
|
|
24
|
+
const [cleanPath, ...queryParts] = filePath.split('?');
|
|
25
|
+
const fragment = hashParts.length ? `#${hashParts.join('#')}` : '';
|
|
26
|
+
const query = queryParts.length ? `?${queryParts.join('?')}` : '';
|
|
27
|
+
if (cleanPath.startsWith('/')) {
|
|
28
|
+
const basePath = base === '/' ? '' : `/${base.replace(/^\/+|\/+$/g, '')}`;
|
|
29
|
+
const rootBase = `${basePath}/`;
|
|
30
|
+
const routedPath =
|
|
31
|
+
base === '/' || cleanPath.startsWith(rootBase)
|
|
32
|
+
? cleanPath
|
|
33
|
+
: cleanPath === basePath
|
|
34
|
+
? rootBase
|
|
35
|
+
: `${basePath}${cleanPath}`;
|
|
36
|
+
const rewritten = `${routedPath}${query}${fragment}`;
|
|
37
|
+
return `${bracketed ? `<${rewritten}>` : rewritten}${suffix}`;
|
|
38
|
+
}
|
|
39
|
+
let resolvedPath;
|
|
40
|
+
try {
|
|
41
|
+
resolvedPath = resolve(sourceFile, '..', decodeURIComponent(cleanPath));
|
|
42
|
+
} catch {
|
|
43
|
+
return rawTarget;
|
|
44
|
+
}
|
|
45
|
+
const relativePath = relative(docsRoot, resolvedPath);
|
|
46
|
+
if (relativePath.startsWith(`..${sep}`) || relativePath === '..' || !existsSync(resolvedPath))
|
|
47
|
+
return rawTarget;
|
|
48
|
+
const extension = extname(resolvedPath).toLowerCase();
|
|
49
|
+
const rewritten = markdownExtensions.has(extension)
|
|
50
|
+
? publicRoute(resolvedPath, docsRoot, base)
|
|
51
|
+
: `${base}_terra-docs/assets/${relativePath.split(sep).join('/')}`;
|
|
52
|
+
return `${bracketed ? `<${rewritten}${query}${fragment}>` : `${rewritten}${query}${fragment}`}${suffix}`;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
function mapOutsideCode(source, callback) {
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
const sections = source.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~)/g);
|
|
57
|
+
return sections.map((section, index) => (index % 2 ? section : callback(section))).join('');
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function docsContentPlugin({ root, base }) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
const docsRoot = resolve(root, 'docs');
|
|
62
|
+
return {
|
|
63
|
+
name: 'terra-docs-content-assets',
|
|
64
|
+
enforce: 'pre',
|
|
65
|
+
transform(source, id) {
|
|
66
|
+
if (id.includes('?') || !markdownExtensions.has(extname(id).toLowerCase())) return null;
|
|
67
|
+
const sourceFile = resolve(id);
|
|
68
|
+
const relativePath = relative(docsRoot, sourceFile);
|
|
69
|
+
if (relativePath.startsWith(`..${sep}`) || relativePath === '..') return null;
|
|
70
|
+
const withMarkdownLinks = source.replace(
|
|
71
|
+
/(!?\[[^\]]*\]\()([^)]+)(\))/g,
|
|
72
|
+
(_full, before, target, after) =>
|
|
73
|
+
`${before}${rewriteTarget(target, sourceFile, docsRoot, base)}${after}`,
|
|
74
|
+
);
|
|
75
|
+
return mapOutsideCode(withMarkdownLinks, (section) =>
|
|
76
|
+
section.replace(
|
|
77
|
+
/\b(src|href)=(['"])(.*?)\2/g,
|
|
78
|
+
(full, attribute, quote, target) =>
|
|
79
|
+
`${attribute}=${quote}${rewriteTarget(target, sourceFile, docsRoot, base)}${quote}`,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
73
84
|
}
|
|
@@ -1,108 +1,123 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
type SearchDocument = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
href: string;
|
|
7
|
+
text: string;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
type Props = { base?: string };
|
|
11
11
|
|
|
12
12
|
function excerpt(text: string, query: string): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
14
|
+
const offset = normalized.toLowerCase().indexOf(query.toLowerCase());
|
|
15
|
+
const start = Math.max(0, offset - 72);
|
|
16
|
+
const end = Math.min(normalized.length, start + 190);
|
|
17
|
+
return `${start > 0 ? '…' : ''}${normalized.slice(start, end)}${end < normalized.length ? '…' : ''}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function DocsSiteSearch({ base = '/' }: Props) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
const dialog = useRef<HTMLDialogElement>(null);
|
|
22
|
+
const input = useRef<HTMLInputElement>(null);
|
|
23
|
+
const [query, setQuery] = useState('');
|
|
24
|
+
const [documents, setDocuments] = useState<SearchDocument[]>([]);
|
|
25
|
+
const [loaded, setLoaded] = useState(false);
|
|
26
|
+
const [failed, setFailed] = useState(false);
|
|
27
|
+
const [opened, setOpened] = useState(false);
|
|
28
|
+
const normalizedBase = base.endsWith('/') ? base : `${base}/`;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
const open = () => {
|
|
31
|
+
if (!dialog.current?.open) dialog.current?.showModal();
|
|
32
|
+
setOpened(true);
|
|
33
|
+
window.setTimeout(() => input.current?.focus(), 0);
|
|
34
|
+
};
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const onOpenSearch = () => open();
|
|
38
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
39
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
open();
|
|
42
|
+
} else if (
|
|
43
|
+
event.key === '/' &&
|
|
44
|
+
!/input|textarea|select/i.test((event.target as HTMLElement)?.tagName ?? '')
|
|
45
|
+
) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
open();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener('docs:open-search', onOpenSearch);
|
|
51
|
+
document.addEventListener('keydown', onKeyDown);
|
|
52
|
+
return () => {
|
|
53
|
+
document.removeEventListener('docs:open-search', onOpenSearch);
|
|
54
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!opened || loaded) return;
|
|
60
|
+
fetch(`${normalizedBase}_terra-docs/search-index.json`)
|
|
61
|
+
.then((response) => {
|
|
62
|
+
if (!response.ok) throw new Error(`Search index returned ${response.status}`);
|
|
63
|
+
return response.json() as Promise<SearchDocument[]>;
|
|
64
|
+
})
|
|
65
|
+
.then((data) => {
|
|
66
|
+
setDocuments(data);
|
|
67
|
+
setLoaded(true);
|
|
68
|
+
})
|
|
69
|
+
.catch(() => setFailed(true));
|
|
70
|
+
}, [loaded, normalizedBase, opened]);
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
const results = useMemo(() => {
|
|
73
|
+
const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
74
|
+
if (!terms.length) return [];
|
|
75
|
+
return documents
|
|
76
|
+
.map((document) => {
|
|
77
|
+
const title = document.title.toLowerCase();
|
|
78
|
+
const text = `${document.description} ${document.text}`.toLowerCase();
|
|
79
|
+
const matches = terms.filter((term) => title.includes(term) || text.includes(term));
|
|
80
|
+
const score = matches.length * 10 + terms.filter((term) => title.includes(term)).length * 5;
|
|
81
|
+
return { document, score };
|
|
82
|
+
})
|
|
83
|
+
.filter((result) => result.score > 0)
|
|
84
|
+
.sort(
|
|
85
|
+
(left, right) =>
|
|
86
|
+
right.score - left.score || left.document.title.localeCompare(right.document.title),
|
|
87
|
+
)
|
|
88
|
+
.slice(0, 10);
|
|
89
|
+
}, [documents, query]);
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
return (
|
|
92
|
+
<div className="terra-docs-search">
|
|
93
|
+
<dialog className="terra-docs-search-dialog" ref={dialog} onClose={() => setQuery('')}>
|
|
94
|
+
<div className="terra-docs-search-panel">
|
|
95
|
+
<label className="terra-docs-search-input-row">
|
|
96
|
+
<span aria-hidden="true">⌕</span>
|
|
97
|
+
<input
|
|
98
|
+
ref={input}
|
|
99
|
+
value={query}
|
|
100
|
+
onChange={(event) => setQuery(event.currentTarget.value)}
|
|
101
|
+
placeholder="Search documentation…"
|
|
102
|
+
/>
|
|
103
|
+
<button type="button" onClick={() => dialog.current?.close()} aria-label="Close search">
|
|
104
|
+
Esc
|
|
105
|
+
</button>
|
|
106
|
+
</label>
|
|
107
|
+
<div className="terra-docs-search-results" aria-live="polite">
|
|
108
|
+
{failed ? <p>Search is unavailable right now.</p> : null}
|
|
109
|
+
{!loaded && !failed ? <p>Loading search…</p> : null}
|
|
110
|
+
{loaded && query.trim() && results.length === 0 ? <p>No matching pages.</p> : null}
|
|
111
|
+
{results.map(({ document }) => (
|
|
112
|
+
<a key={document.href} href={document.href} onClick={() => dialog.current?.close()}>
|
|
113
|
+
<strong>{document.title}</strong>
|
|
114
|
+
{document.description ? <span>{document.description}</span> : null}
|
|
115
|
+
<small>{excerpt(document.text, query.trim().split(/\s+/)[0] ?? query)}</small>
|
|
116
|
+
</a>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</dialog>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
108
123
|
}
|