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,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>&nbsp;</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
+ }