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,112 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { ChevronRight, FileText } from "lucide-react"
|
|
3
|
+
import type { Doc } from "@/lib/mdx"
|
|
4
|
+
import { ReactNode } from "react"
|
|
5
|
+
import { MDXRemote } from "next-mdx-remote/rsc"
|
|
6
|
+
import remarkGfm from "remark-gfm"
|
|
7
|
+
import { remarkCodeMeta } from "@/lib/remark-code-meta"
|
|
8
|
+
import rehypeSlug from "rehype-slug"
|
|
9
|
+
import { mdxComponents } from "./mdx-components"
|
|
10
|
+
import { getConfig, processContentWithEnv, SpecraConfig } from "@/lib/config"
|
|
11
|
+
import { sortSidebarItems } from "@/lib/sidebar-utils"
|
|
12
|
+
|
|
13
|
+
interface CategoryIndexProps {
|
|
14
|
+
categoryPath: string
|
|
15
|
+
version: string
|
|
16
|
+
allDocs: Doc[]
|
|
17
|
+
title: string
|
|
18
|
+
description?: string
|
|
19
|
+
content?: string
|
|
20
|
+
config: SpecraConfig
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CategoryIndex({ categoryPath, version, allDocs, title, description, content , config}: CategoryIndexProps) {
|
|
24
|
+
// Find all docs that are direct children of this category
|
|
25
|
+
const childDocs = allDocs.filter((doc) => {
|
|
26
|
+
// Get the parent path of the doc
|
|
27
|
+
const parts = doc.slug.split("/")
|
|
28
|
+
const docParent = parts.slice(0, -1).join("/")
|
|
29
|
+
|
|
30
|
+
// Check if this doc is a direct child of the category
|
|
31
|
+
return docParent === categoryPath && doc.slug !== categoryPath
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
// const config = getConfig();
|
|
36
|
+
const processedContent = () => {
|
|
37
|
+
if(content){
|
|
38
|
+
return processContentWithEnv(content, config);
|
|
39
|
+
}
|
|
40
|
+
return "";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Sort by sidebar_position using unified sorting function
|
|
44
|
+
const sortedDocs = sortSidebarItems(childDocs)
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="flex-1 min-w-0">
|
|
48
|
+
<div className="mb-8">
|
|
49
|
+
<h1 className="text-4xl font-bold tracking-tight mb-3 text-foreground">{title}</h1>
|
|
50
|
+
{description && <p className="text-lg text-muted-foreground leading-relaxed">{description}</p>}
|
|
51
|
+
|
|
52
|
+
<div className="prose prose-slate dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3 prose-p:text-base prose-p:leading-7 prose-p:text-muted-foreground prose-p:mb-4 prose-a:font-normal prose-a:transition-all prose-code:text-primary prose-code:bg-muted/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-[13px] prose-code:font-mono prose-code:border prose-code:border-border/50 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0 prose-ul:list-disc prose-ul:list-inside prose-ul:space-y-2 prose-ul:mb-4 prose-ol:list-decimal prose-ol:list-inside prose-ol:space-y-2 prose-ol:mb-4 prose-li:leading-7 prose-li:text-muted-foreground prose-strong:text-foreground prose-strong:font-semibold">
|
|
53
|
+
<MDXRemote
|
|
54
|
+
source={processedContent()}
|
|
55
|
+
options={{
|
|
56
|
+
parseFrontmatter: false,
|
|
57
|
+
mdxOptions: {
|
|
58
|
+
remarkPlugins: [remarkGfm, remarkCodeMeta],
|
|
59
|
+
rehypePlugins: [rehypeSlug],
|
|
60
|
+
development: false,
|
|
61
|
+
},
|
|
62
|
+
}}
|
|
63
|
+
components={mdxComponents as any}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
70
|
+
{sortedDocs.map((doc) => (
|
|
71
|
+
<Link
|
|
72
|
+
key={doc.slug}
|
|
73
|
+
href={`/docs/${version}/${doc.slug}`}
|
|
74
|
+
className="group block p-5 rounded-xl border border-border bg-card hover:bg-accent hover:border-primary/50 transition-all duration-200"
|
|
75
|
+
style={{
|
|
76
|
+
textDecoration: "none !important"
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<div className="flex items-start justify-between gap-4">
|
|
80
|
+
<div className="flex-1 min-w-0">
|
|
81
|
+
<div className="flex items-center gap-2 mb-2">
|
|
82
|
+
<FileText className="h-6 w-6 text-primary shrink-0" />
|
|
83
|
+
<h3 className="text-lg font-semibold text-foreground group-hover:text-primary transition-colors">
|
|
84
|
+
{doc.meta.title || doc.title}
|
|
85
|
+
</h3>
|
|
86
|
+
</div>
|
|
87
|
+
{doc.meta.description && (
|
|
88
|
+
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
89
|
+
{doc.meta.description}
|
|
90
|
+
</p>
|
|
91
|
+
)}
|
|
92
|
+
{doc.meta.reading_time && (
|
|
93
|
+
<p className="text-xs text-muted-foreground mt-2">
|
|
94
|
+
{doc.meta.reading_time} min read
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all flex-shrink-0 mt-1" />
|
|
99
|
+
</div>
|
|
100
|
+
</Link>
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{sortedDocs.length === 0 && (
|
|
105
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
106
|
+
<FileText className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
|
107
|
+
<p>No documents found in this category.</p>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { Check, Copy } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
interface CodeBlockProps {
|
|
7
|
+
code: string
|
|
8
|
+
language: string
|
|
9
|
+
filename?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CodeBlock({ code, language, filename }: CodeBlockProps) {
|
|
13
|
+
const [copied, setCopied] = useState(false)
|
|
14
|
+
|
|
15
|
+
const handleCopy = async () => {
|
|
16
|
+
await navigator.clipboard.writeText(code)
|
|
17
|
+
setCopied(true)
|
|
18
|
+
setTimeout(() => setCopied(false), 2000)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const highlightCode = (code: string, lang: string) => {
|
|
22
|
+
const lines = code.split("\n")
|
|
23
|
+
|
|
24
|
+
return lines.map((line, i) => {
|
|
25
|
+
const tokens: Array<{ type: string; value: string }> = []
|
|
26
|
+
let currentPos = 0
|
|
27
|
+
|
|
28
|
+
// Regex patterns for different token types
|
|
29
|
+
const patterns = [
|
|
30
|
+
{ type: "comment", regex: /(\/\/.*$|\/\*[\s\S]*?\*\/|#.*$)/ },
|
|
31
|
+
{ type: "string", regex: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/ },
|
|
32
|
+
{
|
|
33
|
+
type: "keyword",
|
|
34
|
+
regex:
|
|
35
|
+
/\b(const|let|var|function|return|if|else|for|while|do|break|continue|switch|case|default|import|export|from|as|class|extends|implements|interface|type|enum|namespace|async|await|try|catch|finally|throw|new|this|super|static|public|private|protected|readonly|abstract|void|null|undefined|true|false|typeof|instanceof|delete|in|of)\b/,
|
|
36
|
+
},
|
|
37
|
+
{ type: "operator", regex: /([+\-*/%=<>!&|^~?:]+)/ },
|
|
38
|
+
{ type: "number", regex: /\b(0x[a-fA-F0-9]+|0b[01]+|\d+\.?\d*(?:e[+-]?\d+)?)\b/ },
|
|
39
|
+
{ type: "function", regex: /\b([a-zA-Z_$][\w$]*)\s*(?=\()/ },
|
|
40
|
+
{ type: "property", regex: /\.([a-zA-Z_$][\w$]*)/ },
|
|
41
|
+
{ type: "punctuation", regex: /([{}[\]();,])/ },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
while (currentPos < line.length) {
|
|
45
|
+
let matched = false
|
|
46
|
+
|
|
47
|
+
for (const { type, regex } of patterns) {
|
|
48
|
+
const match = line.slice(currentPos).match(regex)
|
|
49
|
+
if (match && match.index === 0) {
|
|
50
|
+
tokens.push({ type, value: match[0] })
|
|
51
|
+
currentPos += match[0].length
|
|
52
|
+
matched = true
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!matched) {
|
|
58
|
+
// Regular text or whitespace
|
|
59
|
+
const nextSpecialChar = line.slice(currentPos).search(/["'`/\w.+\-*/%=<>!&|^~?:;,()[\]{}#]/)
|
|
60
|
+
if (nextSpecialChar === -1) {
|
|
61
|
+
tokens.push({ type: "text", value: line.slice(currentPos) })
|
|
62
|
+
break
|
|
63
|
+
} else if (nextSpecialChar > 0) {
|
|
64
|
+
tokens.push({ type: "text", value: line.slice(currentPos, currentPos + nextSpecialChar) })
|
|
65
|
+
currentPos += nextSpecialChar
|
|
66
|
+
} else {
|
|
67
|
+
tokens.push({ type: "text", value: line[currentPos] })
|
|
68
|
+
currentPos++
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div key={i} className="table-row">
|
|
75
|
+
<span className="table-cell pr-4 text-right select-none text-muted-foreground/40 w-8 align-top">{i + 1}</span>
|
|
76
|
+
<span className="table-cell align-top">
|
|
77
|
+
{tokens.length === 0 ? (
|
|
78
|
+
<span> </span>
|
|
79
|
+
) : (
|
|
80
|
+
tokens.map((token, j) => (
|
|
81
|
+
<span key={j} className={`token-${token.type}`}>
|
|
82
|
+
{token.value}
|
|
83
|
+
</span>
|
|
84
|
+
))
|
|
85
|
+
)}
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="relative group my-2">
|
|
94
|
+
{/* Header - always visible */}
|
|
95
|
+
<div className="bg-muted/50 dark:bg-muted/30 px-4 py-2 rounded-t-xl border border-b-0 border-border/50 flex items-center justify-between">
|
|
96
|
+
{/* Left section: Safari-style dots + filename */}
|
|
97
|
+
<div className="flex items-center gap-3">
|
|
98
|
+
{/* Safari-style window controls */}
|
|
99
|
+
<div className="flex items-center gap-1.5">
|
|
100
|
+
<div className="w-3 h-3 rounded-full bg-red-500/80 dark:bg-red-500/60" />
|
|
101
|
+
<div className="w-3 h-3 rounded-full bg-yellow-500/80 dark:bg-yellow-500/60" />
|
|
102
|
+
<div className="w-3 h-3 rounded-full bg-green-500/80 dark:bg-green-500/60" />
|
|
103
|
+
</div>
|
|
104
|
+
{/* Filename or "Code" */}
|
|
105
|
+
<span className="text-xs font-mono text-foreground">{filename || "Code"}</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Right section: Language + Copy button */}
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<span className="text-xs text-muted-foreground/60 font-mono uppercase tracking-wide">{language}</span>
|
|
111
|
+
<button
|
|
112
|
+
onClick={handleCopy}
|
|
113
|
+
className="p-1.5 rounded-md hover:bg-muted/50 transition-colors"
|
|
114
|
+
aria-label="Copy code"
|
|
115
|
+
>
|
|
116
|
+
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Copy className="h-4 w-4 text-muted-foreground" />}
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Code content */}
|
|
122
|
+
<div className="bg-gray-200/50 dark:bg-[#0d1117] rounded-b-xl overflow-x-auto border border-border/50">
|
|
123
|
+
<pre className="p-2 text-[13px] font-mono leading-relaxed text-gray-800 dark:text-gray-200">
|
|
124
|
+
<code className="table w-full">{highlightCode(code, language)}</code>
|
|
125
|
+
</pre>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
interface ColumnsProps {
|
|
2
|
+
children: React.ReactNode
|
|
3
|
+
cols?: {
|
|
4
|
+
sm?: 1 | 2 | 3 | 4
|
|
5
|
+
md?: 1 | 2 | 3 | 4
|
|
6
|
+
lg?: 1 | 2 | 3 | 4
|
|
7
|
+
xl?: 1 | 2 | 3 | 4
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Columns({ children, cols = { sm: 1, md: 2, lg: 3 } }: ColumnsProps) {
|
|
12
|
+
const colClasses = {
|
|
13
|
+
1: "grid-cols-1",
|
|
14
|
+
2: "grid-cols-2",
|
|
15
|
+
3: "grid-cols-3",
|
|
16
|
+
4: "grid-cols-4",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const smClass = cols.sm ? colClasses[cols.sm] : "grid-cols-1"
|
|
20
|
+
const mdClass = cols.md ? `md:${colClasses[cols.md]}` : ""
|
|
21
|
+
const lgClass = cols.lg ? `lg:${colClasses[cols.lg]}` : ""
|
|
22
|
+
const xlClass = cols.xl ? `xl:${colClasses[cols.xl]}` : ""
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={`grid ${smClass} ${mdClass} ${lgClass} ${xlClass} gap-4 my-6`}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ColumnProps {
|
|
32
|
+
children: React.ReactNode
|
|
33
|
+
span?: 1 | 2 | 3 | 4
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Column({ children, span = 1 }: ColumnProps) {
|
|
37
|
+
const spanClass = {
|
|
38
|
+
1: "col-span-1",
|
|
39
|
+
2: "col-span-2",
|
|
40
|
+
3: "col-span-3",
|
|
41
|
+
4: "col-span-4",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return <div className={spanClass[span]}>{children}</div>
|
|
45
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export const COMPONENT_TEXT_PROPS: Record<string, string[]> = {
|
|
4
|
+
// Accordion components
|
|
5
|
+
Accordion: ["title"],
|
|
6
|
+
AccordionItem: ["title"],
|
|
7
|
+
|
|
8
|
+
// Alert/Callout components
|
|
9
|
+
Alert: ["title", "description"],
|
|
10
|
+
Banner: ["title"],
|
|
11
|
+
Callout: ["title", "content"],
|
|
12
|
+
Note: ["title"],
|
|
13
|
+
Warning: ["title", "text"],
|
|
14
|
+
|
|
15
|
+
// Navigation components
|
|
16
|
+
BreadCrumb: ["title", "slug", "version"],
|
|
17
|
+
|
|
18
|
+
// Card components
|
|
19
|
+
Card: ["title", "description"],
|
|
20
|
+
ImageCard: ["title", "description", "alt"],
|
|
21
|
+
|
|
22
|
+
// Media components
|
|
23
|
+
Image: ["alt", "caption"],
|
|
24
|
+
Video: ["caption"],
|
|
25
|
+
Frame: ["title"],
|
|
26
|
+
Mermaid: ["caption"],
|
|
27
|
+
|
|
28
|
+
// Interactive components
|
|
29
|
+
Tooltip: ["content"],
|
|
30
|
+
|
|
31
|
+
// Code components
|
|
32
|
+
CodeBlock: ["filename"],
|
|
33
|
+
|
|
34
|
+
// Step components
|
|
35
|
+
Step: ["title"],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractComponentPropsText(mdx: string): string {
|
|
39
|
+
return mdx.replace(
|
|
40
|
+
/<([A-Z][\w]*)\b([^/>]*)\/>/g,
|
|
41
|
+
(_, component, props) => {
|
|
42
|
+
const searchableProps = COMPONENT_TEXT_PROPS[component]
|
|
43
|
+
if (!searchableProps) return " "
|
|
44
|
+
|
|
45
|
+
let extracted = ""
|
|
46
|
+
|
|
47
|
+
for (const prop of searchableProps) {
|
|
48
|
+
const match = props.match(
|
|
49
|
+
new RegExp(`${prop}="([^"]+)"`, "i")
|
|
50
|
+
)
|
|
51
|
+
if (match) {
|
|
52
|
+
extracted += " " + match[1]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return extracted || " "
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function extractSearchText(mdx: string): string {
|
|
62
|
+
return extractComponentPropsText(mdx)
|
|
63
|
+
// 2. Remove fenced code blocks
|
|
64
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
65
|
+
|
|
66
|
+
// 3. Remove JSX blocks with children
|
|
67
|
+
.replace(/<([A-Z][\w]*)\b[^>]*>[\s\S]*?<\/\1>/g, " ")
|
|
68
|
+
|
|
69
|
+
// 4. Remove remaining JSX & HTML
|
|
70
|
+
.replace(/<\/?[A-Za-z][^>]*>/g, " ")
|
|
71
|
+
|
|
72
|
+
// 5. Remove inline code
|
|
73
|
+
.replace(/`[^`]+`/g, " ")
|
|
74
|
+
|
|
75
|
+
// 6. Remove markdown links (keep text)
|
|
76
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
77
|
+
|
|
78
|
+
// 7. Remove markdown noise
|
|
79
|
+
.replace(/[#>*_~=-]+/g, " ")
|
|
80
|
+
|
|
81
|
+
// 8. Normalize whitespace
|
|
82
|
+
.replace(/\s+/g, " ")
|
|
83
|
+
.trim()
|
|
84
|
+
.slice(0, 1000)
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { Code2, Wifi } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
export function DevModeBadge() {
|
|
7
|
+
const [isConnected, setIsConnected] = useState(true)
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (process.env.NODE_ENV !== "development") return
|
|
11
|
+
|
|
12
|
+
// Check WebSocket connection status
|
|
13
|
+
const checkConnection = () => {
|
|
14
|
+
setIsConnected(navigator.onLine)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
window.addEventListener("online", checkConnection)
|
|
18
|
+
window.addEventListener("offline", checkConnection)
|
|
19
|
+
|
|
20
|
+
return () => {
|
|
21
|
+
window.removeEventListener("online", checkConnection)
|
|
22
|
+
window.removeEventListener("offline", checkConnection)
|
|
23
|
+
}
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
if (process.env.NODE_ENV !== "development") return null
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="fixed top-20 left-4 z-40 flex items-center gap-2 px-3 py-1.5 bg-orange-500/10 text-orange-600 dark:text-orange-400 border border-orange-500/20 rounded-full text-xs font-medium">
|
|
30
|
+
<Code2 className="h-3 w-3" />
|
|
31
|
+
<span>Dev Mode</span>
|
|
32
|
+
<div className={`h-2 w-2 rounded-full ${isConnected ? "bg-green-500" : "bg-red-500"} animate-pulse`} />
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ReactNode, useEffect, useRef } from "react"
|
|
4
|
+
import { MobileDocLayout } from "./mobile-doc-layout"
|
|
5
|
+
import { useTabContext } from "./tab-context"
|
|
6
|
+
import type { SpecraConfig } from "@/lib/config"
|
|
7
|
+
import type { Doc } from "@/lib/mdx"
|
|
8
|
+
|
|
9
|
+
interface DocLayoutWrapperProps {
|
|
10
|
+
header: ReactNode
|
|
11
|
+
docs: Doc[]
|
|
12
|
+
version: string
|
|
13
|
+
content: ReactNode
|
|
14
|
+
toc: ReactNode
|
|
15
|
+
config: SpecraConfig
|
|
16
|
+
currentPageTabGroup?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DocLayoutWrapper({ header, docs, version, content, toc, config, currentPageTabGroup }: DocLayoutWrapperProps) {
|
|
20
|
+
// Use global tab context instead of local state
|
|
21
|
+
const { activeTabGroup, setActiveTabGroup } = useTabContext()
|
|
22
|
+
const lastPageTabGroupRef = useRef<string | undefined>(undefined)
|
|
23
|
+
const isInitialMount = useRef(true)
|
|
24
|
+
|
|
25
|
+
// Set tab based on page's tab group
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// On initial mount, always set to current page's tab group
|
|
28
|
+
if (isInitialMount.current && currentPageTabGroup) {
|
|
29
|
+
setActiveTabGroup(currentPageTabGroup)
|
|
30
|
+
lastPageTabGroupRef.current = currentPageTabGroup
|
|
31
|
+
isInitialMount.current = false
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// On subsequent renders, only update if navigating to a different page
|
|
36
|
+
if (currentPageTabGroup && lastPageTabGroupRef.current !== currentPageTabGroup) {
|
|
37
|
+
setActiveTabGroup(currentPageTabGroup)
|
|
38
|
+
lastPageTabGroupRef.current = currentPageTabGroup
|
|
39
|
+
}
|
|
40
|
+
}, [currentPageTabGroup, setActiveTabGroup])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<MobileDocLayout
|
|
44
|
+
header={header}
|
|
45
|
+
docs={docs}
|
|
46
|
+
version={version}
|
|
47
|
+
content={content}
|
|
48
|
+
toc={toc}
|
|
49
|
+
config={config}
|
|
50
|
+
activeTabGroup={activeTabGroup}
|
|
51
|
+
onTabChange={setActiveTabGroup}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ExternalLink, FileEdit } from "lucide-react"
|
|
2
|
+
import { MDXRemote, type MDXRemoteProps } from "next-mdx-remote/rsc"
|
|
3
|
+
import remarkGfm from "remark-gfm"
|
|
4
|
+
import rehypeSlug from "rehype-slug"
|
|
5
|
+
import { remarkCodeMeta } from "@/lib/remark-code-meta"
|
|
6
|
+
import { mdxComponents } from "./mdx-components"
|
|
7
|
+
import type { ComponentPropsWithoutRef } from "react"
|
|
8
|
+
import { DocNavigation } from "./doc-navigation"
|
|
9
|
+
import { Breadcrumb } from "./breadcrumb"
|
|
10
|
+
import { DocMetadata } from "./doc-metadata"
|
|
11
|
+
import { DraftBadge } from "./draft-badge"
|
|
12
|
+
import { DocTags } from "./doc-tags"
|
|
13
|
+
import { SearchHighlight } from "./search-highlight"
|
|
14
|
+
import type { DocMeta } from "@/lib/mdx"
|
|
15
|
+
import { getConfig, processContentWithEnv, type SpecraConfig } from "@/lib/config"
|
|
16
|
+
|
|
17
|
+
interface DocLayoutProps {
|
|
18
|
+
meta: DocMeta
|
|
19
|
+
content: string
|
|
20
|
+
previousDoc?: {
|
|
21
|
+
title: string
|
|
22
|
+
slug: string
|
|
23
|
+
}
|
|
24
|
+
nextDoc?: {
|
|
25
|
+
title: string
|
|
26
|
+
slug: string
|
|
27
|
+
}
|
|
28
|
+
version: string
|
|
29
|
+
slug: string
|
|
30
|
+
config: SpecraConfig
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export async function DocLayout({ content, meta, previousDoc, nextDoc, version, slug, config }: DocLayoutProps) {
|
|
36
|
+
const isDevelopment = process.env.NODE_ENV === "development"
|
|
37
|
+
// const config = getConfig()
|
|
38
|
+
|
|
39
|
+
// Process content with environment variables
|
|
40
|
+
const processedContent = processContentWithEnv(content, config)
|
|
41
|
+
|
|
42
|
+
// Build edit URL if configured
|
|
43
|
+
const editUrl = config.features?.editUrl && typeof config.features.editUrl === 'string'
|
|
44
|
+
? `${config.features.editUrl}/${version}/${slug}.mdx`
|
|
45
|
+
: null
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<article className="flex-1 min-w-0">
|
|
49
|
+
<SearchHighlight />
|
|
50
|
+
|
|
51
|
+
{config.navigation?.showBreadcrumbs && (
|
|
52
|
+
<Breadcrumb version={version} slug={slug} title={meta.title} />
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
{isDevelopment && meta.draft && <DraftBadge />}
|
|
56
|
+
|
|
57
|
+
<div className="mb-8">
|
|
58
|
+
<h1 className="text-4xl font-bold tracking-tight mb-3 text-foreground">{meta.title}</h1>
|
|
59
|
+
{meta.description && <p className="text-lg text-muted-foreground leading-relaxed">{meta.description}</p>}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<DocMetadata meta={meta} config={config} />
|
|
63
|
+
|
|
64
|
+
<div className="prose prose-slate dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-h1:text-4xl prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-4 prose-h3:text-2xl prose-h3:mt-8 prose-h3:mb-3 prose-p:text-base prose-p:leading-7 prose-p:text-muted-foreground prose-p:mb-4 prose-a:font-normal prose-a:transition-all prose-code:text-primary prose-code:bg-muted/50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-[13px] prose-code:font-mono prose-code:border prose-code:border-border/50 prose-code:before:content-none prose-code:after:content-none prose-pre:bg-transparent prose-pre:p-0 prose-ul:list-disc prose-ul:list-inside prose-ul:space-y-2 prose-ul:mb-4 prose-ol:list-decimal prose-ol:list-inside prose-ol:space-y-2 prose-ol:mb-4 prose-li:leading-7 prose-li:text-muted-foreground prose-strong:text-foreground prose-strong:font-semibold">
|
|
65
|
+
<MDXRemote
|
|
66
|
+
source={processedContent}
|
|
67
|
+
options={{
|
|
68
|
+
parseFrontmatter: false,
|
|
69
|
+
mdxOptions: {
|
|
70
|
+
remarkPlugins: [remarkGfm, remarkCodeMeta],
|
|
71
|
+
rehypePlugins: [rehypeSlug],
|
|
72
|
+
development: false,
|
|
73
|
+
},
|
|
74
|
+
}}
|
|
75
|
+
components={mdxComponents as any}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{config.features?.showTags && meta.tags && meta.tags.length > 0 && <DocTags tags={meta.tags} />}
|
|
80
|
+
|
|
81
|
+
{(editUrl || config.social?.github) && (
|
|
82
|
+
<div className="mt-12 pt-6 border-t border-border flex items-center justify-between">
|
|
83
|
+
{editUrl ? (
|
|
84
|
+
<a
|
|
85
|
+
href={editUrl}
|
|
86
|
+
target="_blank"
|
|
87
|
+
rel="noopener noreferrer"
|
|
88
|
+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
89
|
+
>
|
|
90
|
+
<FileEdit className="h-4 w-4" />
|
|
91
|
+
Edit this page
|
|
92
|
+
</a>
|
|
93
|
+
) : <div />}
|
|
94
|
+
{config.social?.github && (
|
|
95
|
+
<a
|
|
96
|
+
href={`${config.social.github}/issues/new`}
|
|
97
|
+
target="_blank"
|
|
98
|
+
rel="noopener noreferrer"
|
|
99
|
+
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
100
|
+
>
|
|
101
|
+
<ExternalLink className="h-4 w-4" />
|
|
102
|
+
Report an issue
|
|
103
|
+
</a>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<DocNavigation previousDoc={previousDoc} nextDoc={nextDoc} version={version} />
|
|
109
|
+
</article>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function DocLoading() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="max-w-4xl mx-auto px-6 py-8">
|
|
4
|
+
<div className="animate-pulse space-y-4">
|
|
5
|
+
<div className="h-8 bg-gray-200 rounded w-3/4" />
|
|
6
|
+
<div className="h-4 bg-gray-200 rounded w-1/2" />
|
|
7
|
+
<div className="space-y-3 mt-8">
|
|
8
|
+
<div className="h-4 bg-gray-200 rounded" />
|
|
9
|
+
<div className="h-4 bg-gray-200 rounded w-5/6" />
|
|
10
|
+
<div className="h-4 bg-gray-200 rounded w-4/6" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Clock, Calendar, User } from "lucide-react"
|
|
2
|
+
import type { DocMeta } from "@/lib/mdx"
|
|
3
|
+
import { getConfig, SpecraConfig } from "@/lib/config"
|
|
4
|
+
|
|
5
|
+
interface DocMetadataProps {
|
|
6
|
+
meta: DocMeta
|
|
7
|
+
config: SpecraConfig
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DocMetadata({ meta, config }: DocMetadataProps) {
|
|
11
|
+
// Server component - can use getConfig directly
|
|
12
|
+
// const config = getConfig()
|
|
13
|
+
|
|
14
|
+
const showReadingTime = config.features?.showReadingTime && meta.reading_time
|
|
15
|
+
const showLastUpdated = config.features?.showLastUpdated && meta.last_updated
|
|
16
|
+
const showAuthors = config.features?.showAuthors && meta.authors?.length
|
|
17
|
+
|
|
18
|
+
const hasMetadata = showReadingTime || showLastUpdated || showAuthors
|
|
19
|
+
|
|
20
|
+
if (!hasMetadata) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground border-b border-border pb-4 mb-6">
|
|
26
|
+
{showReadingTime && (
|
|
27
|
+
<div className="flex items-center gap-1.5">
|
|
28
|
+
<Clock className="h-4 w-4" />
|
|
29
|
+
<span>{meta.reading_time} min read</span>
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
{showLastUpdated && meta.last_updated && (
|
|
34
|
+
<div className="flex items-center gap-1.5">
|
|
35
|
+
<Calendar className="h-4 w-4" />
|
|
36
|
+
<span>Updated {new Date(meta.last_updated).toLocaleDateString()}</span>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{showAuthors && (
|
|
41
|
+
<div className="flex items-center gap-1.5">
|
|
42
|
+
<User className="h-4 w-4" />
|
|
43
|
+
<span>
|
|
44
|
+
{meta.authors!.map((author, idx) => (
|
|
45
|
+
<span key={author.id}>
|
|
46
|
+
{author.name || author.id}
|
|
47
|
+
{idx < meta.authors!.length - 1 && ", "}
|
|
48
|
+
</span>
|
|
49
|
+
))}
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|