@clidey/dory 1.0.3 → 1.0.6
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/bin/dist/dory.js +36 -2
- package/bin/dist/dory.js.map +1 -1
- package/index.html +0 -1
- package/package.json +27 -31
- package/scripts/check-trigger-children.mjs +60 -0
- package/scripts/trigger-children-lib.mjs +149 -0
- package/scripts/trigger-children.json +47 -0
- package/src/components/hooks.tsx +2 -1
- package/src/components/store.ts +5 -4
- package/src/config/mdx.js +8 -3
- package/src/config/mdx.ts +8 -3
- package/src/embed/widget-iframe.ts +10 -3
- package/src/entry-server.tsx +11 -7
- package/src/main.tsx +5 -1
- package/src/mdx/accordion.tsx +11 -8
- package/src/mdx/api.tsx +2 -352
- package/src/mdx/async-api.tsx +124 -295
- package/src/mdx/callout.tsx +22 -24
- package/src/mdx/card.tsx +20 -22
- package/src/mdx/chip.tsx +14 -0
- package/src/mdx/code.tsx +133 -110
- package/src/mdx/expandable.tsx +8 -8
- package/src/mdx/fence-meta.test.ts +39 -0
- package/src/mdx/fence-meta.ts +61 -0
- package/src/mdx/fence.tsx +155 -44
- package/src/mdx/icon-grid.tsx +29 -0
- package/src/mdx/icon.tsx +2 -2
- package/src/mdx/latex.tsx +14 -7
- package/src/mdx/list.tsx +3 -3
- package/src/mdx/mdx.tsx +7 -1
- package/src/mdx/mermaid.tsx +81 -11
- package/src/mdx/open-api.tsx +396 -239
- package/src/mdx/source.tsx +148 -40
- package/src/mdx/specimen.tsx +69 -0
- package/src/mdx/step.tsx +22 -30
- package/src/mdx/table.tsx +4 -9
- package/src/mdx/tabs.tsx +39 -0
- package/src/mdx/tag.tsx +13 -21
- package/src/mdx/text.tsx +17 -9
- package/src/mdx/type-scale.tsx +28 -0
- package/src/mdx/websocket-playground.tsx +221 -704
- package/src/plugins/code-meta.ts +23 -0
- package/src/plugins/frontmatter-dev-server.ts +6 -2
- package/src/plugins/html-files-middleware.ts +9 -2
- package/src/plugins/html-metadata-injector.ts +4 -3
- package/src/plugins/llm-txt-dev-server.ts +5 -2
- package/src/plugins/llm-txt-generator.ts +2 -1
- package/src/plugins/prerender.ts +7 -4
- package/src/plugins/relative-assets.test.ts +26 -0
- package/src/plugins/relative-assets.ts +80 -0
- package/src/plugins/robots-generator.ts +6 -4
- package/src/plugins/sanitize.test.ts +5 -0
- package/src/plugins/sanitize.ts +2 -0
- package/src/plugins/shared.ts +17 -0
- package/src/plugins/sitemap-generator.ts +2 -1
- package/src/theme.scss +282 -3
- package/src/types/config.ts +3 -0
- package/src/ui/header.tsx +4 -3
- package/src/ui/layout.tsx +22 -4
- package/src/ui/page-meta.tsx +37 -0
- package/src/ui/prev-next-link.tsx +4 -3
- package/src/ui/table-of-content.tsx +11 -3
- package/src/utils/base-path.test.ts +46 -0
- package/src/utils/base-path.ts +41 -0
- package/vite.config.ssr.ts +3 -0
- package/vite.config.ts +3 -0
|
@@ -81,12 +81,19 @@ export class DoryWidget {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const url = new URL(baseUrl);
|
|
84
|
-
url.pathname = path;
|
|
84
|
+
url.pathname = this.resolveDocsPath(path);
|
|
85
85
|
url.search = params.toString();
|
|
86
86
|
|
|
87
87
|
return url.toString();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// The docs may live under a path prefix (baseUrl is derived from the
|
|
91
|
+
// embed.js script URL), so routes are resolved beneath that prefix.
|
|
92
|
+
private resolveDocsPath(path: string): string {
|
|
93
|
+
const basePath = new URL(this.config.baseUrl).pathname.replace(/\/+$/, '');
|
|
94
|
+
return `${basePath}${path.startsWith('/') ? path : `/${path}`}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
90
97
|
private setupIframeMessaging() {
|
|
91
98
|
window.addEventListener('message', (event) => {
|
|
92
99
|
// Verify origin
|
|
@@ -190,7 +197,7 @@ export class DoryWidget {
|
|
|
190
197
|
// Update iframe URL if path is specified
|
|
191
198
|
if (path && this.iframe) {
|
|
192
199
|
const url = new URL(this.iframe.src);
|
|
193
|
-
url.pathname = path;
|
|
200
|
+
url.pathname = this.resolveDocsPath(path);
|
|
194
201
|
this.iframe.src = url.toString();
|
|
195
202
|
}
|
|
196
203
|
|
|
@@ -240,7 +247,7 @@ export class DoryWidget {
|
|
|
240
247
|
navigate(path: string) {
|
|
241
248
|
if (this.iframe) {
|
|
242
249
|
const url = new URL(this.iframe.src);
|
|
243
|
-
url.pathname = path;
|
|
250
|
+
url.pathname = this.resolveDocsPath(path);
|
|
244
251
|
this.iframe.src = url.toString();
|
|
245
252
|
this.emit('navigate', path);
|
|
246
253
|
}
|
package/src/entry-server.tsx
CHANGED
|
@@ -6,6 +6,8 @@ import { ThemeProvider } from '@clidey/ux';
|
|
|
6
6
|
import * as mdxComponents from './mdx/mdx';
|
|
7
7
|
import docsConfig from '../docs/dory.json' with { type: 'json' };
|
|
8
8
|
import type { DoryConfig } from './types/config';
|
|
9
|
+
import { PageMeta } from './ui/page-meta';
|
|
10
|
+
import { BASE_PATH, assetUrl, withBase } from './utils/base-path';
|
|
9
11
|
|
|
10
12
|
const config = docsConfig as DoryConfig;
|
|
11
13
|
|
|
@@ -95,7 +97,7 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
95
97
|
|
|
96
98
|
return renderToString(
|
|
97
99
|
<ThemeProvider>
|
|
98
|
-
<Router ssrPath={routePath}>
|
|
100
|
+
<Router base={BASE_PATH} ssrPath={withBase(routePath)}>
|
|
99
101
|
<MDXProvider components={mdxComponents}>
|
|
100
102
|
<div class="flex w-full flex-col">
|
|
101
103
|
{/* SYNC NOTE: the static shell below (header chrome, sidebar nav skeleton,
|
|
@@ -111,8 +113,8 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
111
113
|
<div class="flex items-center grow gap-2">
|
|
112
114
|
{config.logo && (
|
|
113
115
|
<>
|
|
114
|
-
<img src={
|
|
115
|
-
<img src={
|
|
116
|
+
<img src={assetUrl(config.logo.light)} alt="logo" class="w-8 hidden dark:block" />
|
|
117
|
+
<img src={assetUrl(config.logo.dark)} alt="logo" class="w-8 block dark:hidden" />
|
|
116
118
|
</>
|
|
117
119
|
)}
|
|
118
120
|
<h1 class="text-base lg:text-2xl font-bold">{config.name}</h1>
|
|
@@ -124,7 +126,7 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
124
126
|
return (
|
|
125
127
|
<li key={tab.title}>
|
|
126
128
|
<a
|
|
127
|
-
href={tab.groups[0].pages[0].href}
|
|
129
|
+
href={withBase(tab.groups[0].pages[0].href)}
|
|
128
130
|
class={`text-xs transition hover:opacity-80${isActive ? ' text-brand-foreground' : ''}`}
|
|
129
131
|
>
|
|
130
132
|
{tab.title}
|
|
@@ -157,7 +159,7 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
157
159
|
{group.pages.map(page => (
|
|
158
160
|
<li key={page.href} class="relative">
|
|
159
161
|
<a
|
|
160
|
-
href={page.href}
|
|
162
|
+
href={withBase(page.href)}
|
|
161
163
|
class={routePath === page.href
|
|
162
164
|
? 'block w-full pl-3.5 font-semibold text-brand-foreground before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full'
|
|
163
165
|
: 'block w-full pl-3.5 text-muted-foreground before:hidden before:bg-slate-300 hover:text-muted-foreground hover:before:block dark:text-muted-foreground dark:before:bg-slate-700 dark:hover:text-slate-300 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full'}
|
|
@@ -183,8 +185,10 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
183
185
|
<header class="mb-8">
|
|
184
186
|
{groupTitle && <h1 class="font-display text-lg tracking-tight">{groupTitle}</h1>}
|
|
185
187
|
{pageFm?.title && <p class="text-4xl tracking-tight">{pageFm.title}</p>}
|
|
188
|
+
{pageFm?.lead && <p class="dory-lead">{pageFm.lead}</p>}
|
|
186
189
|
</header>
|
|
187
190
|
</div>
|
|
191
|
+
<PageMeta meta={pageFm?.meta} />
|
|
188
192
|
<div class="flex-1 min-h-[calc(100vh-2rem)]">
|
|
189
193
|
{/* Matches App > div.antialiased > div.flex > Routes > Content */}
|
|
190
194
|
<div class="h-full antialiased">
|
|
@@ -199,7 +203,7 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
199
203
|
<div>
|
|
200
204
|
<p class="font-display text-sm font-medium">Previous</p>
|
|
201
205
|
<div class="mt-1">
|
|
202
|
-
<a href={prevPage.href} class="text-base font-semibold hover:opacity-80">{prevPage.title}</a>
|
|
206
|
+
<a href={withBase(prevPage.href)} class="text-base font-semibold hover:opacity-80">{prevPage.title}</a>
|
|
203
207
|
</div>
|
|
204
208
|
</div>
|
|
205
209
|
)}
|
|
@@ -207,7 +211,7 @@ export async function render(routePath: string, frontmatter?: Record<string, any
|
|
|
207
211
|
<div class="ml-auto text-right">
|
|
208
212
|
<p class="font-display text-sm font-medium">Next</p>
|
|
209
213
|
<div class="mt-1">
|
|
210
|
-
<a href={nextPage.href} class="text-base font-semibold hover:opacity-80">{nextPage.title}</a>
|
|
214
|
+
<a href={withBase(nextPage.href)} class="text-base font-semibold hover:opacity-80">{nextPage.title}</a>
|
|
211
215
|
</div>
|
|
212
216
|
</div>
|
|
213
217
|
)}
|
package/src/main.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { render, hydrate } from 'preact'
|
|
2
|
+
import { Router } from 'wouter-preact'
|
|
2
3
|
import '@clidey/ux/styles.css';
|
|
3
4
|
import '@clidey/ux/brand.css';
|
|
4
5
|
import { App } from './app.tsx'
|
|
@@ -12,6 +13,7 @@ import { ThemeProvider } from '@clidey/ux'
|
|
|
12
13
|
import { preloadFrontmatter } from './components/store'
|
|
13
14
|
import docsConfig from '../docs/dory.json' with { type: 'json' };
|
|
14
15
|
import type { DoryConfig } from './types/config';
|
|
16
|
+
import { BASE_PATH } from './utils/base-path';
|
|
15
17
|
|
|
16
18
|
const config = docsConfig as DoryConfig;
|
|
17
19
|
|
|
@@ -34,7 +36,9 @@ const tree = (
|
|
|
34
36
|
<ThemeProvider defaultTheme={defaultTheme} storageKey="@clidey/dory/theme">
|
|
35
37
|
<Toaster />
|
|
36
38
|
<ErrorBoundary>
|
|
37
|
-
<
|
|
39
|
+
<Router base={BASE_PATH}>
|
|
40
|
+
<AppWithMDX />
|
|
41
|
+
</Router>
|
|
38
42
|
</ErrorBoundary>
|
|
39
43
|
</ThemeProvider>
|
|
40
44
|
)
|
package/src/mdx/accordion.tsx
CHANGED
|
@@ -4,11 +4,13 @@ import {
|
|
|
4
4
|
AccordionTrigger,
|
|
5
5
|
AccordionContent
|
|
6
6
|
} from '@clidey/ux'
|
|
7
|
+
import classNames from 'classnames'
|
|
8
|
+
import { Plus } from 'lucide-react'
|
|
7
9
|
import { Icon } from './icon'
|
|
8
10
|
|
|
9
|
-
export function AccordionGroup({ children }: { children: React.ReactNode }) {
|
|
11
|
+
export function AccordionGroup({ children, numbered = false }: { children: React.ReactNode; numbered?: boolean }) {
|
|
10
12
|
return (
|
|
11
|
-
<UXAccordion type="single" collapsible className=
|
|
13
|
+
<UXAccordion type="single" collapsible className={classNames('dory-accordion my-5 overflow-hidden rounded-lg border border-[var(--border)] bg-muted/40', { 'dory-accordion-numbered': numbered })}>
|
|
12
14
|
{children}
|
|
13
15
|
</UXAccordion>
|
|
14
16
|
)
|
|
@@ -24,15 +26,16 @@ export function Accordion({
|
|
|
24
26
|
icon?: React.ComponentProps<typeof Icon>['icon']
|
|
25
27
|
}) {
|
|
26
28
|
return (
|
|
27
|
-
<AccordionItem value={title}>
|
|
28
|
-
<AccordionTrigger className="text-left">
|
|
29
|
-
<div className="flex items-center gap-
|
|
30
|
-
{icon && <Icon icon={icon} className="h-5 w-5" />}
|
|
31
|
-
<span className="text-
|
|
29
|
+
<AccordionItem value={title} className="border-b border-[var(--border)] last:border-b-0">
|
|
30
|
+
<AccordionTrigger className="group items-center px-4 py-3 text-left hover:no-underline [&>svg:last-child]:hidden">
|
|
31
|
+
<div className="flex items-center gap-3">
|
|
32
|
+
{icon && <Icon icon={icon} className="h-3.5 w-3.5 !stroke-[var(--primary)] text-[var(--primary)]" />}
|
|
33
|
+
<span className="text-sm font-semibold">{title}</span>
|
|
32
34
|
</div>
|
|
35
|
+
<Plus aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-45" />
|
|
33
36
|
</AccordionTrigger>
|
|
34
37
|
<AccordionContent>
|
|
35
|
-
<div className="
|
|
38
|
+
<div className="px-4 pb-3 leading-relaxed text-muted-foreground">
|
|
36
39
|
{children}
|
|
37
40
|
</div>
|
|
38
41
|
</AccordionContent>
|
package/src/mdx/api.tsx
CHANGED
|
@@ -1,354 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Children,
|
|
5
|
-
createContext,
|
|
6
|
-
isValidElement,
|
|
7
|
-
useContext,
|
|
8
|
-
useEffect,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
} from 'react'
|
|
12
|
-
import { Tag } from './tag'
|
|
13
|
-
import { CheckIcon } from 'lucide-react'
|
|
14
|
-
import { Button, Card } from '@clidey/ux'
|
|
15
|
-
|
|
16
|
-
const languageNames: Record<string, string> = {
|
|
17
|
-
js: 'JavaScript',
|
|
18
|
-
ts: 'TypeScript',
|
|
19
|
-
javascript: 'JavaScript',
|
|
20
|
-
typescript: 'TypeScript',
|
|
21
|
-
php: 'PHP',
|
|
22
|
-
python: 'Python',
|
|
23
|
-
ruby: 'Ruby',
|
|
24
|
-
go: 'Go',
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getPanelTitle({
|
|
28
|
-
title,
|
|
29
|
-
language,
|
|
30
|
-
}: {
|
|
31
|
-
title?: string
|
|
32
|
-
language?: string
|
|
33
|
-
}) {
|
|
34
|
-
if (title) {
|
|
35
|
-
return title
|
|
36
|
-
}
|
|
37
|
-
if (language && language in languageNames) {
|
|
38
|
-
return languageNames[language]
|
|
39
|
-
}
|
|
40
|
-
return 'API'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
|
44
|
-
return (
|
|
45
|
-
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
|
46
|
-
<path
|
|
47
|
-
strokeWidth="0"
|
|
48
|
-
d="M5.5 13.5v-5a2 2 0 0 1 2-2l.447-.894A2 2 0 0 1 9.737 4.5h.527a2 2 0 0 1 1.789 1.106l.447.894a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2Z"
|
|
49
|
-
/>
|
|
50
|
-
<path
|
|
51
|
-
fill="none"
|
|
52
|
-
strokeLinejoin="round"
|
|
53
|
-
d="M12.5 6.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m5 0-.447-.894a2 2 0 0 0-1.79-1.106h-.527a2 2 0 0 0-1.789 1.106L7.5 6.5m5 0-1 1h-3l-1-1"
|
|
54
|
-
/>
|
|
55
|
-
</svg>
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function CopyButton({ code }: { code: string }) {
|
|
60
|
-
const [copyCount, setCopyCount] = useState(0)
|
|
61
|
-
const copied = copyCount > 0
|
|
62
|
-
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
if (copyCount > 0) {
|
|
65
|
-
const timeout = setTimeout(() => setCopyCount(0), 1000)
|
|
66
|
-
return () => {
|
|
67
|
-
clearTimeout(timeout)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}, [copyCount])
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<Button
|
|
74
|
-
className={classNames(
|
|
75
|
-
'group/button absolute top-3.5 right-4 overflow-hidden rounded-full py-1 pr-3 pl-2 text-2xs font-medium opacity-0 backdrop-blur-sm transition group-hover:opacity-100 focus:opacity-100',
|
|
76
|
-
copied
|
|
77
|
-
? 'bg-sky-400/10 ring-1 ring-sky-400/20 ring-inset'
|
|
78
|
-
: 'bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5',
|
|
79
|
-
)}
|
|
80
|
-
onClick={() => {
|
|
81
|
-
window.navigator.clipboard.writeText(code).then(() => {
|
|
82
|
-
setCopyCount((count) => count + 1)
|
|
83
|
-
})
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
86
|
-
<span
|
|
87
|
-
aria-hidden={copied}
|
|
88
|
-
className={classNames(
|
|
89
|
-
'pointer-events-none flex items-center gap-0.5 text-muted-foreground transition duration-300',
|
|
90
|
-
copied && '-translate-y-1.5 opacity-0',
|
|
91
|
-
)}
|
|
92
|
-
>
|
|
93
|
-
<ClipboardIcon className="h-4 w-4 fill-zinc-500/20 stroke-muted-foreground transition-colors group-hover/button:stroke-muted-foreground" />
|
|
94
|
-
</span>
|
|
95
|
-
<span
|
|
96
|
-
aria-hidden={!copied}
|
|
97
|
-
className={classNames(
|
|
98
|
-
'pointer-events-none absolute inset-0 flex items-center justify-center text-sky-400 transition duration-300',
|
|
99
|
-
!copied && 'translate-y-1.5 opacity-0',
|
|
100
|
-
)}
|
|
101
|
-
>
|
|
102
|
-
<CheckIcon className="h-4 w-4" />
|
|
103
|
-
</span>
|
|
104
|
-
</Button>
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function APIPanelHeader({ tag, label }: { tag?: string; label?: string }) {
|
|
109
|
-
if (!tag && !label) {
|
|
110
|
-
return null
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<div className="flex h-9 items-center gap-2 px-4">
|
|
115
|
-
{tag && (
|
|
116
|
-
<div className="dark flex">
|
|
117
|
-
<Tag variant="small">{tag}</Tag>
|
|
118
|
-
</div>
|
|
119
|
-
)}
|
|
120
|
-
{tag && label && (
|
|
121
|
-
<span className="h-0.5 w-0.5 rounded-full bg-zinc-500" />
|
|
122
|
-
)}
|
|
123
|
-
{label && (
|
|
124
|
-
<span className="font-mono text-xs text-muted-foreground">{label}</span>
|
|
125
|
-
)}
|
|
126
|
-
</div>
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function APIPanel({
|
|
131
|
-
children,
|
|
132
|
-
tag,
|
|
133
|
-
label,
|
|
134
|
-
code,
|
|
135
|
-
}: {
|
|
136
|
-
children: React.ReactNode
|
|
137
|
-
tag?: string
|
|
138
|
-
label?: string
|
|
139
|
-
code?: string
|
|
140
|
-
}) {
|
|
141
|
-
const child = Children.only(children)
|
|
142
|
-
|
|
143
|
-
if (isValidElement(child)) {
|
|
144
|
-
// @ts-expect-error child.props is not typed
|
|
145
|
-
tag = child.props.tag ?? tag
|
|
146
|
-
// @ts-expect-error child.props is not typed
|
|
147
|
-
label = child.props.label ?? label
|
|
148
|
-
// @ts-expect-error child.props is not typed
|
|
149
|
-
code = child.props.code ?? code
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<div className="group dark:bg-white/2.5">
|
|
154
|
-
<APIPanelHeader tag={tag} label={label} />
|
|
155
|
-
<div className="relative">
|
|
156
|
-
<pre className="overflow-x-auto p-4 text-xs">{children}</pre>
|
|
157
|
-
{/* @ts-expect-error code is not typed */}
|
|
158
|
-
<CopyButton code={code} />
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function APIGroupHeader({
|
|
165
|
-
title,
|
|
166
|
-
children,
|
|
167
|
-
selectedIndex,
|
|
168
|
-
}: {
|
|
169
|
-
title: string
|
|
170
|
-
children: React.ReactNode
|
|
171
|
-
selectedIndex: number
|
|
172
|
-
}) {
|
|
173
|
-
const hasTabs = Children.count(children) > 1
|
|
174
|
-
|
|
175
|
-
if (!title && !hasTabs) {
|
|
176
|
-
return null
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<div className="flex min-h-[calc(--spacing(12)+1px)] flex-wrap items-start gap-x-4 px-4 pt-2">
|
|
181
|
-
{title && (
|
|
182
|
-
<h3 className="mr-auto pt-3 text-xs font-semibold">
|
|
183
|
-
{title}
|
|
184
|
-
</h3>
|
|
185
|
-
)}
|
|
186
|
-
{hasTabs && (
|
|
187
|
-
<TabList className="-mb-px flex gap-4 text-xs font-medium">
|
|
188
|
-
{Children.map(children, (child, childIndex) => (
|
|
189
|
-
<Tab
|
|
190
|
-
className={classNames(
|
|
191
|
-
'pt-2 transition data-selected:not-data-focus:outline-hidden',
|
|
192
|
-
childIndex === selectedIndex
|
|
193
|
-
? 'border-sky-500 text-sky-400'
|
|
194
|
-
: 'border-transparent text-muted-foreground hover:text-zinc-300',
|
|
195
|
-
)}
|
|
196
|
-
>
|
|
197
|
-
{/* @ts-expect-error child.props is not typed */}
|
|
198
|
-
{getPanelTitle(isValidElement(child) ? child.props : {})}
|
|
199
|
-
</Tab>
|
|
200
|
-
))}
|
|
201
|
-
</TabList>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function APIGroupPanels({
|
|
208
|
-
children,
|
|
209
|
-
...props
|
|
210
|
-
}: React.ComponentPropsWithoutRef<typeof APIPanel>) {
|
|
211
|
-
const hasTabs = Children.count(children) > 1
|
|
212
|
-
|
|
213
|
-
if (hasTabs) {
|
|
214
|
-
return (
|
|
215
|
-
<TabPanels>
|
|
216
|
-
{Children.map(children, (child) => (
|
|
217
|
-
<TabPanel>
|
|
218
|
-
<APIPanel {...props}>{child}</APIPanel>
|
|
219
|
-
</TabPanel>
|
|
220
|
-
))}
|
|
221
|
-
</TabPanels>
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return <APIPanel {...props}>{children}</APIPanel>
|
|
226
|
-
}
|
|
227
|
-
function usePreventLayoutShift() {
|
|
228
|
-
const positionRef = useRef<HTMLElement>(null)
|
|
229
|
-
const rafRef = useRef<number>()
|
|
230
|
-
|
|
231
|
-
useEffect(() => {
|
|
232
|
-
return () => {
|
|
233
|
-
if (typeof rafRef.current !== 'undefined') {
|
|
234
|
-
window.cancelAnimationFrame(rafRef.current)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}, [])
|
|
238
|
-
|
|
239
|
-
return {
|
|
240
|
-
positionRef,
|
|
241
|
-
preventLayoutShift(callback: () => void) {
|
|
242
|
-
if (!positionRef.current) {
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const initialTop = positionRef.current.getBoundingClientRect().top
|
|
247
|
-
|
|
248
|
-
callback()
|
|
249
|
-
|
|
250
|
-
rafRef.current = window.requestAnimationFrame(() => {
|
|
251
|
-
const newTop =
|
|
252
|
-
positionRef.current?.getBoundingClientRect().top ?? initialTop
|
|
253
|
-
window.scrollBy(0, newTop - initialTop)
|
|
254
|
-
})
|
|
255
|
-
},
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function useTabGroupProps(availableLanguages: Array<string>) {
|
|
260
|
-
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
261
|
-
const [preferredLanguages, setPreferredLanguages] = useState<string[]>([])
|
|
262
|
-
|
|
263
|
-
const activeLanguage = [...availableLanguages].sort(
|
|
264
|
-
(a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a),
|
|
265
|
-
)[0]
|
|
266
|
-
const languageIndex = availableLanguages.indexOf(activeLanguage)
|
|
267
|
-
const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
|
|
268
|
-
|
|
269
|
-
if (newSelectedIndex !== selectedIndex) {
|
|
270
|
-
setSelectedIndex(newSelectedIndex)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const { positionRef, preventLayoutShift } = usePreventLayoutShift()
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
as: 'div' as const,
|
|
277
|
-
ref: positionRef,
|
|
278
|
-
selectedIndex,
|
|
279
|
-
onChange: (newSelectedIndex: number) => {
|
|
280
|
-
preventLayoutShift(() => {
|
|
281
|
-
const newLanguage = availableLanguages[newSelectedIndex]
|
|
282
|
-
setPreferredLanguages(prev => [
|
|
283
|
-
...prev.filter(lang => lang !== newLanguage),
|
|
284
|
-
newLanguage
|
|
285
|
-
])
|
|
286
|
-
})
|
|
287
|
-
},
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const APIGroupContext = createContext(false)
|
|
292
|
-
|
|
293
|
-
export function APIGroup({
|
|
294
|
-
children,
|
|
295
|
-
title,
|
|
296
|
-
...props
|
|
297
|
-
}: React.ComponentPropsWithoutRef<typeof APIGroupPanels> & { title: string }) {
|
|
298
|
-
const languages =
|
|
299
|
-
Children.map(children, (child) =>
|
|
300
|
-
// @ts-expect-error child.props is not typed
|
|
301
|
-
getPanelTitle(isValidElement(child) ? child.props : {}),
|
|
302
|
-
) ?? []
|
|
303
|
-
const tabGroupProps = useTabGroupProps(languages)
|
|
304
|
-
const hasTabs = Children.count(children) > 1
|
|
305
|
-
|
|
306
|
-
const header = (
|
|
307
|
-
<APIGroupHeader title={title} selectedIndex={tabGroupProps.selectedIndex}>
|
|
308
|
-
{children}
|
|
309
|
-
</APIGroupHeader>
|
|
310
|
-
)
|
|
311
|
-
const panels = <APIGroupPanels {...props}>{children}</APIGroupPanels>
|
|
312
|
-
|
|
313
|
-
return (
|
|
314
|
-
<APIGroupContext.Provider value={true}>
|
|
315
|
-
{hasTabs ? (
|
|
316
|
-
<Card className="py-0 border-black/10 bg-none overflow-hidden mb-4">
|
|
317
|
-
<TabGroup {...tabGroupProps}>
|
|
318
|
-
<div className="not-prose">
|
|
319
|
-
{header}
|
|
320
|
-
{panels}
|
|
321
|
-
</div>
|
|
322
|
-
</TabGroup>
|
|
323
|
-
</Card>
|
|
324
|
-
) : (
|
|
325
|
-
<Card className="py-0 border-black/10 bg-none overflow-hidden mb-4">
|
|
326
|
-
<div className="not-prose">
|
|
327
|
-
{header}
|
|
328
|
-
{panels}
|
|
329
|
-
</div>
|
|
330
|
-
</Card>
|
|
331
|
-
)}
|
|
332
|
-
</APIGroupContext.Provider>
|
|
333
|
-
)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function API({
|
|
337
|
-
children,
|
|
338
|
-
...props
|
|
339
|
-
}: React.ComponentPropsWithoutRef<'code'>) {
|
|
340
|
-
const isGrouped = useContext(APIGroupContext)
|
|
341
|
-
|
|
342
|
-
if (isGrouped) {
|
|
343
|
-
if (typeof children !== 'string') {
|
|
344
|
-
throw new Error(
|
|
345
|
-
'`API` children must be a string when nested inside a `APIGroup`.',
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
return <code {...props}>
|
|
349
|
-
{children}
|
|
350
|
-
</code>
|
|
351
|
-
}
|
|
352
|
-
|
|
1
|
+
/** Inline code element kept for MDX content that uses <API>. */
|
|
2
|
+
export function API({ children, ...props }: React.ComponentPropsWithoutRef<'code'>) {
|
|
353
3
|
return <code {...props}>{children}</code>
|
|
354
4
|
}
|