@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.
Files changed (61) hide show
  1. package/README.md +91 -0
  2. package/app/.astro/collections/docs.schema.json +24 -0
  3. package/app/.astro/content-assets.mjs +1 -0
  4. package/app/.astro/content-modules.mjs +4 -0
  5. package/app/.astro/content.d.ts +218 -0
  6. package/app/.astro/data-store.json +1 -0
  7. package/app/.astro/settings.json +5 -0
  8. package/app/.astro/types.d.ts +2 -0
  9. package/app/astro.config.mjs +43 -0
  10. package/app/node_modules/.astro/data-store.json +1 -0
  11. package/app/node_modules/.vite/deps/@astrojs_react_client__js.js +163 -0
  12. package/app/node_modules/.vite/deps/@astrojs_react_client__js.js.map +7 -0
  13. package/app/node_modules/.vite/deps/_metadata.json +67 -0
  14. package/app/node_modules/.vite/deps/astro___aria-query.js +6776 -0
  15. package/app/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
  16. package/app/node_modules/.vite/deps/astro___axobject-query.js +3754 -0
  17. package/app/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
  18. package/app/node_modules/.vite/deps/astro___cssesc.js +99 -0
  19. package/app/node_modules/.vite/deps/astro___cssesc.js.map +7 -0
  20. package/app/node_modules/.vite/deps/chunk-55ZOATU5.js +305 -0
  21. package/app/node_modules/.vite/deps/chunk-55ZOATU5.js.map +7 -0
  22. package/app/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  23. package/app/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  24. package/app/node_modules/.vite/deps/chunk-FEZZJEG2.js +6935 -0
  25. package/app/node_modules/.vite/deps/chunk-FEZZJEG2.js.map +7 -0
  26. package/app/node_modules/.vite/deps/package.json +3 -0
  27. package/app/node_modules/.vite/deps/react-dom.js +6 -0
  28. package/app/node_modules/.vite/deps/react-dom.js.map +7 -0
  29. package/app/node_modules/.vite/deps/react.js +5 -0
  30. package/app/node_modules/.vite/deps/react.js.map +7 -0
  31. package/app/node_modules/.vite/deps/react_jsx-dev-runtime.js +39 -0
  32. package/app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  33. package/app/node_modules/.vite/deps/react_jsx-runtime.js +57 -0
  34. package/app/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  35. package/app/src/components/DocsMobileNav.tsx +124 -0
  36. package/app/src/components/DocsSearch.tsx +315 -0
  37. package/app/src/components/DocsSidebar.astro +46 -0
  38. package/app/src/components/DocsTableOfContents.tsx +92 -0
  39. package/app/src/components/Navbar.astro +39 -0
  40. package/app/src/components/SocialIcon.astro +54 -0
  41. package/app/src/components/ThemeToggle.tsx +62 -0
  42. package/app/src/content.config.ts +17 -0
  43. package/app/src/env.d.ts +7 -0
  44. package/app/src/integrations/open-doc-config.mjs +65 -0
  45. package/app/src/layouts/DocsLayout.astro +369 -0
  46. package/app/src/lib/config.ts +36 -0
  47. package/app/src/lib/navigation.ts +68 -0
  48. package/app/src/lib/withBase.ts +11 -0
  49. package/app/src/pages/404.astro +24 -0
  50. package/app/src/pages/[...slug].astro +34 -0
  51. package/app/src/pages/index.astro +107 -0
  52. package/app/src/styles/global.css +324 -0
  53. package/app/tailwind.config.mjs +53 -0
  54. package/app/tsconfig.json +11 -0
  55. package/bin/open-doc.js +2 -0
  56. package/dist/chunk-BRUM67K7.js +30 -0
  57. package/dist/cli.d.ts +2 -0
  58. package/dist/cli.js +268 -0
  59. package/dist/index.d.ts +116 -0
  60. package/dist/index.js +8 -0
  61. package/package.json +77 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,6 @@
1
+ import {
2
+ require_react_dom
3
+ } from "./chunk-FEZZJEG2.js";
4
+ import "./chunk-55ZOATU5.js";
5
+ import "./chunk-5WRI5ZAA.js";
6
+ export default require_react_dom();
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ import {
2
+ require_react
3
+ } from "./chunk-55ZOATU5.js";
4
+ import "./chunk-5WRI5ZAA.js";
5
+ export default require_react();
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -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 &ldquo;{query}&rdquo;
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>