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.
Files changed (142) hide show
  1. package/LICENSE.MD +21 -0
  2. package/README.md +157 -0
  3. package/dist/app/api/mdx-watch/route.d.mts +12 -0
  4. package/dist/app/api/mdx-watch/route.d.ts +12 -0
  5. package/dist/app/api/mdx-watch/route.js +98 -0
  6. package/dist/app/api/mdx-watch/route.js.map +1 -0
  7. package/dist/app/api/mdx-watch/route.mjs +71 -0
  8. package/dist/app/api/mdx-watch/route.mjs.map +1 -0
  9. package/dist/app/docs-page.d.mts +32 -0
  10. package/dist/app/docs-page.d.ts +32 -0
  11. package/dist/app/docs-page.js +4072 -0
  12. package/dist/app/docs-page.js.map +1 -0
  13. package/dist/app/docs-page.mjs +14 -0
  14. package/dist/app/docs-page.mjs.map +1 -0
  15. package/dist/app/layout.css +297 -0
  16. package/dist/app/layout.css.map +1 -0
  17. package/dist/app/layout.d.mts +19 -0
  18. package/dist/app/layout.d.ts +19 -0
  19. package/dist/app/layout.js +112 -0
  20. package/dist/app/layout.js.map +1 -0
  21. package/dist/app/layout.mjs +13 -0
  22. package/dist/app/layout.mjs.map +1 -0
  23. package/dist/chunk-DR4EPLMT.mjs +1013 -0
  24. package/dist/chunk-DR4EPLMT.mjs.map +1 -0
  25. package/dist/chunk-INL2EC72.mjs +170 -0
  26. package/dist/chunk-INL2EC72.mjs.map +1 -0
  27. package/dist/chunk-IZFGEAD6.mjs +61 -0
  28. package/dist/chunk-IZFGEAD6.mjs.map +1 -0
  29. package/dist/chunk-KTRWWAGL.mjs +50 -0
  30. package/dist/chunk-KTRWWAGL.mjs.map +1 -0
  31. package/dist/chunk-MZJHJ6BV.mjs +21 -0
  32. package/dist/chunk-MZJHJ6BV.mjs.map +1 -0
  33. package/dist/chunk-NXRIAL7T.mjs +3119 -0
  34. package/dist/chunk-NXRIAL7T.mjs.map +1 -0
  35. package/dist/components/index.d.mts +822 -0
  36. package/dist/components/index.d.ts +822 -0
  37. package/dist/components/index.js +3738 -0
  38. package/dist/components/index.js.map +1 -0
  39. package/dist/components/index.mjs +3627 -0
  40. package/dist/components/index.mjs.map +1 -0
  41. package/dist/index.css +297 -0
  42. package/dist/index.css.map +1 -0
  43. package/dist/index.d.mts +545 -0
  44. package/dist/index.d.ts +545 -0
  45. package/dist/index.js +4648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/index.mjs +347 -0
  48. package/dist/index.mjs.map +1 -0
  49. package/dist/lib/index.d.mts +798 -0
  50. package/dist/lib/index.d.ts +798 -0
  51. package/dist/lib/index.js +1301 -0
  52. package/dist/lib/index.js.map +1 -0
  53. package/dist/lib/index.mjs +89 -0
  54. package/dist/lib/index.mjs.map +1 -0
  55. package/package.json +119 -0
  56. package/src/app/api/mdx-watch/route.ts +86 -0
  57. package/src/app/docs-page.tsx +212 -0
  58. package/src/app/layout.tsx +74 -0
  59. package/src/components/docs/accordion.tsx +53 -0
  60. package/src/components/docs/api/api-endpoint.tsx +59 -0
  61. package/src/components/docs/api/api-params.tsx +43 -0
  62. package/src/components/docs/api/api-playground.tsx +233 -0
  63. package/src/components/docs/api/api-reference.tsx +291 -0
  64. package/src/components/docs/api/api-response.tsx +48 -0
  65. package/src/components/docs/api/index.ts +5 -0
  66. package/src/components/docs/badge.tsx +22 -0
  67. package/src/components/docs/breadcrumb.tsx +51 -0
  68. package/src/components/docs/callout.tsx +109 -0
  69. package/src/components/docs/card.tsx +84 -0
  70. package/src/components/docs/category-index.tsx +112 -0
  71. package/src/components/docs/code-block.tsx +129 -0
  72. package/src/components/docs/columns.tsx +45 -0
  73. package/src/components/docs/componentTextProps.ts +85 -0
  74. package/src/components/docs/dev-mode-badge.tsx +35 -0
  75. package/src/components/docs/doc-layout-wrapper.tsx +54 -0
  76. package/src/components/docs/doc-layout.tsx +111 -0
  77. package/src/components/docs/doc-loading.tsx +15 -0
  78. package/src/components/docs/doc-metadata.tsx +55 -0
  79. package/src/components/docs/doc-navigation.tsx +62 -0
  80. package/src/components/docs/doc-tags.tsx +25 -0
  81. package/src/components/docs/draft-badge.tsx +10 -0
  82. package/src/components/docs/footer.tsx +47 -0
  83. package/src/components/docs/frame.tsx +22 -0
  84. package/src/components/docs/header.tsx +122 -0
  85. package/src/components/docs/hot-reload-indicator.tsx +77 -0
  86. package/src/components/docs/icon.tsx +70 -0
  87. package/src/components/docs/image-card.tsx +95 -0
  88. package/src/components/docs/image.tsx +73 -0
  89. package/src/components/docs/index.ts +48 -0
  90. package/src/components/docs/math.tsx +46 -0
  91. package/src/components/docs/mdx-components.tsx +166 -0
  92. package/src/components/docs/mdx-hot-reload.tsx +37 -0
  93. package/src/components/docs/mermaid.tsx +77 -0
  94. package/src/components/docs/mobile-doc-layout.tsx +115 -0
  95. package/src/components/docs/not-found-content.tsx +55 -0
  96. package/src/components/docs/search-highlight.tsx +127 -0
  97. package/src/components/docs/search-modal.tsx +223 -0
  98. package/src/components/docs/sidebar-skeleton.tsx +39 -0
  99. package/src/components/docs/sidebar.tsx +323 -0
  100. package/src/components/docs/site-banner.tsx +92 -0
  101. package/src/components/docs/steps.tsx +29 -0
  102. package/src/components/docs/tab-context.tsx +28 -0
  103. package/src/components/docs/tab-groups.tsx +50 -0
  104. package/src/components/docs/table-of-contents.tsx +104 -0
  105. package/src/components/docs/tabs.tsx +63 -0
  106. package/src/components/docs/theme-toggle.tsx +39 -0
  107. package/src/components/docs/tooltip.tsx +37 -0
  108. package/src/components/docs/version-switcher.tsx +52 -0
  109. package/src/components/docs/video.tsx +80 -0
  110. package/src/components/global/index.ts +3 -0
  111. package/src/components/global/version-not-found.tsx +26 -0
  112. package/src/components/index.ts +8 -0
  113. package/src/components/theme-provider.tsx +11 -0
  114. package/src/components/ui/badge.tsx +46 -0
  115. package/src/components/ui/button.tsx +60 -0
  116. package/src/components/ui/dialog.tsx +143 -0
  117. package/src/components/ui/index.ts +6 -0
  118. package/src/components/ui/input.tsx +21 -0
  119. package/src/components/ui/textarea.tsx +18 -0
  120. package/src/index.ts +41 -0
  121. package/src/lib/api-parser.types.ts +78 -0
  122. package/src/lib/api.types.ts +202 -0
  123. package/src/lib/category.ts +71 -0
  124. package/src/lib/config.server.ts +170 -0
  125. package/src/lib/config.ts +20 -0
  126. package/src/lib/config.types.ts +295 -0
  127. package/src/lib/dev-utils.ts +75 -0
  128. package/src/lib/index.ts +27 -0
  129. package/src/lib/mdx-cache.ts +200 -0
  130. package/src/lib/mdx.ts +402 -0
  131. package/src/lib/parsers/base-parser.ts +16 -0
  132. package/src/lib/parsers/index.ts +69 -0
  133. package/src/lib/parsers/openapi-parser.ts +251 -0
  134. package/src/lib/parsers/postman-parser.ts +301 -0
  135. package/src/lib/parsers/specra-parser.ts +24 -0
  136. package/src/lib/redirects.ts +40 -0
  137. package/src/lib/remark-code-meta.ts +23 -0
  138. package/src/lib/sidebar-utils.ts +188 -0
  139. package/src/lib/toc.ts +24 -0
  140. package/src/lib/utils.ts +36 -0
  141. package/src/specra.config.json +124 -0
  142. 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
+ }