@openlaboratory/open-doc 0.1.0
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 +91 -0
- package/app/.astro/collections/docs.schema.json +24 -0
- package/app/.astro/content-assets.mjs +1 -0
- package/app/.astro/content-modules.mjs +4 -0
- package/app/.astro/content.d.ts +218 -0
- package/app/.astro/data-store.json +1 -0
- package/app/.astro/settings.json +5 -0
- package/app/.astro/types.d.ts +2 -0
- package/app/astro.config.mjs +43 -0
- package/app/node_modules/.astro/data-store.json +1 -0
- package/app/node_modules/.vite/deps/@astrojs_react_client__js.js +163 -0
- package/app/node_modules/.vite/deps/@astrojs_react_client__js.js.map +7 -0
- package/app/node_modules/.vite/deps/_metadata.json +67 -0
- package/app/node_modules/.vite/deps/astro___aria-query.js +6776 -0
- package/app/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/app/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
- package/app/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/app/node_modules/.vite/deps/astro___cssesc.js +99 -0
- package/app/node_modules/.vite/deps/astro___cssesc.js.map +7 -0
- package/app/node_modules/.vite/deps/chunk-55ZOATU5.js +305 -0
- package/app/node_modules/.vite/deps/chunk-55ZOATU5.js.map +7 -0
- package/app/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/app/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/app/node_modules/.vite/deps/chunk-FEZZJEG2.js +6935 -0
- package/app/node_modules/.vite/deps/chunk-FEZZJEG2.js.map +7 -0
- package/app/node_modules/.vite/deps/package.json +3 -0
- package/app/node_modules/.vite/deps/react-dom.js +6 -0
- package/app/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/app/node_modules/.vite/deps/react.js +5 -0
- package/app/node_modules/.vite/deps/react.js.map +7 -0
- package/app/node_modules/.vite/deps/react_jsx-dev-runtime.js +39 -0
- package/app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/app/node_modules/.vite/deps/react_jsx-runtime.js +57 -0
- package/app/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/app/src/components/DocsMobileNav.tsx +124 -0
- package/app/src/components/DocsSearch.tsx +315 -0
- package/app/src/components/DocsSidebar.astro +46 -0
- package/app/src/components/DocsTableOfContents.tsx +92 -0
- package/app/src/components/Navbar.astro +39 -0
- package/app/src/components/SocialIcon.astro +54 -0
- package/app/src/components/ThemeToggle.tsx +62 -0
- package/app/src/content.config.ts +17 -0
- package/app/src/env.d.ts +7 -0
- package/app/src/integrations/open-doc-config.mjs +65 -0
- package/app/src/layouts/DocsLayout.astro +369 -0
- package/app/src/lib/config.ts +36 -0
- package/app/src/lib/navigation.ts +68 -0
- package/app/src/lib/withBase.ts +11 -0
- package/app/src/pages/404.astro +24 -0
- package/app/src/pages/[...slug].astro +34 -0
- package/app/src/pages/index.astro +107 -0
- package/app/src/styles/global.css +324 -0
- package/app/tailwind.config.mjs +53 -0
- package/app/tsconfig.json +11 -0
- package/bin/open-doc.js +2 -0
- package/dist/chunk-BRUM67K7.js +30 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +268 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +8 -0
- package/package.json +77 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__commonJS
|
|
3
|
+
} from "./chunk-5WRI5ZAA.js";
|
|
4
|
+
|
|
5
|
+
// ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-dev-runtime.production.min.js
|
|
6
|
+
var require_react_jsx_dev_runtime_production_min = __commonJS({
|
|
7
|
+
"../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-dev-runtime.production.min.js"(exports) {
|
|
8
|
+
"use strict";
|
|
9
|
+
var a = Symbol.for("react.fragment");
|
|
10
|
+
exports.Fragment = a;
|
|
11
|
+
exports.jsxDEV = void 0;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-dev-runtime.js
|
|
16
|
+
var require_jsx_dev_runtime = __commonJS({
|
|
17
|
+
"../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-dev-runtime.js"(exports, module) {
|
|
18
|
+
if (true) {
|
|
19
|
+
module.exports = require_react_jsx_dev_runtime_production_min();
|
|
20
|
+
} else {
|
|
21
|
+
module.exports = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
export default require_jsx_dev_runtime();
|
|
26
|
+
/*! Bundled license information:
|
|
27
|
+
|
|
28
|
+
react/cjs/react-jsx-dev-runtime.production.min.js:
|
|
29
|
+
(**
|
|
30
|
+
* @license React
|
|
31
|
+
* react-jsx-dev-runtime.production.min.js
|
|
32
|
+
*
|
|
33
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
34
|
+
*
|
|
35
|
+
* This source code is licensed under the MIT license found in the
|
|
36
|
+
* LICENSE file in the root directory of this source tree.
|
|
37
|
+
*)
|
|
38
|
+
*/
|
|
39
|
+
//# sourceMappingURL=react_jsx-dev-runtime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-dev-runtime.production.min.js", "../../../../../../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-dev-runtime.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * @license React\n * react-jsx-dev-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var a=Symbol.for(\"react.fragment\");exports.Fragment=a;exports.jsxDEV=void 0;\n", "'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react-jsx-dev-runtime.production.min.js');\n} else {\n module.exports = require('./cjs/react-jsx-dev-runtime.development.js');\n}\n"],
|
|
5
|
+
"mappings": ";;;;;AAAA;AAAA;AAAA;AASa,QAAI,IAAE,OAAO,IAAI,gBAAgB;AAAE,YAAQ,WAAS;AAAE,YAAQ,SAAO;AAAA;AAAA;;;ACTlF;AAAA;AAEA,QAAI,MAAuC;AACzC,aAAO,UAAU;AAAA,IACnB,OAAO;AACL,aAAO,UAAU;AAAA,IACnB;AAAA;AAAA;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
require_react
|
|
3
|
+
} from "./chunk-55ZOATU5.js";
|
|
4
|
+
import {
|
|
5
|
+
__commonJS
|
|
6
|
+
} from "./chunk-5WRI5ZAA.js";
|
|
7
|
+
|
|
8
|
+
// ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.js
|
|
9
|
+
var require_react_jsx_runtime_production_min = __commonJS({
|
|
10
|
+
"../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.js"(exports) {
|
|
11
|
+
"use strict";
|
|
12
|
+
var f = require_react();
|
|
13
|
+
var k = Symbol.for("react.element");
|
|
14
|
+
var l = Symbol.for("react.fragment");
|
|
15
|
+
var m = Object.prototype.hasOwnProperty;
|
|
16
|
+
var n = f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
|
|
17
|
+
var p = { key: true, ref: true, __self: true, __source: true };
|
|
18
|
+
function q(c, a, g) {
|
|
19
|
+
var b, d = {}, e = null, h = null;
|
|
20
|
+
void 0 !== g && (e = "" + g);
|
|
21
|
+
void 0 !== a.key && (e = "" + a.key);
|
|
22
|
+
void 0 !== a.ref && (h = a.ref);
|
|
23
|
+
for (b in a) m.call(a, b) && !p.hasOwnProperty(b) && (d[b] = a[b]);
|
|
24
|
+
if (c && c.defaultProps) for (b in a = c.defaultProps, a) void 0 === d[b] && (d[b] = a[b]);
|
|
25
|
+
return { $$typeof: k, type: c, key: e, ref: h, props: d, _owner: n.current };
|
|
26
|
+
}
|
|
27
|
+
exports.Fragment = l;
|
|
28
|
+
exports.jsx = q;
|
|
29
|
+
exports.jsxs = q;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js
|
|
34
|
+
var require_jsx_runtime = __commonJS({
|
|
35
|
+
"../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js"(exports, module) {
|
|
36
|
+
if (true) {
|
|
37
|
+
module.exports = require_react_jsx_runtime_production_min();
|
|
38
|
+
} else {
|
|
39
|
+
module.exports = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export default require_jsx_runtime();
|
|
44
|
+
/*! Bundled license information:
|
|
45
|
+
|
|
46
|
+
react/cjs/react-jsx-runtime.production.min.js:
|
|
47
|
+
(**
|
|
48
|
+
* @license React
|
|
49
|
+
* react-jsx-runtime.production.min.js
|
|
50
|
+
*
|
|
51
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
52
|
+
*
|
|
53
|
+
* This source code is licensed under the MIT license found in the
|
|
54
|
+
* LICENSE file in the root directory of this source tree.
|
|
55
|
+
*)
|
|
56
|
+
*/
|
|
57
|
+
//# sourceMappingURL=react_jsx-runtime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react-jsx-runtime.production.min.js", "../../../../../../node_modules/.pnpm/react@18.3.1/node_modules/react/jsx-runtime.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n", "'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;AAAA;AAAA;AAAA;AASa,QAAI,IAAE;AAAN,QAAuB,IAAE,OAAO,IAAI,eAAe;AAAnD,QAAqD,IAAE,OAAO,IAAI,gBAAgB;AAAlF,QAAoF,IAAE,OAAO,UAAU;AAAvG,QAAsH,IAAE,EAAE,mDAAmD;AAA7K,QAA+L,IAAE,EAAC,KAAI,MAAG,KAAI,MAAG,QAAO,MAAG,UAAS,KAAE;AAClP,aAAS,EAAE,GAAE,GAAE,GAAE;AAAC,UAAI,GAAE,IAAE,CAAC,GAAE,IAAE,MAAK,IAAE;AAAK,iBAAS,MAAI,IAAE,KAAG;AAAG,iBAAS,EAAE,QAAM,IAAE,KAAG,EAAE;AAAK,iBAAS,EAAE,QAAM,IAAE,EAAE;AAAK,WAAI,KAAK,EAAE,GAAE,KAAK,GAAE,CAAC,KAAG,CAAC,EAAE,eAAe,CAAC,MAAI,EAAE,CAAC,IAAE,EAAE,CAAC;AAAG,UAAG,KAAG,EAAE,aAAa,MAAI,KAAK,IAAE,EAAE,cAAa,EAAE,YAAS,EAAE,CAAC,MAAI,EAAE,CAAC,IAAE,EAAE,CAAC;AAAG,aAAM,EAAC,UAAS,GAAE,MAAK,GAAE,KAAI,GAAE,KAAI,GAAE,OAAM,GAAE,QAAO,EAAE,QAAO;AAAA,IAAC;AAAC,YAAQ,WAAS;AAAE,YAAQ,MAAI;AAAE,YAAQ,OAAK;AAAA;AAAA;;;ACV1W;AAAA;AAEA,QAAI,MAAuC;AACzC,aAAO,UAAU;AAAA,IACnB,OAAO;AACL,aAAO,UAAU;AAAA,IACnB;AAAA;AAAA;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import type { DocSection } from '../lib/navigation'
|
|
3
|
+
import { withBase } from '../lib/withBase'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
navigation: DocSection[]
|
|
7
|
+
currentSlug: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Mobile-only trigger bar + slide-in navigation drawer. */
|
|
11
|
+
export function DocsMobileNav({ navigation, currentSlug }: Props) {
|
|
12
|
+
const [open, setOpen] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handler = () => setOpen(false)
|
|
16
|
+
document.addEventListener('astro:page-load', handler)
|
|
17
|
+
return () => document.removeEventListener('astro:page-load', handler)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
document.body.style.overflow = open ? 'hidden' : ''
|
|
22
|
+
return () => {
|
|
23
|
+
document.body.style.overflow = ''
|
|
24
|
+
}
|
|
25
|
+
}, [open])
|
|
26
|
+
|
|
27
|
+
const allPages = navigation.flatMap((s) => s.pages)
|
|
28
|
+
const currentPage = allPages.find((p) => p.slug === currentSlug)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<div className="sticky top-[57px] z-30 flex items-center border-b border-foreground/[0.08] bg-surface-sidebar px-4 py-2.5 lg:hidden">
|
|
33
|
+
<button
|
|
34
|
+
onClick={() => setOpen(true)}
|
|
35
|
+
className="flex items-center gap-2 text-sm text-foreground/60 transition-colors hover:text-foreground/90"
|
|
36
|
+
aria-label="Open navigation"
|
|
37
|
+
>
|
|
38
|
+
<svg
|
|
39
|
+
className="h-4 w-4"
|
|
40
|
+
fill="none"
|
|
41
|
+
viewBox="0 0 24 24"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
strokeWidth={2}
|
|
44
|
+
>
|
|
45
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />
|
|
46
|
+
</svg>
|
|
47
|
+
<span>Menu</span>
|
|
48
|
+
</button>
|
|
49
|
+
{currentPage && (
|
|
50
|
+
<>
|
|
51
|
+
<svg
|
|
52
|
+
className="mx-2 h-4 w-4 text-foreground/25"
|
|
53
|
+
fill="none"
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
stroke="currentColor"
|
|
56
|
+
strokeWidth={1.5}
|
|
57
|
+
>
|
|
58
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
|
59
|
+
</svg>
|
|
60
|
+
<span className="truncate text-sm text-foreground/80">{currentPage.title}</span>
|
|
61
|
+
</>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{open && (
|
|
66
|
+
<div className="fixed inset-0 z-50 lg:hidden">
|
|
67
|
+
<div
|
|
68
|
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
69
|
+
onClick={() => setOpen(false)}
|
|
70
|
+
/>
|
|
71
|
+
<aside className="absolute inset-y-0 left-0 w-72 overflow-y-auto bg-surface-sidebar shadow-2xl">
|
|
72
|
+
<div className="flex items-center justify-between border-b border-foreground/[0.08] px-4 py-3">
|
|
73
|
+
<span className="text-sm font-semibold text-foreground/90">Documentation</span>
|
|
74
|
+
<button
|
|
75
|
+
onClick={() => setOpen(false)}
|
|
76
|
+
className="flex h-7 w-7 items-center justify-center rounded-md text-foreground/40 transition-colors hover:bg-foreground/[0.08] hover:text-foreground/80"
|
|
77
|
+
aria-label="Close navigation"
|
|
78
|
+
>
|
|
79
|
+
<svg
|
|
80
|
+
className="h-4 w-4"
|
|
81
|
+
fill="none"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
stroke="currentColor"
|
|
84
|
+
strokeWidth={2}
|
|
85
|
+
>
|
|
86
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
87
|
+
</svg>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<nav className="flex flex-col gap-6 px-4 py-5">
|
|
92
|
+
{navigation.map((section) => (
|
|
93
|
+
<div key={section.label} className="flex flex-col gap-1">
|
|
94
|
+
<p className="mb-1 px-2 text-[11px] font-semibold uppercase tracking-widest text-foreground/40">
|
|
95
|
+
{section.label}
|
|
96
|
+
</p>
|
|
97
|
+
{section.pages.map((page) => {
|
|
98
|
+
const isActive = currentSlug === page.slug
|
|
99
|
+
return (
|
|
100
|
+
<a
|
|
101
|
+
key={page.slug}
|
|
102
|
+
href={withBase(page.slug)}
|
|
103
|
+
onClick={() => setOpen(false)}
|
|
104
|
+
className={[
|
|
105
|
+
'flex items-center rounded-md px-2 py-1.5 text-sm transition-all',
|
|
106
|
+
isActive
|
|
107
|
+
? 'bg-foreground/[0.08] font-medium text-foreground'
|
|
108
|
+
: 'text-foreground/60 hover:bg-foreground/[0.05] hover:text-foreground/90',
|
|
109
|
+
].join(' ')}
|
|
110
|
+
aria-current={isActive ? 'page' : undefined}
|
|
111
|
+
>
|
|
112
|
+
{page.title}
|
|
113
|
+
</a>
|
|
114
|
+
)
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</nav>
|
|
119
|
+
</aside>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import Fuse from 'fuse.js'
|
|
3
|
+
import type { DocSection, DocPage } from '../lib/navigation'
|
|
4
|
+
import { withBase } from '../lib/withBase'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
navigation: DocSection[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Result {
|
|
11
|
+
url: string
|
|
12
|
+
title: string
|
|
13
|
+
excerpt?: string
|
|
14
|
+
/** Whether `excerpt` contains pre-highlighted HTML (Pagefind) or plain text. */
|
|
15
|
+
html?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isEditableTarget(target: EventTarget | null): boolean {
|
|
19
|
+
const el = target as HTMLElement | null
|
|
20
|
+
if (!el) return false
|
|
21
|
+
return (
|
|
22
|
+
el.tagName === 'INPUT' ||
|
|
23
|
+
el.tagName === 'TEXTAREA' ||
|
|
24
|
+
el.tagName === 'SELECT' ||
|
|
25
|
+
el.isContentEditable
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* ⌘K search modal. Uses Pagefind full-text search against the built site, and
|
|
31
|
+
* gracefully falls back to a lightweight title/description search (Fuse.js) when
|
|
32
|
+
* the Pagefind index isn't available yet — e.g. during `open-doc dev` before a
|
|
33
|
+
* build has been run.
|
|
34
|
+
*/
|
|
35
|
+
export function DocsSearch({ navigation }: Props) {
|
|
36
|
+
const allPages = useMemo<DocPage[]>(() => navigation.flatMap((s) => s.pages), [navigation])
|
|
37
|
+
const fuse = useMemo(
|
|
38
|
+
() => new Fuse(allPages, { keys: ['title', 'description'], threshold: 0.4 }),
|
|
39
|
+
[allPages],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const [open, setOpen] = useState(false)
|
|
43
|
+
const [query, setQuery] = useState('')
|
|
44
|
+
const [results, setResults] = useState<Result[]>([])
|
|
45
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
46
|
+
const [fullText, setFullText] = useState(true)
|
|
47
|
+
|
|
48
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
49
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
50
|
+
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
51
|
+
const pagefindRef = useRef<any>(null)
|
|
52
|
+
const triedPagefind = useRef(false)
|
|
53
|
+
const requestId = useRef(0)
|
|
54
|
+
const wasOpen = useRef(false)
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
58
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
setOpen(true)
|
|
61
|
+
} else if (e.key === '/' && !isEditableTarget(e.target)) {
|
|
62
|
+
e.preventDefault()
|
|
63
|
+
setOpen(true)
|
|
64
|
+
} else if (e.key === 'Escape') {
|
|
65
|
+
setOpen(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
window.addEventListener('keydown', handleKey)
|
|
69
|
+
return () => window.removeEventListener('keydown', handleKey)
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (open) {
|
|
74
|
+
setTimeout(() => inputRef.current?.focus(), 50)
|
|
75
|
+
setQuery('')
|
|
76
|
+
setResults([])
|
|
77
|
+
setSelectedIndex(0)
|
|
78
|
+
} else if (wasOpen.current) {
|
|
79
|
+
// Restore focus to the trigger when the dialog closes.
|
|
80
|
+
triggerRef.current?.focus()
|
|
81
|
+
}
|
|
82
|
+
wasOpen.current = open
|
|
83
|
+
}, [open])
|
|
84
|
+
|
|
85
|
+
async function loadPagefind(): Promise<any | null> {
|
|
86
|
+
if (pagefindRef.current) return pagefindRef.current
|
|
87
|
+
if (triedPagefind.current) return null
|
|
88
|
+
triedPagefind.current = true
|
|
89
|
+
try {
|
|
90
|
+
const url = withBase('pagefind/pagefind.js')
|
|
91
|
+
const mod = await import(/* @vite-ignore */ url)
|
|
92
|
+
await mod.init?.()
|
|
93
|
+
pagefindRef.current = mod
|
|
94
|
+
return mod
|
|
95
|
+
} catch {
|
|
96
|
+
setFullText(false)
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const id = ++requestId.current
|
|
103
|
+
const q = query.trim()
|
|
104
|
+
if (!q) {
|
|
105
|
+
setResults([])
|
|
106
|
+
setSelectedIndex(0)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
void (async () => {
|
|
111
|
+
const pf = await loadPagefind()
|
|
112
|
+
if (id !== requestId.current) return
|
|
113
|
+
|
|
114
|
+
if (pf) {
|
|
115
|
+
const search = await pf.search(q)
|
|
116
|
+
const data = await Promise.all(search.results.slice(0, 8).map((r: any) => r.data()))
|
|
117
|
+
if (id !== requestId.current) return
|
|
118
|
+
setResults(
|
|
119
|
+
data.map((d: any) => ({
|
|
120
|
+
url: d.url,
|
|
121
|
+
title: d.meta?.title || d.url,
|
|
122
|
+
excerpt: d.excerpt,
|
|
123
|
+
html: true,
|
|
124
|
+
})),
|
|
125
|
+
)
|
|
126
|
+
} else {
|
|
127
|
+
setResults(
|
|
128
|
+
fuse.search(q).map((r) => ({
|
|
129
|
+
url: withBase(r.item.slug),
|
|
130
|
+
title: r.item.title,
|
|
131
|
+
excerpt: r.item.description,
|
|
132
|
+
})),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
setSelectedIndex(0)
|
|
136
|
+
})()
|
|
137
|
+
}, [query, fuse])
|
|
138
|
+
|
|
139
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
140
|
+
if (e.key === 'ArrowDown') {
|
|
141
|
+
e.preventDefault()
|
|
142
|
+
setSelectedIndex((i) => Math.min(i + 1, results.length - 1))
|
|
143
|
+
} else if (e.key === 'ArrowUp') {
|
|
144
|
+
e.preventDefault()
|
|
145
|
+
setSelectedIndex((i) => Math.max(i - 1, 0))
|
|
146
|
+
} else if (e.key === 'Enter') {
|
|
147
|
+
const result = results[selectedIndex]
|
|
148
|
+
if (result) {
|
|
149
|
+
window.location.href = result.url
|
|
150
|
+
setOpen(false)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<button
|
|
158
|
+
ref={triggerRef}
|
|
159
|
+
onClick={() => setOpen(true)}
|
|
160
|
+
className="flex w-full items-center gap-2 rounded-md border border-foreground/[0.1] bg-foreground/[0.04] px-3 py-2 text-sm text-foreground/45 transition-colors hover:border-foreground/[0.16] hover:bg-foreground/[0.07] hover:text-foreground/60"
|
|
161
|
+
aria-label="Search documentation"
|
|
162
|
+
>
|
|
163
|
+
<svg
|
|
164
|
+
className="h-3.5 w-3.5 shrink-0"
|
|
165
|
+
fill="none"
|
|
166
|
+
viewBox="0 0 24 24"
|
|
167
|
+
stroke="currentColor"
|
|
168
|
+
strokeWidth={2}
|
|
169
|
+
>
|
|
170
|
+
<path
|
|
171
|
+
strokeLinecap="round"
|
|
172
|
+
strokeLinejoin="round"
|
|
173
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
174
|
+
/>
|
|
175
|
+
</svg>
|
|
176
|
+
<span className="flex-1 text-left">Search docs…</span>
|
|
177
|
+
<kbd className="hidden rounded border border-foreground/[0.12] bg-foreground/[0.05] px-1.5 py-0.5 font-mono text-[10px] leading-none text-foreground/40 sm:block">
|
|
178
|
+
⌘K
|
|
179
|
+
</kbd>
|
|
180
|
+
</button>
|
|
181
|
+
|
|
182
|
+
{open && (
|
|
183
|
+
<div
|
|
184
|
+
className="fixed inset-0 z-[100] flex items-start justify-center px-4 pt-[15vh]"
|
|
185
|
+
onClick={(e) => {
|
|
186
|
+
if (!dialogRef.current?.contains(e.target as Node)) setOpen(false)
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
190
|
+
|
|
191
|
+
<div
|
|
192
|
+
ref={dialogRef}
|
|
193
|
+
className="relative w-full max-w-xl overflow-hidden rounded-xl border border-foreground/[0.12] bg-surface-raised shadow-2xl"
|
|
194
|
+
role="dialog"
|
|
195
|
+
aria-modal="true"
|
|
196
|
+
aria-label="Search documentation"
|
|
197
|
+
>
|
|
198
|
+
<div className="flex items-center gap-3 border-b border-foreground/[0.08] px-4 py-3">
|
|
199
|
+
<svg
|
|
200
|
+
className="h-4 w-4 shrink-0 text-foreground/40"
|
|
201
|
+
fill="none"
|
|
202
|
+
viewBox="0 0 24 24"
|
|
203
|
+
stroke="currentColor"
|
|
204
|
+
strokeWidth={2}
|
|
205
|
+
>
|
|
206
|
+
<path
|
|
207
|
+
strokeLinecap="round"
|
|
208
|
+
strokeLinejoin="round"
|
|
209
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
210
|
+
/>
|
|
211
|
+
</svg>
|
|
212
|
+
<input
|
|
213
|
+
ref={inputRef}
|
|
214
|
+
type="text"
|
|
215
|
+
placeholder="Search documentation…"
|
|
216
|
+
value={query}
|
|
217
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
218
|
+
onKeyDown={handleKeyDown}
|
|
219
|
+
role="combobox"
|
|
220
|
+
aria-expanded={results.length > 0}
|
|
221
|
+
aria-controls="od-search-results"
|
|
222
|
+
aria-activedescendant={
|
|
223
|
+
results[selectedIndex] ? `od-search-option-${selectedIndex}` : undefined
|
|
224
|
+
}
|
|
225
|
+
className="flex-1 bg-transparent text-sm text-foreground placeholder-foreground/30 outline-none"
|
|
226
|
+
/>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => setOpen(false)}
|
|
229
|
+
className="rounded border border-foreground/[0.1] px-1.5 py-0.5 text-[11px] text-foreground/40 transition-colors hover:text-foreground/70"
|
|
230
|
+
>
|
|
231
|
+
Esc
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<div
|
|
236
|
+
className="max-h-[360px] overflow-y-auto py-2"
|
|
237
|
+
id="od-search-results"
|
|
238
|
+
role="listbox"
|
|
239
|
+
>
|
|
240
|
+
{query.trim() && results.length === 0 ? (
|
|
241
|
+
<p className="px-4 py-8 text-center text-sm text-foreground/40">
|
|
242
|
+
No results for “{query}”
|
|
243
|
+
</p>
|
|
244
|
+
) : (
|
|
245
|
+
results.map((result, i) => {
|
|
246
|
+
const isSelected = i === selectedIndex
|
|
247
|
+
return (
|
|
248
|
+
<a
|
|
249
|
+
key={result.url + i}
|
|
250
|
+
id={`od-search-option-${i}`}
|
|
251
|
+
role="option"
|
|
252
|
+
aria-selected={isSelected}
|
|
253
|
+
href={result.url}
|
|
254
|
+
onClick={() => setOpen(false)}
|
|
255
|
+
onMouseEnter={() => setSelectedIndex(i)}
|
|
256
|
+
className={[
|
|
257
|
+
'flex items-start gap-3 px-4 py-2.5 transition-colors',
|
|
258
|
+
isSelected ? 'bg-foreground/[0.08]' : 'hover:bg-foreground/[0.04]',
|
|
259
|
+
].join(' ')}
|
|
260
|
+
>
|
|
261
|
+
<svg
|
|
262
|
+
className="mt-0.5 h-4 w-4 shrink-0 text-foreground/30"
|
|
263
|
+
fill="none"
|
|
264
|
+
viewBox="0 0 24 24"
|
|
265
|
+
stroke="currentColor"
|
|
266
|
+
strokeWidth={2}
|
|
267
|
+
>
|
|
268
|
+
<path
|
|
269
|
+
strokeLinecap="round"
|
|
270
|
+
strokeLinejoin="round"
|
|
271
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
|
272
|
+
/>
|
|
273
|
+
</svg>
|
|
274
|
+
<div className="min-w-0">
|
|
275
|
+
<div className="text-sm font-medium leading-snug text-foreground/90">
|
|
276
|
+
{result.title}
|
|
277
|
+
</div>
|
|
278
|
+
{result.excerpt &&
|
|
279
|
+
(result.html ? (
|
|
280
|
+
<div
|
|
281
|
+
className="search-excerpt mt-0.5 line-clamp-2 text-xs text-foreground/45"
|
|
282
|
+
dangerouslySetInnerHTML={{ __html: result.excerpt }}
|
|
283
|
+
/>
|
|
284
|
+
) : (
|
|
285
|
+
<div className="mt-0.5 truncate text-xs text-foreground/45">
|
|
286
|
+
{result.excerpt}
|
|
287
|
+
</div>
|
|
288
|
+
))}
|
|
289
|
+
</div>
|
|
290
|
+
</a>
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div className="flex items-center gap-4 border-t border-foreground/[0.06] px-4 py-2 text-[10px] text-foreground/30">
|
|
297
|
+
<span className="flex items-center gap-1">
|
|
298
|
+
<kbd className="font-mono">↑↓</kbd> navigate
|
|
299
|
+
</span>
|
|
300
|
+
<span className="flex items-center gap-1">
|
|
301
|
+
<kbd className="font-mono">↵</kbd> open
|
|
302
|
+
</span>
|
|
303
|
+
<span className="flex items-center gap-1">
|
|
304
|
+
<kbd className="font-mono">esc</kbd> close
|
|
305
|
+
</span>
|
|
306
|
+
{!fullText && (
|
|
307
|
+
<span className="ml-auto">Title search · run a build for full-text</span>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { DocSection } from '../lib/navigation'
|
|
3
|
+
import { withBase } from '../lib/withBase'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
navigation: DocSection[]
|
|
7
|
+
currentSlug: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { navigation, currentSlug } = Astro.props
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<nav class="flex flex-col gap-6 px-4 py-6" aria-label="Documentation navigation">
|
|
14
|
+
{
|
|
15
|
+
navigation.map((section) => (
|
|
16
|
+
<div class="flex flex-col gap-1">
|
|
17
|
+
<p class="mb-1 px-2 text-[11px] font-semibold uppercase tracking-widest text-foreground/40">
|
|
18
|
+
{section.label}
|
|
19
|
+
</p>
|
|
20
|
+
{section.pages.map((page) => {
|
|
21
|
+
const isActive = currentSlug === page.slug
|
|
22
|
+
return (
|
|
23
|
+
<a
|
|
24
|
+
href={withBase(page.slug)}
|
|
25
|
+
class:list={[
|
|
26
|
+
'group flex items-center rounded-md px-2 py-1.5 text-sm transition-all',
|
|
27
|
+
isActive
|
|
28
|
+
? 'bg-foreground/[0.08] text-foreground'
|
|
29
|
+
: 'text-foreground/60 hover:bg-foreground/[0.05] hover:text-foreground/90',
|
|
30
|
+
]}
|
|
31
|
+
aria-current={isActive ? 'page' : undefined}
|
|
32
|
+
>
|
|
33
|
+
<span
|
|
34
|
+
class:list={[
|
|
35
|
+
'mr-2 h-1 w-1 shrink-0 rounded-full',
|
|
36
|
+
isActive ? 'bg-foreground/70' : 'bg-transparent group-hover:bg-foreground/30',
|
|
37
|
+
]}
|
|
38
|
+
/>
|
|
39
|
+
{page.title}
|
|
40
|
+
</a>
|
|
41
|
+
)
|
|
42
|
+
})}
|
|
43
|
+
</div>
|
|
44
|
+
))
|
|
45
|
+
}
|
|
46
|
+
</nav>
|