specra 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/LICENSE.MD +21 -0
- package/README.md +157 -0
- package/dist/app/api/mdx-watch/route.d.mts +12 -0
- package/dist/app/api/mdx-watch/route.d.ts +12 -0
- package/dist/app/api/mdx-watch/route.js +98 -0
- package/dist/app/api/mdx-watch/route.js.map +1 -0
- package/dist/app/api/mdx-watch/route.mjs +71 -0
- package/dist/app/api/mdx-watch/route.mjs.map +1 -0
- package/dist/app/docs-page.d.mts +32 -0
- package/dist/app/docs-page.d.ts +32 -0
- package/dist/app/docs-page.js +4072 -0
- package/dist/app/docs-page.js.map +1 -0
- package/dist/app/docs-page.mjs +14 -0
- package/dist/app/docs-page.mjs.map +1 -0
- package/dist/app/layout.css +297 -0
- package/dist/app/layout.css.map +1 -0
- package/dist/app/layout.d.mts +19 -0
- package/dist/app/layout.d.ts +19 -0
- package/dist/app/layout.js +112 -0
- package/dist/app/layout.js.map +1 -0
- package/dist/app/layout.mjs +13 -0
- package/dist/app/layout.mjs.map +1 -0
- package/dist/chunk-DR4EPLMT.mjs +1013 -0
- package/dist/chunk-DR4EPLMT.mjs.map +1 -0
- package/dist/chunk-INL2EC72.mjs +170 -0
- package/dist/chunk-INL2EC72.mjs.map +1 -0
- package/dist/chunk-IZFGEAD6.mjs +61 -0
- package/dist/chunk-IZFGEAD6.mjs.map +1 -0
- package/dist/chunk-KTRWWAGL.mjs +50 -0
- package/dist/chunk-KTRWWAGL.mjs.map +1 -0
- package/dist/chunk-MZJHJ6BV.mjs +21 -0
- package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
- package/dist/chunk-NXRIAL7T.mjs +3119 -0
- package/dist/chunk-NXRIAL7T.mjs.map +1 -0
- package/dist/components/index.d.mts +822 -0
- package/dist/components/index.d.ts +822 -0
- package/dist/components/index.js +3738 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +3627 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/index.css +297 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +545 -0
- package/dist/index.d.ts +545 -0
- package/dist/index.js +4648 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +347 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/index.d.mts +798 -0
- package/dist/lib/index.d.ts +798 -0
- package/dist/lib/index.js +1301 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/index.mjs +89 -0
- package/dist/lib/index.mjs.map +1 -0
- package/package.json +119 -0
- package/src/app/api/mdx-watch/route.ts +86 -0
- package/src/app/docs-page.tsx +212 -0
- package/src/app/layout.tsx +74 -0
- package/src/components/docs/accordion.tsx +53 -0
- package/src/components/docs/api/api-endpoint.tsx +59 -0
- package/src/components/docs/api/api-params.tsx +43 -0
- package/src/components/docs/api/api-playground.tsx +233 -0
- package/src/components/docs/api/api-reference.tsx +291 -0
- package/src/components/docs/api/api-response.tsx +48 -0
- package/src/components/docs/api/index.ts +5 -0
- package/src/components/docs/badge.tsx +22 -0
- package/src/components/docs/breadcrumb.tsx +51 -0
- package/src/components/docs/callout.tsx +109 -0
- package/src/components/docs/card.tsx +84 -0
- package/src/components/docs/category-index.tsx +112 -0
- package/src/components/docs/code-block.tsx +129 -0
- package/src/components/docs/columns.tsx +45 -0
- package/src/components/docs/componentTextProps.ts +85 -0
- package/src/components/docs/dev-mode-badge.tsx +35 -0
- package/src/components/docs/doc-layout-wrapper.tsx +54 -0
- package/src/components/docs/doc-layout.tsx +111 -0
- package/src/components/docs/doc-loading.tsx +15 -0
- package/src/components/docs/doc-metadata.tsx +55 -0
- package/src/components/docs/doc-navigation.tsx +62 -0
- package/src/components/docs/doc-tags.tsx +25 -0
- package/src/components/docs/draft-badge.tsx +10 -0
- package/src/components/docs/footer.tsx +47 -0
- package/src/components/docs/frame.tsx +22 -0
- package/src/components/docs/header.tsx +122 -0
- package/src/components/docs/hot-reload-indicator.tsx +77 -0
- package/src/components/docs/icon.tsx +70 -0
- package/src/components/docs/image-card.tsx +95 -0
- package/src/components/docs/image.tsx +73 -0
- package/src/components/docs/index.ts +48 -0
- package/src/components/docs/math.tsx +46 -0
- package/src/components/docs/mdx-components.tsx +166 -0
- package/src/components/docs/mdx-hot-reload.tsx +37 -0
- package/src/components/docs/mermaid.tsx +77 -0
- package/src/components/docs/mobile-doc-layout.tsx +115 -0
- package/src/components/docs/not-found-content.tsx +55 -0
- package/src/components/docs/search-highlight.tsx +127 -0
- package/src/components/docs/search-modal.tsx +223 -0
- package/src/components/docs/sidebar-skeleton.tsx +39 -0
- package/src/components/docs/sidebar.tsx +323 -0
- package/src/components/docs/site-banner.tsx +92 -0
- package/src/components/docs/steps.tsx +29 -0
- package/src/components/docs/tab-context.tsx +28 -0
- package/src/components/docs/tab-groups.tsx +50 -0
- package/src/components/docs/table-of-contents.tsx +104 -0
- package/src/components/docs/tabs.tsx +63 -0
- package/src/components/docs/theme-toggle.tsx +39 -0
- package/src/components/docs/tooltip.tsx +37 -0
- package/src/components/docs/version-switcher.tsx +52 -0
- package/src/components/docs/video.tsx +80 -0
- package/src/components/global/index.ts +3 -0
- package/src/components/global/version-not-found.tsx +26 -0
- package/src/components/index.ts +8 -0
- package/src/components/theme-provider.tsx +11 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/index.ts +41 -0
- package/src/lib/api-parser.types.ts +78 -0
- package/src/lib/api.types.ts +202 -0
- package/src/lib/category.ts +71 -0
- package/src/lib/config.server.ts +170 -0
- package/src/lib/config.ts +20 -0
- package/src/lib/config.types.ts +295 -0
- package/src/lib/dev-utils.ts +75 -0
- package/src/lib/index.ts +27 -0
- package/src/lib/mdx-cache.ts +200 -0
- package/src/lib/mdx.ts +402 -0
- package/src/lib/parsers/base-parser.ts +16 -0
- package/src/lib/parsers/index.ts +69 -0
- package/src/lib/parsers/openapi-parser.ts +251 -0
- package/src/lib/parsers/postman-parser.ts +301 -0
- package/src/lib/parsers/specra-parser.ts +24 -0
- package/src/lib/redirects.ts +40 -0
- package/src/lib/remark-code-meta.ts +23 -0
- package/src/lib/sidebar-utils.ts +188 -0
- package/src/lib/toc.ts +24 -0
- package/src/lib/utils.ts +36 -0
- package/src/specra.config.json +124 -0
- package/src/styles/globals.css +427 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
|
|
5
|
+
interface MathProps {
|
|
6
|
+
children: string
|
|
7
|
+
block?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Math({ children, block = false }: MathProps) {
|
|
11
|
+
const containerRef = useRef<HTMLSpanElement | HTMLDivElement>(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const renderMath = async () => {
|
|
15
|
+
try {
|
|
16
|
+
// Dynamically import KaTeX
|
|
17
|
+
const katex = (await import("katex")).default
|
|
18
|
+
|
|
19
|
+
if (containerRef.current) {
|
|
20
|
+
katex.render(children, containerRef.current, {
|
|
21
|
+
throwOnError: false,
|
|
22
|
+
displayMode: block,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error("KaTeX rendering error:", err)
|
|
27
|
+
if (containerRef.current) {
|
|
28
|
+
containerRef.current.textContent = children
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
renderMath()
|
|
34
|
+
}, [children, block])
|
|
35
|
+
|
|
36
|
+
if (block) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
ref={containerRef as React.RefObject<HTMLDivElement>}
|
|
40
|
+
className="my-6 overflow-x-auto text-center"
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return <span ref={containerRef as React.RefObject<HTMLSpanElement>} className="inline-block" />
|
|
46
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { CodeBlock } from "./code-block"
|
|
3
|
+
import { Callout } from "./callout"
|
|
4
|
+
import { Accordion, AccordionItem } from "./accordion"
|
|
5
|
+
import { Tabs, Tab } from "./tabs"
|
|
6
|
+
import { Image } from "./image"
|
|
7
|
+
import { Video } from "./video"
|
|
8
|
+
import { Card, CardGrid } from "./card"
|
|
9
|
+
import { ImageCard, ImageCardGrid } from "./image-card"
|
|
10
|
+
import { Steps, Step } from "./steps"
|
|
11
|
+
import { Icon } from "./icon"
|
|
12
|
+
import { Mermaid } from "./mermaid"
|
|
13
|
+
import { Math } from "./math"
|
|
14
|
+
import { Columns, Column } from "./columns"
|
|
15
|
+
import { Badge } from "./badge"
|
|
16
|
+
import { Tooltip } from "./tooltip"
|
|
17
|
+
import { Frame } from "./frame"
|
|
18
|
+
import { ApiEndpoint, ApiParams, ApiResponse, ApiPlayground, ApiReference } from "./api"
|
|
19
|
+
|
|
20
|
+
export const mdxComponents = {
|
|
21
|
+
h1: ({ children }: { children: ReactNode }) => (
|
|
22
|
+
<h1 className="text-3xl font-semibold tracking-tight mb-6 text-foreground">{children}</h1>
|
|
23
|
+
),
|
|
24
|
+
h2: ({ children, id }: { children: ReactNode; id?: string }) => (
|
|
25
|
+
<h2 id={id} className="text-2xl font-semibold tracking-tight mt-10 mb-4 text-foreground scroll-mt-24">
|
|
26
|
+
{children}
|
|
27
|
+
</h2>
|
|
28
|
+
),
|
|
29
|
+
h3: ({ children, id }: { children: ReactNode; id?: string }) => (
|
|
30
|
+
<h3 id={id} className="text-xl font-medium tracking-tight mt-8 mb-3 text-foreground scroll-mt-24">
|
|
31
|
+
{children}
|
|
32
|
+
</h3>
|
|
33
|
+
),
|
|
34
|
+
p: ({ children }: { children: ReactNode }) => (
|
|
35
|
+
<p className="text-base leading-7 text-muted-foreground mb-4">{children}</p>
|
|
36
|
+
),
|
|
37
|
+
code: ({ children, className, meta, ...props }: { children: ReactNode; className?: string; meta?: string; [key: string]: any }) => {
|
|
38
|
+
const isInline = !className
|
|
39
|
+
if (isInline) {
|
|
40
|
+
return (
|
|
41
|
+
<code className="px-1.5 py-0.5 rounded-md bg-muted/50 text-primary font-mono text-[13px] border border-border/50">
|
|
42
|
+
{children}
|
|
43
|
+
</code>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract language from className
|
|
48
|
+
const language = className?.replace("language-", "") || "text"
|
|
49
|
+
|
|
50
|
+
// Use meta string as filename if provided
|
|
51
|
+
const filename = meta || undefined
|
|
52
|
+
|
|
53
|
+
const code = String(children).replace(/\n$/, "")
|
|
54
|
+
|
|
55
|
+
return <CodeBlock code={code} language={language} filename={filename} />
|
|
56
|
+
},
|
|
57
|
+
pre: ({ children }: { children: ReactNode }) => <>{children}</>,
|
|
58
|
+
ul: ({ children }: { children: ReactNode }) => (
|
|
59
|
+
<ul className="list-disc list-outside pl-5 space-y-2 mb-4 text-muted-foreground [&_p]:mb-0 [&_p]:inline [&_ul]:ml-6 [&_ol]:ml-6">{children}</ul>
|
|
60
|
+
),
|
|
61
|
+
ol: ({ children }: { children: ReactNode }) => (
|
|
62
|
+
<ol className="list-decimal list-outside pl-5 space-y-2 mb-4 text-muted-foreground [&_p]:mb-0 [&_p]:inline [&_ul]:ml-6 [&_ol]:ml-6">{children}</ol>
|
|
63
|
+
),
|
|
64
|
+
li: ({ children }: { children: ReactNode }) => <li className="leading-7 [&>p]:mb-0 [&>p]:inline">{children}</li>,
|
|
65
|
+
a: ({ children, href }: { children: ReactNode; href?: string }) => (
|
|
66
|
+
<a
|
|
67
|
+
href={href}
|
|
68
|
+
className="text-primary hover:underline font-medium"
|
|
69
|
+
target={href?.startsWith("http") ? "_blank" : undefined}
|
|
70
|
+
rel={href?.startsWith("http") ? "noopener noreferrer" : undefined}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</a>
|
|
74
|
+
),
|
|
75
|
+
blockquote: ({ children }: { children: ReactNode }) => {
|
|
76
|
+
// Check if this is a GitHub-style alert blockquote
|
|
77
|
+
const childrenArray = Array.isArray(children) ? children : [children]
|
|
78
|
+
const firstChild = childrenArray[0]
|
|
79
|
+
|
|
80
|
+
// Extract text content from the blockquote
|
|
81
|
+
let textContent = ""
|
|
82
|
+
if (firstChild && typeof firstChild === "object" && "props" in firstChild) {
|
|
83
|
+
const props = (firstChild as any).props
|
|
84
|
+
if (props.children) {
|
|
85
|
+
const text = Array.isArray(props.children) ? props.children.join("") : String(props.children)
|
|
86
|
+
textContent = text
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for alert patterns like [!INFO], [!WARNING], etc.
|
|
91
|
+
const alertMatch = textContent.match(/^\[!(INFO|TIP|WARNING|SUCCESS|ERROR)\]/)
|
|
92
|
+
|
|
93
|
+
if (alertMatch) {
|
|
94
|
+
const type = alertMatch[1].toLowerCase() as "info" | "tip" | "warning" | "success" | "error"
|
|
95
|
+
|
|
96
|
+
// Extract the content after the alert marker
|
|
97
|
+
const processChildren = (node: any): any => {
|
|
98
|
+
if (typeof node === "string") {
|
|
99
|
+
return node.replace(/^\[!(INFO|TIP|WARNING|SUCCESS|ERROR)\]\s*\n?/, "")
|
|
100
|
+
}
|
|
101
|
+
if (node && typeof node === "object" && "props" in node) {
|
|
102
|
+
return {
|
|
103
|
+
...node,
|
|
104
|
+
props: {
|
|
105
|
+
...node.props,
|
|
106
|
+
children: Array.isArray(node.props.children)
|
|
107
|
+
? node.props.children.map(processChildren)
|
|
108
|
+
: processChildren(node.props.children),
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return node
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const cleanedChildren = Array.isArray(children) ? children.map(processChildren) : processChildren(children)
|
|
116
|
+
|
|
117
|
+
return <Callout type={type}>{cleanedChildren}</Callout>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Regular blockquote
|
|
121
|
+
return (
|
|
122
|
+
<blockquote className="border-l-4 border-primary/50 bg-muted/30 pl-4 pr-4 py-3 my-6 rounded-r-lg">
|
|
123
|
+
<div className="text-muted-foreground italic [&>p]:mb-0">{children}</div>
|
|
124
|
+
</blockquote>
|
|
125
|
+
)
|
|
126
|
+
},
|
|
127
|
+
table: ({ children }: { children: ReactNode }) => (
|
|
128
|
+
<div className="overflow-x-auto mb-6 rounded-xl border border-border">
|
|
129
|
+
<table className="min-w-full border-collapse">{children}</table>
|
|
130
|
+
</div>
|
|
131
|
+
),
|
|
132
|
+
th: ({ children }: { children: ReactNode }) => (
|
|
133
|
+
<th className="border-b border-r border-border bg-muted px-4 py-2 text-left font-semibold text-foreground last:border-r-0">{children}</th>
|
|
134
|
+
),
|
|
135
|
+
td: ({ children }: { children: ReactNode }) => (
|
|
136
|
+
<td className="border-b border-r border-border px-4 py-2 text-muted-foreground last:border-r-0">{children}</td>
|
|
137
|
+
),
|
|
138
|
+
// Custom components
|
|
139
|
+
Callout,
|
|
140
|
+
Accordion,
|
|
141
|
+
AccordionItem,
|
|
142
|
+
Tabs,
|
|
143
|
+
Tab,
|
|
144
|
+
Image,
|
|
145
|
+
Video,
|
|
146
|
+
Card,
|
|
147
|
+
CardGrid,
|
|
148
|
+
ImageCard,
|
|
149
|
+
ImageCardGrid,
|
|
150
|
+
Steps,
|
|
151
|
+
Step,
|
|
152
|
+
Icon,
|
|
153
|
+
Mermaid,
|
|
154
|
+
Math,
|
|
155
|
+
Columns,
|
|
156
|
+
Column,
|
|
157
|
+
Badge,
|
|
158
|
+
Tooltip,
|
|
159
|
+
Frame,
|
|
160
|
+
// API Documentation components
|
|
161
|
+
ApiEndpoint,
|
|
162
|
+
ApiParams,
|
|
163
|
+
ApiResponse,
|
|
164
|
+
ApiPlayground,
|
|
165
|
+
ApiReference,
|
|
166
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
|
|
6
|
+
export function MdxHotReload() {
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (process.env.NODE_ENV !== "development") return
|
|
11
|
+
|
|
12
|
+
// Use Server-Sent Events to watch for file changes
|
|
13
|
+
const eventSource = new EventSource('/api/mdx-watch')
|
|
14
|
+
|
|
15
|
+
eventSource.onmessage = (event) => {
|
|
16
|
+
const data = JSON.parse(event.data)
|
|
17
|
+
|
|
18
|
+
if (data.type === 'change') {
|
|
19
|
+
console.log('[MDX Hot Reload] File changed:', data.file)
|
|
20
|
+
router.refresh()
|
|
21
|
+
} else if (data.type === 'connected') {
|
|
22
|
+
console.log('[MDX Hot Reload] Watching for changes...')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
eventSource.onerror = (error) => {
|
|
27
|
+
console.error('[MDX Hot Reload] Connection error:', error)
|
|
28
|
+
eventSource.close()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return () => {
|
|
32
|
+
eventSource.close()
|
|
33
|
+
}
|
|
34
|
+
}, [router])
|
|
35
|
+
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react"
|
|
4
|
+
|
|
5
|
+
interface MermaidProps {
|
|
6
|
+
chart: string
|
|
7
|
+
caption?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Mermaid({ chart, caption }: MermaidProps) {
|
|
11
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
12
|
+
const [error, setError] = useState<string | null>(null)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const renderChart = async () => {
|
|
16
|
+
try {
|
|
17
|
+
// Dynamically import mermaid
|
|
18
|
+
const mermaid = (await import("mermaid")).default
|
|
19
|
+
|
|
20
|
+
mermaid.initialize({
|
|
21
|
+
startOnLoad: false,
|
|
22
|
+
theme: document.documentElement.classList.contains("dark") ? "dark" : "default",
|
|
23
|
+
securityLevel: "loose",
|
|
24
|
+
fontFamily: "inherit",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (containerRef.current) {
|
|
28
|
+
const id = `mermaid-${Math.random().toString(36).substr(2, 9)}`
|
|
29
|
+
const { svg } = await mermaid.render(id, chart)
|
|
30
|
+
containerRef.current.innerHTML = svg
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error("Mermaid rendering error:", err)
|
|
34
|
+
setError(err instanceof Error ? err.message : "Failed to render diagram")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
renderChart()
|
|
39
|
+
|
|
40
|
+
// Re-render on theme change
|
|
41
|
+
const observer = new MutationObserver((mutations) => {
|
|
42
|
+
mutations.forEach((mutation) => {
|
|
43
|
+
if (mutation.attributeName === "class") {
|
|
44
|
+
renderChart()
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
observer.observe(document.documentElement, { attributes: true })
|
|
50
|
+
|
|
51
|
+
return () => observer.disconnect()
|
|
52
|
+
}, [chart])
|
|
53
|
+
|
|
54
|
+
if (error) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="my-6 p-4 rounded-xl border border-red-500/50 bg-red-500/10">
|
|
57
|
+
<p className="text-sm text-red-600 dark:text-red-400 font-mono">
|
|
58
|
+
Mermaid Error: {error}
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<figure className="my-6">
|
|
66
|
+
<div
|
|
67
|
+
ref={containerRef}
|
|
68
|
+
className="flex justify-center items-center p-6 rounded-xl border border-border bg-muted/30 overflow-x-auto"
|
|
69
|
+
/>
|
|
70
|
+
{caption && (
|
|
71
|
+
<figcaption className="mt-2 text-center text-sm text-muted-foreground italic">
|
|
72
|
+
{caption}
|
|
73
|
+
</figcaption>
|
|
74
|
+
)}
|
|
75
|
+
</figure>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, ReactNode, cloneElement, isValidElement } from "react"
|
|
4
|
+
import { Footer } from "./footer"
|
|
5
|
+
import { SiteBanner } from "./site-banner"
|
|
6
|
+
import { TabGroups } from "./tab-groups"
|
|
7
|
+
import { Sidebar } from "./sidebar"
|
|
8
|
+
import type { SpecraConfig } from "@/lib/config"
|
|
9
|
+
import type { Doc } from "@/lib/mdx"
|
|
10
|
+
|
|
11
|
+
interface MobileDocLayoutProps {
|
|
12
|
+
header: ReactNode
|
|
13
|
+
docs: Doc[]
|
|
14
|
+
version: string
|
|
15
|
+
content: ReactNode
|
|
16
|
+
toc: ReactNode
|
|
17
|
+
config: SpecraConfig
|
|
18
|
+
activeTabGroup?: string
|
|
19
|
+
onTabChange?: (tabId: string) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function MobileDocLayout({ header, docs, version, content, toc, config, activeTabGroup, onTabChange }: MobileDocLayoutProps) {
|
|
23
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
24
|
+
|
|
25
|
+
const handleTabChange = (tabId: string) => {
|
|
26
|
+
onTabChange?.(tabId)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const closeSidebar = () => setSidebarOpen(false)
|
|
30
|
+
const toggleSidebar = () => setSidebarOpen(!sidebarOpen)
|
|
31
|
+
|
|
32
|
+
// Clone header and pass onMenuClick prop if it's a valid React element
|
|
33
|
+
const headerWithProps = isValidElement(header)
|
|
34
|
+
? cloneElement(header as React.ReactElement<any>, {
|
|
35
|
+
onMenuClick: toggleSidebar,
|
|
36
|
+
})
|
|
37
|
+
: header
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="min-h-screen bg-background">
|
|
41
|
+
{/* Header */}
|
|
42
|
+
{headerWithProps}
|
|
43
|
+
|
|
44
|
+
{/* Site-wide Banner */}
|
|
45
|
+
<SiteBanner config={config} />
|
|
46
|
+
|
|
47
|
+
{/* Tab Groups - shown only if configured */}
|
|
48
|
+
{config.navigation?.tabGroups && config.navigation.tabGroups.length > 0 && (
|
|
49
|
+
<TabGroups
|
|
50
|
+
tabGroups={config.navigation.tabGroups}
|
|
51
|
+
activeTabId={activeTabGroup}
|
|
52
|
+
onTabChange={handleTabChange}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Mobile Sidebar Overlay */}
|
|
57
|
+
{sidebarOpen && (
|
|
58
|
+
<div
|
|
59
|
+
className="lg:hidden fixed inset-0 bg-background/80 backdrop-blur-sm z-40"
|
|
60
|
+
onClick={() => setSidebarOpen(false)}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* Mobile Sidebar */}
|
|
65
|
+
<div
|
|
66
|
+
className={`lg:hidden fixed top-0 left-0 h-full w-64 bg-background border-r border-border z-40 transform transition-transform duration-300 ease-in-out overflow-y-auto ${sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
<div className="pt-20 px-4">
|
|
70
|
+
<Sidebar
|
|
71
|
+
docs={docs}
|
|
72
|
+
version={version}
|
|
73
|
+
config={config}
|
|
74
|
+
onLinkClick={closeSidebar}
|
|
75
|
+
activeTabGroup={activeTabGroup}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Main Content */}
|
|
81
|
+
<main className="container mx-auto px-6 py-8">
|
|
82
|
+
<div className="flex">
|
|
83
|
+
{/* Desktop Sidebar */}
|
|
84
|
+
<div className="hidden lg:block">
|
|
85
|
+
<Sidebar
|
|
86
|
+
docs={docs}
|
|
87
|
+
version={version}
|
|
88
|
+
config={config}
|
|
89
|
+
activeTabGroup={activeTabGroup}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="flex-1 min-w-0">
|
|
94
|
+
<div className="flex flex-col gap-2 px-2 md:px-8">
|
|
95
|
+
{/* Content */}
|
|
96
|
+
{content}
|
|
97
|
+
|
|
98
|
+
{/* Footer */}
|
|
99
|
+
<Footer config={config} />
|
|
100
|
+
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* ToC */}
|
|
105
|
+
{toc}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
</main>
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
import { AlertTriangle, Home, ArrowLeft } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
interface NotFoundContentProps {
|
|
7
|
+
version: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function NotFoundContent({ version }: NotFoundContentProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex min-h-[calc(100vh-12rem)] items-center justify-center px-4 py-12">
|
|
13
|
+
<div className="w-full max-w-2xl text-center">
|
|
14
|
+
<div className="mb-6 flex justify-center">
|
|
15
|
+
<div className="rounded-full bg-yellow-500/10 p-4">
|
|
16
|
+
<AlertTriangle className="h-16 w-16 text-yellow-500" />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<h1 className="mb-3 text-5xl font-bold tracking-tight">404</h1>
|
|
21
|
+
<h2 className="mb-4 text-2xl font-semibold">Page Not Found</h2>
|
|
22
|
+
|
|
23
|
+
<p className="mb-8 text-base text-muted-foreground">
|
|
24
|
+
The documentation page you're looking for doesn't exist or may have been moved.
|
|
25
|
+
<br />
|
|
26
|
+
Try using the sidebar to find what you're looking for, or return to the documentation home.
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<div className="flex flex-col items-center justify-center gap-3 sm:flex-row">
|
|
30
|
+
<Link
|
|
31
|
+
href={`/docs/${version}`}
|
|
32
|
+
className="inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
33
|
+
>
|
|
34
|
+
<ArrowLeft className="h-4 w-4" />
|
|
35
|
+
Back to Documentation
|
|
36
|
+
</Link>
|
|
37
|
+
|
|
38
|
+
<Link
|
|
39
|
+
href="/"
|
|
40
|
+
className="inline-flex items-center gap-2 rounded-lg border border-border bg-background px-6 py-3 text-sm font-medium hover:bg-muted transition-colors"
|
|
41
|
+
>
|
|
42
|
+
<Home className="h-4 w-4" />
|
|
43
|
+
Go to Homepage
|
|
44
|
+
</Link>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="mt-12 rounded-lg border border-border bg-muted/30 p-6">
|
|
48
|
+
<p className="text-sm text-muted-foreground">
|
|
49
|
+
<strong className="font-medium text-foreground">Tip:</strong> Use the sidebar navigation on the left to browse all available documentation pages.
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react"
|
|
4
|
+
import { useSearchParams } from "next/navigation"
|
|
5
|
+
|
|
6
|
+
export function SearchHighlight() {
|
|
7
|
+
const searchParams = useSearchParams()
|
|
8
|
+
const query = searchParams.get("q")
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!query) {
|
|
12
|
+
// Remove any existing highlights
|
|
13
|
+
document.querySelectorAll("mark.search-highlight").forEach((mark) => {
|
|
14
|
+
const parent = mark.parentNode
|
|
15
|
+
if (parent) {
|
|
16
|
+
parent.replaceChild(document.createTextNode(mark.textContent || ""), mark)
|
|
17
|
+
parent.normalize()
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Wait for content to load
|
|
24
|
+
const timeout = setTimeout(() => {
|
|
25
|
+
highlightSearchTerm(query)
|
|
26
|
+
}, 100)
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
clearTimeout(timeout)
|
|
30
|
+
// Cleanup highlights on unmount
|
|
31
|
+
document.querySelectorAll("mark.search-highlight").forEach((mark) => {
|
|
32
|
+
const parent = mark.parentNode
|
|
33
|
+
if (parent) {
|
|
34
|
+
parent.replaceChild(document.createTextNode(mark.textContent || ""), mark)
|
|
35
|
+
parent.normalize()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}, [query])
|
|
40
|
+
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function highlightSearchTerm(searchTerm: string) {
|
|
45
|
+
// Remove existing highlights first
|
|
46
|
+
document.querySelectorAll("mark.search-highlight").forEach((mark) => {
|
|
47
|
+
const parent = mark.parentNode
|
|
48
|
+
if (parent) {
|
|
49
|
+
parent.replaceChild(document.createTextNode(mark.textContent || ""), mark)
|
|
50
|
+
parent.normalize()
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Only highlight in the main content area
|
|
55
|
+
const contentArea = document.querySelector("main") || document.body
|
|
56
|
+
|
|
57
|
+
const walker = document.createTreeWalker(
|
|
58
|
+
contentArea,
|
|
59
|
+
NodeFilter.SHOW_TEXT,
|
|
60
|
+
{
|
|
61
|
+
acceptNode: (node) => {
|
|
62
|
+
// Skip if parent is already a mark, script, style, or code element
|
|
63
|
+
const parent = node.parentElement
|
|
64
|
+
if (!parent) return NodeFilter.FILTER_REJECT
|
|
65
|
+
|
|
66
|
+
const tagName = parent.tagName.toLowerCase()
|
|
67
|
+
if (["mark", "script", "style", "code", "pre"].includes(tagName)) {
|
|
68
|
+
return NodeFilter.FILTER_REJECT
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if text contains the search term
|
|
72
|
+
if (node.textContent && node.textContent.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
73
|
+
return NodeFilter.FILTER_ACCEPT
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return NodeFilter.FILTER_REJECT
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const nodesToHighlight: { node: Text; text: string }[] = []
|
|
82
|
+
let currentNode: Node | null
|
|
83
|
+
|
|
84
|
+
while ((currentNode = walker.nextNode())) {
|
|
85
|
+
if (currentNode.textContent) {
|
|
86
|
+
nodesToHighlight.push({
|
|
87
|
+
node: currentNode as Text,
|
|
88
|
+
text: currentNode.textContent
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Highlight all found nodes
|
|
94
|
+
nodesToHighlight.forEach(({ node, text }) => {
|
|
95
|
+
const regex = new RegExp(`(${escapeRegex(searchTerm)})`, "gi")
|
|
96
|
+
const parts = text.split(regex)
|
|
97
|
+
|
|
98
|
+
if (parts.length > 1) {
|
|
99
|
+
const fragment = document.createDocumentFragment()
|
|
100
|
+
|
|
101
|
+
parts.forEach((part) => {
|
|
102
|
+
if (part.toLowerCase() === searchTerm.toLowerCase()) {
|
|
103
|
+
const mark = document.createElement("mark")
|
|
104
|
+
mark.className = "search-highlight bg-yellow-200 dark:bg-yellow-900/50 text-foreground px-1 rounded"
|
|
105
|
+
mark.textContent = part
|
|
106
|
+
fragment.appendChild(mark)
|
|
107
|
+
} else if (part) {
|
|
108
|
+
fragment.appendChild(document.createTextNode(part))
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
node.parentNode?.replaceChild(fragment, node)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Scroll to first highlight
|
|
117
|
+
const firstHighlight = document.querySelector("mark.search-highlight")
|
|
118
|
+
if (firstHighlight) {
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
firstHighlight.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
121
|
+
}, 200)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function escapeRegex(string: string) {
|
|
126
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
127
|
+
}
|