@openlaboratory/open-doc 0.1.2 → 0.1.4
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 +2 -2
- package/app/src/components/DocsMobileNav.tsx +3 -0
- package/app/src/components/DocsSearch.tsx +206 -132
- package/app/src/components/DocsTableOfContents.tsx +3 -0
- package/app/src/components/ThemeToggle.tsx +3 -0
- package/app/src/integrations/open-doc-config.mjs +56 -14
- package/app/src/layouts/DocsLayout.astro +14 -2
- package/app/src/lib/reactRenderer.ts +12 -0
- package/app/src/styles/global.css +30 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -83,8 +83,8 @@ Set these in `open-doc.config.ts`:
|
|
|
83
83
|
The full guide and a live example live in the
|
|
84
84
|
[project repository](https://github.com/openlaboratory-org/open-doc).
|
|
85
85
|
|
|
86
|
-
>
|
|
87
|
-
>
|
|
86
|
+
> `open-doc` provides the React runtime for MDX components. TSX authoring and
|
|
87
|
+
> `open-doc check` still need `@types/react` and `@types/react-dom` in your project.
|
|
88
88
|
|
|
89
89
|
## License
|
|
90
90
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import type { DocSection } from '../lib/navigation'
|
|
3
|
+
import { tagReactComponent } from '../lib/reactRenderer'
|
|
3
4
|
import { withBase } from '../lib/withBase'
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -122,3 +123,5 @@ export function DocsMobileNav({ navigation, currentSlug }: Props) {
|
|
|
122
123
|
</>
|
|
123
124
|
)
|
|
124
125
|
}
|
|
126
|
+
|
|
127
|
+
tagReactComponent(DocsMobileNav)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import Fuse from 'fuse.js'
|
|
3
4
|
import type { DocSection, DocPage } from '../lib/navigation'
|
|
5
|
+
import { tagReactComponent } from '../lib/reactRenderer'
|
|
4
6
|
import { withBase } from '../lib/withBase'
|
|
5
7
|
|
|
6
8
|
interface Props {
|
|
@@ -26,6 +28,10 @@ function isEditableTarget(target: EventTarget | null): boolean {
|
|
|
26
28
|
)
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
function isVisibleElement(element: HTMLElement | null): boolean {
|
|
32
|
+
return !!element && element.getClientRects().length > 0
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* ⌘K search modal. Uses Pagefind full-text search against the built site, and
|
|
31
37
|
* gracefully falls back to a lightweight title/description search (Fuse.js) when
|
|
@@ -34,6 +40,10 @@ function isEditableTarget(target: EventTarget | null): boolean {
|
|
|
34
40
|
*/
|
|
35
41
|
export function DocsSearch({ navigation }: Props) {
|
|
36
42
|
const allPages = useMemo<DocPage[]>(() => navigation.flatMap((s) => s.pages), [navigation])
|
|
43
|
+
const indexedSections = useMemo(
|
|
44
|
+
() => navigation.filter((section) => section.pages.length > 0).slice(0, 3),
|
|
45
|
+
[navigation],
|
|
46
|
+
)
|
|
37
47
|
const fuse = useMemo(
|
|
38
48
|
() => new Fuse(allPages, { keys: ['title', 'description'], threshold: 0.4 }),
|
|
39
49
|
[allPages],
|
|
@@ -55,10 +65,13 @@ export function DocsSearch({ navigation }: Props) {
|
|
|
55
65
|
|
|
56
66
|
useEffect(() => {
|
|
57
67
|
const handleKey = (e: KeyboardEvent) => {
|
|
58
|
-
|
|
68
|
+
const key = e.key.toLowerCase()
|
|
69
|
+
if ((e.metaKey || e.ctrlKey) && key === 'k') {
|
|
70
|
+
if (!isVisibleElement(triggerRef.current)) return
|
|
59
71
|
e.preventDefault()
|
|
60
72
|
setOpen(true)
|
|
61
73
|
} else if (e.key === '/' && !isEditableTarget(e.target)) {
|
|
74
|
+
if (!isVisibleElement(triggerRef.current)) return
|
|
62
75
|
e.preventDefault()
|
|
63
76
|
setOpen(true)
|
|
64
77
|
} else if (e.key === 'Escape') {
|
|
@@ -152,6 +165,195 @@ export function DocsSearch({ navigation }: Props) {
|
|
|
152
165
|
}
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
const hasQuery = query.trim().length > 0
|
|
169
|
+
const pageCountLabel = `${allPages.length} ${allPages.length === 1 ? 'page' : 'pages'}`
|
|
170
|
+
|
|
171
|
+
const searchDialog = open ? (
|
|
172
|
+
<div
|
|
173
|
+
className="fixed inset-0 z-[1000] flex items-start justify-center px-4 pt-[15vh]"
|
|
174
|
+
onClick={(e) => {
|
|
175
|
+
if (!dialogRef.current?.contains(e.target as Node)) setOpen(false)
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
|
179
|
+
|
|
180
|
+
<div
|
|
181
|
+
ref={dialogRef}
|
|
182
|
+
className="relative w-full max-w-xl overflow-hidden rounded-xl border border-foreground/[0.12] bg-surface-raised shadow-2xl"
|
|
183
|
+
role="dialog"
|
|
184
|
+
aria-modal="true"
|
|
185
|
+
aria-label="Search documentation"
|
|
186
|
+
>
|
|
187
|
+
<div className="flex items-center gap-3 border-b border-foreground/[0.08] px-4 py-3">
|
|
188
|
+
<svg
|
|
189
|
+
className="h-4 w-4 shrink-0 text-foreground/40"
|
|
190
|
+
fill="none"
|
|
191
|
+
viewBox="0 0 24 24"
|
|
192
|
+
stroke="currentColor"
|
|
193
|
+
strokeWidth={2}
|
|
194
|
+
>
|
|
195
|
+
<path
|
|
196
|
+
strokeLinecap="round"
|
|
197
|
+
strokeLinejoin="round"
|
|
198
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
199
|
+
/>
|
|
200
|
+
</svg>
|
|
201
|
+
<input
|
|
202
|
+
ref={inputRef}
|
|
203
|
+
type="text"
|
|
204
|
+
placeholder="Search documentation…"
|
|
205
|
+
value={query}
|
|
206
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
207
|
+
onKeyDown={handleKeyDown}
|
|
208
|
+
role="combobox"
|
|
209
|
+
aria-expanded={hasQuery && results.length > 0}
|
|
210
|
+
aria-controls="od-search-results"
|
|
211
|
+
aria-activedescendant={
|
|
212
|
+
hasQuery && results[selectedIndex] ? `od-search-option-${selectedIndex}` : undefined
|
|
213
|
+
}
|
|
214
|
+
className="flex-1 bg-transparent text-sm text-foreground placeholder-foreground/30 outline-none"
|
|
215
|
+
/>
|
|
216
|
+
<button
|
|
217
|
+
onClick={() => setOpen(false)}
|
|
218
|
+
className="rounded border border-foreground/[0.1] px-1.5 py-0.5 text-[11px] text-foreground/40 transition-colors hover:text-foreground/70"
|
|
219
|
+
>
|
|
220
|
+
Esc
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div
|
|
225
|
+
className="max-h-[360px] overflow-y-auto py-2"
|
|
226
|
+
id="od-search-results"
|
|
227
|
+
role={hasQuery ? 'listbox' : undefined}
|
|
228
|
+
>
|
|
229
|
+
{!hasQuery ? (
|
|
230
|
+
<div className="px-4 py-3">
|
|
231
|
+
<div className="flex items-center gap-3 rounded-lg border border-foreground/[0.08] bg-foreground/[0.025] px-3 py-2.5">
|
|
232
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-link/15 bg-link/10 text-link">
|
|
233
|
+
<svg
|
|
234
|
+
className="h-4 w-4"
|
|
235
|
+
fill="none"
|
|
236
|
+
viewBox="0 0 24 24"
|
|
237
|
+
stroke="currentColor"
|
|
238
|
+
strokeWidth={1.8}
|
|
239
|
+
>
|
|
240
|
+
<path
|
|
241
|
+
strokeLinecap="round"
|
|
242
|
+
strokeLinejoin="round"
|
|
243
|
+
d="M7 7h6M7 11h4m2 8H6a2 2 0 01-2-2V5a2 2 0 012-2h8l4 4v3.5"
|
|
244
|
+
/>
|
|
245
|
+
<path
|
|
246
|
+
strokeLinecap="round"
|
|
247
|
+
strokeLinejoin="round"
|
|
248
|
+
d="M15.5 17.5l3 3m1-5a4 4 0 11-8 0 4 4 0 018 0z"
|
|
249
|
+
/>
|
|
250
|
+
</svg>
|
|
251
|
+
</div>
|
|
252
|
+
<div className="min-w-0 flex-1">
|
|
253
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
254
|
+
<span className="text-sm font-medium text-foreground/75">{pageCountLabel}</span>
|
|
255
|
+
<span className="text-xs text-foreground/35">available</span>
|
|
256
|
+
</div>
|
|
257
|
+
{indexedSections.length > 0 && (
|
|
258
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
259
|
+
{indexedSections.map((section) => (
|
|
260
|
+
<span
|
|
261
|
+
key={section.label}
|
|
262
|
+
className="rounded border border-foreground/[0.08] bg-surface-raised px-1.5 py-0.5 text-[11px] font-medium text-foreground/50"
|
|
263
|
+
>
|
|
264
|
+
{section.label}
|
|
265
|
+
</span>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
) : results.length === 0 ? (
|
|
273
|
+
<p className="px-4 py-8 text-center text-sm text-foreground/40">
|
|
274
|
+
No results for “{query}”
|
|
275
|
+
</p>
|
|
276
|
+
) : (
|
|
277
|
+
results.map((result, i) => {
|
|
278
|
+
const isSelected = i === selectedIndex
|
|
279
|
+
return (
|
|
280
|
+
<a
|
|
281
|
+
key={result.url + i}
|
|
282
|
+
id={`od-search-option-${i}`}
|
|
283
|
+
role="option"
|
|
284
|
+
aria-selected={isSelected}
|
|
285
|
+
href={result.url}
|
|
286
|
+
onClick={() => setOpen(false)}
|
|
287
|
+
onMouseEnter={() => setSelectedIndex(i)}
|
|
288
|
+
className={[
|
|
289
|
+
'flex items-start gap-3 px-4 py-2.5 transition-colors',
|
|
290
|
+
isSelected ? 'bg-foreground/[0.08]' : 'hover:bg-foreground/[0.04]',
|
|
291
|
+
].join(' ')}
|
|
292
|
+
>
|
|
293
|
+
<svg
|
|
294
|
+
className="mt-0.5 h-4 w-4 shrink-0 text-foreground/30"
|
|
295
|
+
fill="none"
|
|
296
|
+
viewBox="0 0 24 24"
|
|
297
|
+
stroke="currentColor"
|
|
298
|
+
strokeWidth={2}
|
|
299
|
+
>
|
|
300
|
+
<path
|
|
301
|
+
strokeLinecap="round"
|
|
302
|
+
strokeLinejoin="round"
|
|
303
|
+
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"
|
|
304
|
+
/>
|
|
305
|
+
</svg>
|
|
306
|
+
<div className="min-w-0">
|
|
307
|
+
<div className="text-sm font-medium leading-snug text-foreground/90">
|
|
308
|
+
{result.title}
|
|
309
|
+
</div>
|
|
310
|
+
{result.excerpt &&
|
|
311
|
+
(result.html ? (
|
|
312
|
+
<div
|
|
313
|
+
className="search-excerpt mt-0.5 line-clamp-2 text-xs text-foreground/45"
|
|
314
|
+
dangerouslySetInnerHTML={{ __html: result.excerpt }}
|
|
315
|
+
/>
|
|
316
|
+
) : (
|
|
317
|
+
<div className="mt-0.5 truncate text-xs text-foreground/45">
|
|
318
|
+
{result.excerpt}
|
|
319
|
+
</div>
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
322
|
+
</a>
|
|
323
|
+
)
|
|
324
|
+
})
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-foreground/[0.06] px-4 py-3 text-[13px] text-foreground/50">
|
|
329
|
+
<span className="flex items-center gap-1.5 whitespace-nowrap">
|
|
330
|
+
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-foreground/[0.14] bg-foreground/[0.04] px-2 font-mono text-xs leading-none text-foreground/65">
|
|
331
|
+
↑↓
|
|
332
|
+
</kbd>
|
|
333
|
+
navigate
|
|
334
|
+
</span>
|
|
335
|
+
<span className="flex items-center gap-1.5 whitespace-nowrap">
|
|
336
|
+
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-foreground/[0.14] bg-foreground/[0.04] px-2 font-mono text-xs leading-none text-foreground/65">
|
|
337
|
+
↵
|
|
338
|
+
</kbd>
|
|
339
|
+
open
|
|
340
|
+
</span>
|
|
341
|
+
<span className="flex items-center gap-1.5 whitespace-nowrap">
|
|
342
|
+
<kbd className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-foreground/[0.14] bg-foreground/[0.04] px-2 font-mono text-xs leading-none text-foreground/65">
|
|
343
|
+
Esc
|
|
344
|
+
</kbd>
|
|
345
|
+
close
|
|
346
|
+
</span>
|
|
347
|
+
{!fullText && (
|
|
348
|
+
<span className="text-foreground/40 sm:ml-auto">
|
|
349
|
+
Title search · run a build for full-text
|
|
350
|
+
</span>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
) : null
|
|
356
|
+
|
|
155
357
|
return (
|
|
156
358
|
<>
|
|
157
359
|
<button
|
|
@@ -179,137 +381,9 @@ export function DocsSearch({ navigation }: Props) {
|
|
|
179
381
|
</kbd>
|
|
180
382
|
</button>
|
|
181
383
|
|
|
182
|
-
{
|
|
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
|
-
)}
|
|
384
|
+
{searchDialog && createPortal(searchDialog, document.body)}
|
|
313
385
|
</>
|
|
314
386
|
)
|
|
315
387
|
}
|
|
388
|
+
|
|
389
|
+
tagReactComponent(DocsSearch)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { tagReactComponent } from '../lib/reactRenderer'
|
|
2
3
|
|
|
3
4
|
interface Heading {
|
|
4
5
|
depth: number
|
|
@@ -90,3 +91,5 @@ export function DocsTableOfContents({ headings }: Props) {
|
|
|
90
91
|
</nav>
|
|
91
92
|
)
|
|
92
93
|
}
|
|
94
|
+
|
|
95
|
+
tagReactComponent(DocsTableOfContents)
|
|
@@ -1,16 +1,66 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
2
4
|
|
|
3
5
|
// The bundled Astro app this integration ships inside, e.g.
|
|
4
6
|
// `…/node_modules/@openlaboratory/open-doc/app`. Its `src/` holds the React
|
|
5
7
|
// island components Vite must be allowed to serve in dev.
|
|
6
8
|
const APP_ROOT = fileURLToPath(new URL('../../', import.meta.url))
|
|
7
9
|
|
|
10
|
+
// Resolve specifiers from open-doc's own install (not the consumer's).
|
|
11
|
+
const require = createRequire(import.meta.url)
|
|
12
|
+
const ASTRO_ROOT = dirname(require.resolve('astro/package.json'))
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Astro's static build emits an SSR server bundle into the consumer's `dist/`
|
|
16
|
+
* and then runs it with Node to prerender the pages. That bundle keeps open-doc's
|
|
17
|
+
* runtime deps (Astro, the React renderer, and their transitive deps like
|
|
18
|
+
* `clsx`/`piccolore`) as bare `import` specifiers — but those live in open-doc's
|
|
19
|
+
* own `node_modules`, not the consumer's, so under a strict (non-hoisted) install
|
|
20
|
+
* Node can't resolve them from `dist/` and prerendering fails.
|
|
21
|
+
*
|
|
22
|
+
* This plugin rewrites those bare specifiers to absolute paths resolved from
|
|
23
|
+
* open-doc's install, making the build self-contained under any package manager
|
|
24
|
+
* or workspace layout — without consumer-side hoisting. It runs in `renderChunk`
|
|
25
|
+
* (after Astro's module graph is fully resolved) so Astro's own resolution and
|
|
26
|
+
* virtual modules (`astro:*`) are left untouched. Client chunks bundle their
|
|
27
|
+
* deps, so they have nothing bare to rewrite.
|
|
28
|
+
*
|
|
29
|
+
* @returns {import('vite').Plugin}
|
|
30
|
+
*/
|
|
31
|
+
function absolutizeSsrExternals() {
|
|
32
|
+
const rewrite = (_match, pre, quote, spec) => {
|
|
33
|
+
if (/^[./]/.test(spec) || spec.startsWith('\0') || spec.includes(':')) return _match
|
|
34
|
+
try {
|
|
35
|
+
return pre + quote + require.resolve(spec) + quote
|
|
36
|
+
} catch {
|
|
37
|
+
return _match
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
name: 'open-doc:absolutize-ssr-externals',
|
|
42
|
+
renderChunk(code) {
|
|
43
|
+
// Only the SSR build emits bare external imports; skip the client build.
|
|
44
|
+
if (this.environment && this.environment.name !== 'ssr') return null
|
|
45
|
+
const out = code
|
|
46
|
+
// `import … from '<spec>'` / `export … from '<spec>'`
|
|
47
|
+
.replace(/^(\s*(?:import|export)\b[^\n'"]*?\bfrom\s*)(['"])([^'"]+)\2/gm, rewrite)
|
|
48
|
+
// bare side-effect `import '<spec>'`
|
|
49
|
+
.replace(/^(\s*import\s+)(['"])([^'"]+)\2/gm, rewrite)
|
|
50
|
+
// dynamic `import('<spec>')`
|
|
51
|
+
.replace(/(\bimport\s*\(\s*)(['"])([^'"]+)\2/g, rewrite)
|
|
52
|
+
return out === code ? null : { code: out }
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
8
57
|
/**
|
|
9
58
|
* Internal Astro integration that wires the consumer's project into the bundled
|
|
10
59
|
* app:
|
|
11
60
|
* • exposes the resolved config as the `virtual:open-doc-config` module
|
|
12
61
|
* • aliases `@docs` to the consumer's content directory
|
|
13
|
-
* • allows Vite to read
|
|
62
|
+
* • allows Vite to read the app's island sources + the consumer's content/public dirs
|
|
63
|
+
* • makes the production build self-contained (see absolutizeSsrExternals)
|
|
14
64
|
*
|
|
15
65
|
* The resolved config is passed from the CLI via OPEN_DOC_CONFIG_JSON.
|
|
16
66
|
*
|
|
@@ -21,31 +71,23 @@ export function openDocConfig({ contentDir, publicDir } = {}) {
|
|
|
21
71
|
const configJson = process.env.OPEN_DOC_CONFIG_JSON || '{}'
|
|
22
72
|
// The app lives in node_modules (under pnpm, behind a symlinked .pnpm path),
|
|
23
73
|
// so its island sources fall outside Vite's default fs.allow and 404 in dev.
|
|
24
|
-
// Allow the app root
|
|
25
|
-
|
|
74
|
+
// Allow the app root, Astro's dev-toolbar client, and the consumer's
|
|
75
|
+
// content/public dirs explicitly.
|
|
76
|
+
const allow = [APP_ROOT, ASTRO_ROOT, contentDir, publicDir].filter(Boolean)
|
|
26
77
|
|
|
27
78
|
return {
|
|
28
79
|
name: 'open-doc:config',
|
|
29
80
|
hooks: {
|
|
30
81
|
'astro:config:setup': ({ command, updateConfig }) => {
|
|
31
|
-
|
|
32
|
-
// output so the build is self-contained under every package manager
|
|
33
|
-
// (they live in open-doc's node_modules, not the consumer's). Bundling
|
|
34
|
-
// *everything* (`noExternal: true`) breaks `astro sync` on a CommonJS
|
|
35
|
-
// dep, so we target the specific packages. In `dev` they must stay
|
|
36
|
-
// external — bundling CommonJS React breaks the dev SSR runtime.
|
|
37
|
-
const noExternal =
|
|
38
|
-
command === 'build' ? ['fuse.js', 'react', 'react-dom', '@astrojs/react'] : []
|
|
39
|
-
|
|
82
|
+
const buildPlugins = command === 'build' ? [absolutizeSsrExternals()] : []
|
|
40
83
|
updateConfig({
|
|
41
84
|
vite: {
|
|
42
|
-
plugins: [virtualConfigPlugin(configJson)],
|
|
85
|
+
plugins: [virtualConfigPlugin(configJson), ...buildPlugins],
|
|
43
86
|
resolve: {
|
|
44
87
|
alias: contentDir ? { '@docs': contentDir } : {},
|
|
45
88
|
// One React instance, even if a consumer installs their own for MDX.
|
|
46
89
|
dedupe: ['react', 'react-dom'],
|
|
47
90
|
},
|
|
48
|
-
ssr: { noExternal },
|
|
49
91
|
server: { fs: { allow } },
|
|
50
92
|
},
|
|
51
93
|
})
|
|
@@ -335,9 +335,21 @@ const darkVars = toVars(config.theme.dark)
|
|
|
335
335
|
const prose = document.querySelector('.docs-prose')
|
|
336
336
|
if (!prose) return
|
|
337
337
|
prose.querySelectorAll('pre').forEach((pre) => {
|
|
338
|
-
|
|
338
|
+
const existingWrapper = pre.parentElement?.classList.contains('code-block')
|
|
339
|
+
? pre.parentElement
|
|
340
|
+
: null
|
|
341
|
+
if (existingWrapper?.querySelector('.code-copy-btn')) return
|
|
342
|
+
|
|
343
|
+
const wrapper = existingWrapper ?? document.createElement('div')
|
|
344
|
+
if (!existingWrapper) {
|
|
345
|
+
wrapper.className = 'code-block'
|
|
346
|
+
pre.parentNode?.insertBefore(wrapper, pre)
|
|
347
|
+
wrapper.appendChild(pre)
|
|
348
|
+
}
|
|
349
|
+
|
|
339
350
|
const btn = document.createElement('button')
|
|
340
351
|
btn.className = 'code-copy-btn'
|
|
352
|
+
btn.type = 'button'
|
|
341
353
|
btn.setAttribute('aria-label', 'Copy code')
|
|
342
354
|
btn.innerHTML = clipboardIcon
|
|
343
355
|
btn.addEventListener('click', () => {
|
|
@@ -352,7 +364,7 @@ const darkVars = toVars(config.theme.dark)
|
|
|
352
364
|
}, 2000)
|
|
353
365
|
})
|
|
354
366
|
})
|
|
355
|
-
|
|
367
|
+
wrapper.appendChild(btn)
|
|
356
368
|
})
|
|
357
369
|
}
|
|
358
370
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const ASTRO_RENDERER = Symbol.for('astro:renderer')
|
|
2
|
+
|
|
3
|
+
export function tagReactComponent<T extends (...args: any[]) => unknown>(Component: T): T {
|
|
4
|
+
if (!(ASTRO_RENDERER in Component)) {
|
|
5
|
+
Object.defineProperty(Component, ASTRO_RENDERER, {
|
|
6
|
+
value: '@astrojs/react',
|
|
7
|
+
enumerable: false,
|
|
8
|
+
writable: false,
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
return Component
|
|
12
|
+
}
|
|
@@ -155,12 +155,33 @@
|
|
|
155
155
|
content: none;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
.docs-prose kbd {
|
|
159
|
+
border: 1px solid hsl(var(--foreground) / 0.16);
|
|
160
|
+
border-radius: 0.375rem;
|
|
161
|
+
background: hsl(var(--surface-raised));
|
|
162
|
+
box-shadow:
|
|
163
|
+
0 1px 0 hsl(var(--foreground) / 0.12),
|
|
164
|
+
inset 0 -1px 0 hsl(var(--foreground) / 0.08);
|
|
165
|
+
color: hsl(var(--foreground) / 0.88);
|
|
166
|
+
}
|
|
167
|
+
|
|
158
168
|
.docs-prose pre {
|
|
159
169
|
border: 1px solid hsl(var(--code-border));
|
|
160
170
|
border-radius: 0.625rem;
|
|
161
171
|
position: relative;
|
|
162
172
|
}
|
|
163
173
|
|
|
174
|
+
.docs-prose .code-block {
|
|
175
|
+
position: relative;
|
|
176
|
+
margin-top: 1.7142857em;
|
|
177
|
+
margin-bottom: 1.7142857em;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.docs-prose .code-block pre {
|
|
181
|
+
margin-top: 0;
|
|
182
|
+
margin-bottom: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
164
185
|
.docs-prose blockquote {
|
|
165
186
|
border-left: 2px solid hsl(var(--border));
|
|
166
187
|
background: hsl(var(--foreground) / 0.03);
|
|
@@ -240,6 +261,13 @@
|
|
|
240
261
|
position: relative;
|
|
241
262
|
}
|
|
242
263
|
|
|
264
|
+
.docs-prose h1[id],
|
|
265
|
+
.docs-prose h2[id],
|
|
266
|
+
.docs-prose h3[id],
|
|
267
|
+
.docs-prose h4[id] {
|
|
268
|
+
scroll-margin-top: calc(57px + 1rem);
|
|
269
|
+
}
|
|
270
|
+
|
|
243
271
|
.docs-prose .heading-anchor {
|
|
244
272
|
position: absolute;
|
|
245
273
|
right: 100%;
|
|
@@ -289,7 +317,8 @@
|
|
|
289
317
|
background 0.15s;
|
|
290
318
|
}
|
|
291
319
|
|
|
292
|
-
.docs-prose
|
|
320
|
+
.docs-prose .code-block:hover .code-copy-btn,
|
|
321
|
+
.code-copy-btn:focus-visible {
|
|
293
322
|
opacity: 1;
|
|
294
323
|
}
|
|
295
324
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlaboratory/open-doc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A polished, GitBook-style documentation-site framework. Write Markdown, run one command, get a beautiful static site.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"documentation",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"type": "module",
|
|
27
27
|
"bin": {
|
|
28
|
-
"open-doc": "
|
|
28
|
+
"open-doc": "bin/open-doc.js"
|
|
29
29
|
},
|
|
30
30
|
"main": "./dist/index.js",
|
|
31
31
|
"types": "./dist/index.d.ts",
|