@geenius/docs 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/.changeset/config.json +11 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.nvmrc +1 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +11 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/SECURITY.md +15 -0
- package/SUPPORT.md +8 -0
- package/package.json +58 -0
- package/packages/convex/README.md +1 -0
- package/packages/convex/package.json +12 -0
- package/packages/convex/src/convex.config.ts +3 -0
- package/packages/convex/src/index.ts +3 -0
- package/packages/convex/src/mutations.ts +270 -0
- package/packages/convex/src/queries.ts +175 -0
- package/packages/convex/src/schema.ts +55 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +36 -0
- package/packages/react/src/DocsLayout.tsx +116 -0
- package/packages/react/src/DocsProvider.tsx +93 -0
- package/packages/react/src/RouterDocsContent.tsx +148 -0
- package/packages/react/src/RouterDocsLayout.tsx +161 -0
- package/packages/react/src/components/Breadcrumbs.tsx +34 -0
- package/packages/react/src/components/DocPage.tsx +191 -0
- package/packages/react/src/components/DocSearch.tsx +140 -0
- package/packages/react/src/components/DocSidebar.tsx +86 -0
- package/packages/react/src/components/DocsLayout.tsx +62 -0
- package/packages/react/src/components/EditButton.tsx +26 -0
- package/packages/react/src/components/PageNavigation.tsx +45 -0
- package/packages/react/src/components/TableOfContents.tsx +46 -0
- package/packages/react/src/components/VersionSelector.tsx +60 -0
- package/packages/react/src/components/index.ts +9 -0
- package/packages/react/src/hooks/index.ts +8 -0
- package/packages/react/src/hooks/useDocSearch.ts +55 -0
- package/packages/react/src/hooks/useDocs.ts +57 -0
- package/packages/react/src/hooks/useDocsAdmin.ts +151 -0
- package/packages/react/src/hooks/useTableOfContents.ts +66 -0
- package/packages/react/src/index.ts +38 -0
- package/packages/react/src/pages/DocSearchPage.tsx +129 -0
- package/packages/react/src/pages/DocViewPage.tsx +158 -0
- package/packages/react/src/pages/DocsAdminPage.tsx +330 -0
- package/packages/react/src/pages/DocsIndexPage.tsx +172 -0
- package/packages/react/src/pages/index.ts +4 -0
- package/packages/react/src/useDocs.ts +58 -0
- package/packages/react/tsup.config.ts +12 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +37 -0
- package/packages/react-css/src/DocsLayout.tsx +117 -0
- package/packages/react-css/src/DocsProvider.tsx +93 -0
- package/packages/react-css/src/RouterDocsContent.tsx +60 -0
- package/packages/react-css/src/RouterDocsLayout.tsx +101 -0
- package/packages/react-css/src/components/DocPage.tsx +21 -0
- package/packages/react-css/src/components/DocSearch.tsx +55 -0
- package/packages/react-css/src/components/DocSidebar.tsx +56 -0
- package/packages/react-css/src/components/DocsLayout.tsx +28 -0
- package/packages/react-css/src/components/common.tsx +93 -0
- package/packages/react-css/src/components/index.ts +5 -0
- package/packages/react-css/src/hooks/index.ts +2 -0
- package/packages/react-css/src/index.ts +6 -0
- package/packages/react-css/src/index.tsx +3 -0
- package/packages/react-css/src/pages/DocViewPage.tsx +78 -0
- package/packages/react-css/src/pages/DocsAdminPage.tsx +101 -0
- package/packages/react-css/src/pages/DocsIndexPage.tsx +68 -0
- package/packages/react-css/src/pages/index.ts +3 -0
- package/packages/react-css/src/styles.css +1271 -0
- package/packages/react-css/src/useDocs.ts +58 -0
- package/packages/react-css/tsconfig.json +19 -0
- package/packages/react-css/tsup.config.ts +10 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +31 -0
- package/packages/shared/src/__tests__/docs.test.ts +69 -0
- package/packages/shared/src/config.ts +80 -0
- package/packages/shared/src/index.ts +179 -0
- package/packages/shared/src/providers/astro.ts +94 -0
- package/packages/shared/src/providers/fumadocs.ts +116 -0
- package/packages/shared/src/providers/internal.ts +80 -0
- package/packages/shared/src/types.ts +73 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/shared/tsup.config.ts +12 -0
- package/packages/shared/vitest.config.ts +4 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +33 -0
- package/packages/solidjs/src/DocsLayout.tsx +87 -0
- package/packages/solidjs/src/DocsProvider.tsx +95 -0
- package/packages/solidjs/src/RouterDocsContent.tsx +147 -0
- package/packages/solidjs/src/RouterDocsLayout.tsx +161 -0
- package/packages/solidjs/src/components/Breadcrumbs.tsx +27 -0
- package/packages/solidjs/src/components/DocPage.tsx +110 -0
- package/packages/solidjs/src/components/DocSearch.tsx +81 -0
- package/packages/solidjs/src/components/DocSidebar.tsx +92 -0
- package/packages/solidjs/src/components/DocsLayout.tsx +38 -0
- package/packages/solidjs/src/components/EditButton.tsx +15 -0
- package/packages/solidjs/src/components/PageNavigation.tsx +31 -0
- package/packages/solidjs/src/components/TableOfContents.tsx +41 -0
- package/packages/solidjs/src/components/VersionSelector.tsx +30 -0
- package/packages/solidjs/src/components/index.ts +9 -0
- package/packages/solidjs/src/createDocs.ts +62 -0
- package/packages/solidjs/src/index.ts +28 -0
- package/packages/solidjs/src/pages/DocSearchPage.tsx +72 -0
- package/packages/solidjs/src/pages/DocViewPage.tsx +80 -0
- package/packages/solidjs/src/pages/DocsAdminPage.tsx +123 -0
- package/packages/solidjs/src/pages/DocsIndexPage.tsx +85 -0
- package/packages/solidjs/src/pages/index.ts +4 -0
- package/packages/solidjs/src/primitives/createDocSearch.ts +42 -0
- package/packages/solidjs/src/primitives/createDocs.ts +35 -0
- package/packages/solidjs/src/primitives/createDocsAdmin.ts +63 -0
- package/packages/solidjs/src/primitives/createTableOfContents.ts +51 -0
- package/packages/solidjs/src/primitives/index.ts +4 -0
- package/packages/solidjs/tsup.config.ts +12 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +36 -0
- package/packages/solidjs-css/src/DocsLayout.tsx +106 -0
- package/packages/solidjs-css/src/DocsProvider.tsx +95 -0
- package/packages/solidjs-css/src/RouterDocsContent.tsx +54 -0
- package/packages/solidjs-css/src/RouterDocsLayout.tsx +104 -0
- package/packages/solidjs-css/src/createDocs.ts +62 -0
- package/packages/solidjs-css/src/index.ts +7 -0
- package/packages/solidjs-css/src/index.tsx +17 -0
- package/packages/solidjs-css/src/pages/DocViewPage.tsx +111 -0
- package/packages/solidjs-css/src/pages/DocsAdminPage.tsx +332 -0
- package/packages/solidjs-css/src/pages/DocsIndexPage.tsx +116 -0
- package/packages/solidjs-css/src/pages/index.ts +3 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/src/styles.css +1271 -0
- package/packages/solidjs-css/tsconfig.json +20 -0
- package/packages/solidjs-css/tsup.config.ts +10 -0
- package/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// src/components/docs/DocsLayout.tsx
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react'
|
|
4
|
+
import { Link, useLocation } from '@tanstack/react-router'
|
|
5
|
+
import { Book, Menu, X, Search, ChevronRight } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
export interface DocItem {
|
|
8
|
+
slug: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
category: string
|
|
12
|
+
order: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DocsLayoutProps {
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
docs: DocItem[]
|
|
18
|
+
currentSlug?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DocsLayout: React.FC<DocsLayoutProps> = ({
|
|
22
|
+
children,
|
|
23
|
+
docs,
|
|
24
|
+
currentSlug,
|
|
25
|
+
}) => {
|
|
26
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
|
27
|
+
const location = useLocation()
|
|
28
|
+
|
|
29
|
+
// Group docs by category
|
|
30
|
+
const groupedDocs = docs.reduce(
|
|
31
|
+
(acc, doc) => {
|
|
32
|
+
if (!acc[doc.category]) {
|
|
33
|
+
acc[doc.category] = []
|
|
34
|
+
}
|
|
35
|
+
acc[doc.category].push(doc)
|
|
36
|
+
return acc
|
|
37
|
+
},
|
|
38
|
+
{} as Record<string, DocItem[]>,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Sort categories and docs within each category
|
|
42
|
+
const sortedCategories = Object.entries(groupedDocs).sort(([a], [b]) => {
|
|
43
|
+
const categoryOrder = [
|
|
44
|
+
'Introduction',
|
|
45
|
+
'Getting Started',
|
|
46
|
+
'Guides',
|
|
47
|
+
'API',
|
|
48
|
+
'Advanced',
|
|
49
|
+
]
|
|
50
|
+
return categoryOrder.indexOf(a) - categoryOrder.indexOf(b)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="min-h-screen bg-bg">
|
|
55
|
+
{/* Mobile Header */}
|
|
56
|
+
<div className="lg:hidden sticky top-0 z-40 flex items-center justify-between px-4 py-3 bg-card border-b border-border">
|
|
57
|
+
<Link to="/" className="flex items-center gap-2 text-primary">
|
|
58
|
+
<Book className="w-5 h-5" />
|
|
59
|
+
<span className="font-bold">Documentation</span>
|
|
60
|
+
</Link>
|
|
61
|
+
<button
|
|
62
|
+
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
63
|
+
className="p-2 rounded-lg hover:bg-bg-muted transition-colors"
|
|
64
|
+
>
|
|
65
|
+
{isSidebarOpen ? (
|
|
66
|
+
<X className="w-5 h-5" />
|
|
67
|
+
) : (
|
|
68
|
+
<Menu className="w-5 h-5" />
|
|
69
|
+
)}
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="flex">
|
|
74
|
+
{/* Sidebar */}
|
|
75
|
+
<aside
|
|
76
|
+
className={`
|
|
77
|
+
fixed lg:sticky top-16 left-0 z-30 w-72 h-[calc(100vh-4rem)] overflow-y-auto
|
|
78
|
+
bg-card border-r border-border transition-transform duration-300
|
|
79
|
+
lg:translate-x-0
|
|
80
|
+
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
|
|
81
|
+
`}
|
|
82
|
+
>
|
|
83
|
+
<div className="p-6">
|
|
84
|
+
{/* Logo */}
|
|
85
|
+
<Link
|
|
86
|
+
to="/"
|
|
87
|
+
className="hidden lg:flex items-center gap-2 text-primary mb-8"
|
|
88
|
+
>
|
|
89
|
+
<Book className="w-6 h-6" />
|
|
90
|
+
<span className="font-bold text-lg">Documentation</span>
|
|
91
|
+
</Link>
|
|
92
|
+
|
|
93
|
+
{/* Search */}
|
|
94
|
+
<div className="relative mb-6">
|
|
95
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
placeholder="Search docs..."
|
|
99
|
+
className="w-full pl-10 pr-4 py-2.5 bg-bg-muted border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{/* Navigation */}
|
|
104
|
+
<nav className="space-y-6">
|
|
105
|
+
{sortedCategories.map(([category, categoryDocs]) => (
|
|
106
|
+
<div key={category}>
|
|
107
|
+
<h3 className="text-xs font-bold text-text-muted uppercase tracking-wider mb-3">
|
|
108
|
+
{category}
|
|
109
|
+
</h3>
|
|
110
|
+
<ul className="space-y-1">
|
|
111
|
+
{categoryDocs
|
|
112
|
+
.sort((a, b) => a.order - b.order)
|
|
113
|
+
.map((doc) => {
|
|
114
|
+
const isActive = currentSlug === doc.slug
|
|
115
|
+
return (
|
|
116
|
+
<li key={doc.slug}>
|
|
117
|
+
<Link
|
|
118
|
+
to="/docs/$slug"
|
|
119
|
+
params={{ slug: doc.slug }}
|
|
120
|
+
activeOptions={{ exact: true }}
|
|
121
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
122
|
+
className={`
|
|
123
|
+
flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
|
124
|
+
${
|
|
125
|
+
isActive
|
|
126
|
+
? 'bg-primary/10 text-primary'
|
|
127
|
+
: 'text-text-muted hover:bg-bg-muted hover:text-text-main'
|
|
128
|
+
}
|
|
129
|
+
`}
|
|
130
|
+
>
|
|
131
|
+
<ChevronRight
|
|
132
|
+
className={`w-4 h-4 transition-transform ${isActive ? 'rotate-90' : ''}`}
|
|
133
|
+
/>
|
|
134
|
+
{doc.title}
|
|
135
|
+
</Link>
|
|
136
|
+
</li>
|
|
137
|
+
)
|
|
138
|
+
})}
|
|
139
|
+
</ul>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
|
+
</nav>
|
|
143
|
+
</div>
|
|
144
|
+
</aside>
|
|
145
|
+
|
|
146
|
+
{/* Overlay for mobile */}
|
|
147
|
+
{isSidebarOpen && (
|
|
148
|
+
<div
|
|
149
|
+
className="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
|
150
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Main Content */}
|
|
155
|
+
<main className="flex-1 min-w-0">
|
|
156
|
+
<div className="max-w-4xl mx-auto px-6 py-12">{children}</div>
|
|
157
|
+
</main>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { BreadcrumbItem } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
interface BreadcrumbsProps {
|
|
5
|
+
items: BreadcrumbItem[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Breadcrumbs({ items }: BreadcrumbsProps) {
|
|
9
|
+
if (items.length === 0) return null
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<nav className="flex items-center gap-1.5 text-sm" aria-label="Breadcrumb">
|
|
13
|
+
{items.map((item, idx) => (
|
|
14
|
+
<React.Fragment key={idx}>
|
|
15
|
+
{idx > 0 && (
|
|
16
|
+
<svg className="h-3.5 w-3.5 shrink-0 text-white/20" viewBox="0 0 16 16" fill="none">
|
|
17
|
+
<path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" />
|
|
18
|
+
</svg>
|
|
19
|
+
)}
|
|
20
|
+
{idx === items.length - 1 ? (
|
|
21
|
+
<span className="truncate text-white/60">{item.title}</span>
|
|
22
|
+
) : (
|
|
23
|
+
<a
|
|
24
|
+
href={item.href}
|
|
25
|
+
className="truncate text-white/40 transition-colors hover:text-white/70"
|
|
26
|
+
>
|
|
27
|
+
{item.title}
|
|
28
|
+
</a>
|
|
29
|
+
)}
|
|
30
|
+
</React.Fragment>
|
|
31
|
+
))}
|
|
32
|
+
</nav>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { DocPage as DocPageType } from '@geenius-docs/shared'
|
|
3
|
+
import { slugify } from '@geenius-docs/shared'
|
|
4
|
+
|
|
5
|
+
interface DocPageProps {
|
|
6
|
+
page: DocPageType
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function renderCallout(type: string, content: string) {
|
|
10
|
+
const styles: Record<string, { bg: string; border: string; icon: string; label: string }> = {
|
|
11
|
+
NOTE: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', icon: 'ℹ️', label: 'Note' },
|
|
12
|
+
WARNING: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', icon: '⚠️', label: 'Warning' },
|
|
13
|
+
TIP: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', icon: '💡', label: 'Tip' },
|
|
14
|
+
IMPORTANT: { bg: 'bg-purple-500/10', border: 'border-purple-500/30', icon: '❗', label: 'Important' },
|
|
15
|
+
CAUTION: { bg: 'bg-red-500/10', border: 'border-red-500/30', icon: '🔴', label: 'Caution' },
|
|
16
|
+
}
|
|
17
|
+
const s = styles[type] ?? styles.NOTE
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={`${s.bg} ${s.border} my-4 rounded-lg border-l-4 p-4`}>
|
|
21
|
+
<p className="mb-1 text-sm font-semibold">
|
|
22
|
+
{s.icon} {s.label}
|
|
23
|
+
</p>
|
|
24
|
+
<p className="text-sm leading-relaxed text-white/80">{content}</p>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderMdxContent(content: string) {
|
|
30
|
+
const lines = content.split('\n')
|
|
31
|
+
const elements: React.ReactNode[] = []
|
|
32
|
+
let i = 0
|
|
33
|
+
let codeBlock: string[] | null = null
|
|
34
|
+
let codeLang = ''
|
|
35
|
+
|
|
36
|
+
while (i < lines.length) {
|
|
37
|
+
const line = lines[i]
|
|
38
|
+
|
|
39
|
+
// Fenced code blocks
|
|
40
|
+
if (line.startsWith('```')) {
|
|
41
|
+
if (codeBlock === null) {
|
|
42
|
+
codeBlock = []
|
|
43
|
+
codeLang = line.slice(3).trim()
|
|
44
|
+
} else {
|
|
45
|
+
elements.push(
|
|
46
|
+
<div key={i} className="group relative my-4">
|
|
47
|
+
<div className="flex items-center justify-between rounded-t-lg bg-white/5 px-4 py-2 text-xs text-white/50">
|
|
48
|
+
<span>{codeLang || 'code'}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<pre className="overflow-x-auto rounded-b-lg bg-black/30 p-4 text-sm leading-relaxed">
|
|
51
|
+
<code>{codeBlock.join('\n')}</code>
|
|
52
|
+
</pre>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
codeBlock = null
|
|
56
|
+
codeLang = ''
|
|
57
|
+
}
|
|
58
|
+
i++
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (codeBlock !== null) {
|
|
63
|
+
codeBlock.push(line)
|
|
64
|
+
i++
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Callouts [!NOTE], [!WARNING], etc.
|
|
69
|
+
const calloutMatch = line.match(/^>\s*\[!(NOTE|WARNING|TIP|IMPORTANT|CAUTION)\]/)
|
|
70
|
+
if (calloutMatch) {
|
|
71
|
+
const type = calloutMatch[1]
|
|
72
|
+
const contentLines: string[] = []
|
|
73
|
+
i++
|
|
74
|
+
while (i < lines.length && lines[i].startsWith('>')) {
|
|
75
|
+
contentLines.push(lines[i].replace(/^>\s?/, ''))
|
|
76
|
+
i++
|
|
77
|
+
}
|
|
78
|
+
elements.push(
|
|
79
|
+
<React.Fragment key={i}>{renderCallout(type, contentLines.join(' '))}</React.Fragment>
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Headings
|
|
85
|
+
const headingMatch = line.match(/^(#{1,4})\s+(.+)$/)
|
|
86
|
+
if (headingMatch) {
|
|
87
|
+
const level = headingMatch[1].length
|
|
88
|
+
const text = headingMatch[2]
|
|
89
|
+
const id = slugify(text)
|
|
90
|
+
const Tag = `h${level}` as keyof JSX.IntrinsicElements
|
|
91
|
+
|
|
92
|
+
const headingClasses: Record<number, string> = {
|
|
93
|
+
1: 'text-3xl font-bold mt-8 mb-4',
|
|
94
|
+
2: 'text-2xl font-bold mt-8 mb-3 pb-2 border-b border-white/10',
|
|
95
|
+
3: 'text-xl font-semibold mt-6 mb-2',
|
|
96
|
+
4: 'text-lg font-medium mt-4 mb-2',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
elements.push(
|
|
100
|
+
<Tag key={i} id={id} className={`${headingClasses[level]} scroll-mt-20 group`}>
|
|
101
|
+
<a href={`#${id}`} className="hover:text-indigo-400 transition-colors">
|
|
102
|
+
{text}
|
|
103
|
+
</a>
|
|
104
|
+
</Tag>
|
|
105
|
+
)
|
|
106
|
+
i++
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Blockquotes
|
|
111
|
+
if (line.startsWith('>')) {
|
|
112
|
+
const quoteLines: string[] = []
|
|
113
|
+
while (i < lines.length && lines[i].startsWith('>')) {
|
|
114
|
+
quoteLines.push(lines[i].replace(/^>\s?/, ''))
|
|
115
|
+
i++
|
|
116
|
+
}
|
|
117
|
+
elements.push(
|
|
118
|
+
<blockquote key={i} className="my-4 border-l-4 border-indigo-500/40 bg-white/5 py-3 pl-4 pr-4 text-white/70 italic rounded-r-lg">
|
|
119
|
+
{quoteLines.join(' ')}
|
|
120
|
+
</blockquote>
|
|
121
|
+
)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Tables
|
|
126
|
+
if (line.includes('|') && i + 1 < lines.length && lines[i + 1]?.match(/^\|[\s-:|]+\|$/)) {
|
|
127
|
+
const headers = line.split('|').filter(Boolean).map((h) => h.trim())
|
|
128
|
+
i += 2 // skip header and separator
|
|
129
|
+
const rows: string[][] = []
|
|
130
|
+
while (i < lines.length && lines[i].includes('|')) {
|
|
131
|
+
rows.push(lines[i].split('|').filter(Boolean).map((c) => c.trim()))
|
|
132
|
+
i++
|
|
133
|
+
}
|
|
134
|
+
elements.push(
|
|
135
|
+
<div key={i} className="my-4 overflow-x-auto rounded-lg border border-white/10">
|
|
136
|
+
<table className="w-full text-sm">
|
|
137
|
+
<thead>
|
|
138
|
+
<tr className="border-b border-white/10 bg-white/5">
|
|
139
|
+
{headers.map((h, idx) => (
|
|
140
|
+
<th key={idx} className="px-4 py-2.5 text-left font-semibold">{h}</th>
|
|
141
|
+
))}
|
|
142
|
+
</tr>
|
|
143
|
+
</thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
{rows.map((row, rIdx) => (
|
|
146
|
+
<tr key={rIdx} className="border-b border-white/5 last:border-none">
|
|
147
|
+
{row.map((c, cIdx) => (
|
|
148
|
+
<td key={cIdx} className="px-4 py-2 text-white/70">{c}</td>
|
|
149
|
+
))}
|
|
150
|
+
</tr>
|
|
151
|
+
))}
|
|
152
|
+
</tbody>
|
|
153
|
+
</table>
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Empty line
|
|
160
|
+
if (!line.trim()) {
|
|
161
|
+
i++
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Paragraph with inline formatting
|
|
166
|
+
const formatted = line
|
|
167
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
168
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
169
|
+
.replace(/`(.+?)`/g, '<code class="rounded bg-white/10 px-1.5 py-0.5 text-sm text-indigo-300">$1</code>')
|
|
170
|
+
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" class="text-indigo-400 hover:text-indigo-300 underline underline-offset-2 transition-colors">$1</a>')
|
|
171
|
+
|
|
172
|
+
elements.push(
|
|
173
|
+
<p
|
|
174
|
+
key={i}
|
|
175
|
+
className="my-3 leading-relaxed text-white/80"
|
|
176
|
+
dangerouslySetInnerHTML={{ __html: formatted }}
|
|
177
|
+
/>
|
|
178
|
+
)
|
|
179
|
+
i++
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return elements
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function DocPage({ page }: DocPageProps) {
|
|
186
|
+
return (
|
|
187
|
+
<article className="prose-custom max-w-none">
|
|
188
|
+
{renderMdxContent(page.content)}
|
|
189
|
+
</article>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
+
import type { SearchResult } from '@geenius-docs/shared'
|
|
3
|
+
import { highlightMatch } from '@geenius-docs/shared'
|
|
4
|
+
|
|
5
|
+
interface DocSearchProps {
|
|
6
|
+
results: SearchResult[]
|
|
7
|
+
query: string
|
|
8
|
+
onQuery: (q: string) => void
|
|
9
|
+
onSelect: (result: SearchResult) => void
|
|
10
|
+
isOpen: boolean
|
|
11
|
+
onClose: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DocSearch({ results, query, onQuery, onSelect, isOpen, onClose }: DocSearchProps) {
|
|
15
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
16
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (isOpen) {
|
|
20
|
+
inputRef.current?.focus()
|
|
21
|
+
setActiveIndex(0)
|
|
22
|
+
}
|
|
23
|
+
}, [isOpen])
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setActiveIndex(0)
|
|
27
|
+
}, [results])
|
|
28
|
+
|
|
29
|
+
const handleKeyDown = useCallback(
|
|
30
|
+
(e: React.KeyboardEvent) => {
|
|
31
|
+
if (e.key === 'ArrowDown') {
|
|
32
|
+
e.preventDefault()
|
|
33
|
+
setActiveIndex((i) => Math.min(i + 1, results.length - 1))
|
|
34
|
+
} else if (e.key === 'ArrowUp') {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
setActiveIndex((i) => Math.max(i - 1, 0))
|
|
37
|
+
} else if (e.key === 'Enter' && results[activeIndex]) {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
onSelect(results[activeIndex])
|
|
40
|
+
} else if (e.key === 'Escape') {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
onClose()
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
[results, activeIndex, onSelect, onClose]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if (!isOpen) return null
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]" onClick={onClose}>
|
|
52
|
+
{/* Backdrop */}
|
|
53
|
+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" />
|
|
54
|
+
|
|
55
|
+
{/* Modal */}
|
|
56
|
+
<div
|
|
57
|
+
className="relative z-10 w-full max-w-xl overflow-hidden rounded-2xl border border-white/10 bg-[#0d0e14] shadow-2xl"
|
|
58
|
+
onClick={(e) => e.stopPropagation()}
|
|
59
|
+
>
|
|
60
|
+
{/* Search input */}
|
|
61
|
+
<div className="flex items-center gap-3 border-b border-white/10 px-5 py-4">
|
|
62
|
+
<svg className="h-5 w-5 shrink-0 text-white/30" viewBox="0 0 20 20" fill="currentColor">
|
|
63
|
+
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
|
64
|
+
</svg>
|
|
65
|
+
<input
|
|
66
|
+
ref={inputRef}
|
|
67
|
+
type="text"
|
|
68
|
+
value={query}
|
|
69
|
+
onChange={(e) => onQuery(e.target.value)}
|
|
70
|
+
onKeyDown={handleKeyDown}
|
|
71
|
+
placeholder="Search documentation…"
|
|
72
|
+
className="flex-1 bg-transparent text-sm text-white placeholder-white/30 outline-none"
|
|
73
|
+
/>
|
|
74
|
+
<kbd className="rounded border border-white/10 bg-white/5 px-1.5 py-0.5 text-[11px] text-white/30">
|
|
75
|
+
ESC
|
|
76
|
+
</kbd>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* Results */}
|
|
80
|
+
<div className="max-h-80 overflow-y-auto p-2">
|
|
81
|
+
{results.length === 0 && query.trim() && (
|
|
82
|
+
<div className="flex flex-col items-center py-10 text-center text-white/30">
|
|
83
|
+
<svg className="mb-3 h-10 w-10 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
84
|
+
<circle cx="11" cy="11" r="8" strokeWidth="1.5" />
|
|
85
|
+
<path d="M21 21l-4.35-4.35" strokeWidth="1.5" />
|
|
86
|
+
</svg>
|
|
87
|
+
<p className="text-sm">No results for “{query}”</p>
|
|
88
|
+
<p className="mt-1 text-xs opacity-60">Try different keywords</p>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{results.length === 0 && !query.trim() && (
|
|
93
|
+
<div className="py-8 text-center text-sm text-white/30">
|
|
94
|
+
Start typing to search…
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{results.map((result, idx) => (
|
|
99
|
+
<button
|
|
100
|
+
key={result.pageId}
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => onSelect(result)}
|
|
103
|
+
className={`flex w-full flex-col gap-1 rounded-xl px-4 py-3 text-left transition-colors ${
|
|
104
|
+
idx === activeIndex
|
|
105
|
+
? 'bg-indigo-500/15 text-white'
|
|
106
|
+
: 'text-white/70 hover:bg-white/5'
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-center gap-2">
|
|
110
|
+
<span className="rounded bg-indigo-500/20 px-1.5 py-0.5 text-[10px] font-medium text-indigo-300">
|
|
111
|
+
{result.sectionTitle}
|
|
112
|
+
</span>
|
|
113
|
+
<span className="text-sm font-medium">{result.pageTitle}</span>
|
|
114
|
+
</div>
|
|
115
|
+
<p className="truncate text-xs text-white/40">
|
|
116
|
+
{highlightMatch(result.highlight, query)}
|
|
117
|
+
</p>
|
|
118
|
+
{result.tags.length > 0 && (
|
|
119
|
+
<div className="flex gap-1">
|
|
120
|
+
{result.tags.slice(0, 3).map((tag) => (
|
|
121
|
+
<span key={tag} className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] text-white/30">
|
|
122
|
+
{tag}
|
|
123
|
+
</span>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Footer hint */}
|
|
132
|
+
<div className="flex items-center gap-4 border-t border-white/5 px-5 py-2.5 text-[11px] text-white/20">
|
|
133
|
+
<span>↑↓ navigate</span>
|
|
134
|
+
<span>↵ select</span>
|
|
135
|
+
<span>esc close</span>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
|
+
import type { DocSection, DocPage } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
interface DocSidebarProps {
|
|
5
|
+
sections: (DocSection & { pages?: DocPage[]; pageCount?: number })[]
|
|
6
|
+
currentPageId?: string
|
|
7
|
+
onNavigate: (page: DocPage, section: DocSection) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DocSidebar({ sections, currentPageId, onNavigate }: DocSidebarProps) {
|
|
11
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
|
|
12
|
+
|
|
13
|
+
const toggle = useCallback((id: string) => {
|
|
14
|
+
setExpandedIds((prev) => {
|
|
15
|
+
const next = new Set(prev)
|
|
16
|
+
if (next.has(id)) next.delete(id)
|
|
17
|
+
else next.add(id)
|
|
18
|
+
return next
|
|
19
|
+
})
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
const topLevel = sections.filter((s) => !s.parentId)
|
|
23
|
+
const childrenOf = (parentId: string) =>
|
|
24
|
+
sections.filter((s) => s.parentId === parentId)
|
|
25
|
+
|
|
26
|
+
const renderSection = (section: DocSection & { pages?: DocPage[]; pageCount?: number }, depth: number) => {
|
|
27
|
+
const children = childrenOf(section.id)
|
|
28
|
+
const isExpanded = expandedIds.has(section.id)
|
|
29
|
+
const pages = section.pages ?? []
|
|
30
|
+
const hasContent = children.length > 0 || pages.length > 0
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div key={section.id} className="mb-1" style={{ paddingLeft: depth * 12 }}>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => toggle(section.id)}
|
|
37
|
+
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-white/5"
|
|
38
|
+
>
|
|
39
|
+
{section.icon && <span className="text-base">{section.icon}</span>}
|
|
40
|
+
<span className="flex-1 truncate">{section.title}</span>
|
|
41
|
+
{section.pageCount != null && (
|
|
42
|
+
<span className="text-xs tabular-nums opacity-50">{section.pageCount}</span>
|
|
43
|
+
)}
|
|
44
|
+
{hasContent && (
|
|
45
|
+
<svg
|
|
46
|
+
className={`h-3.5 w-3.5 shrink-0 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
|
47
|
+
viewBox="0 0 16 16"
|
|
48
|
+
fill="currentColor"
|
|
49
|
+
>
|
|
50
|
+
<path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="2" fill="none" />
|
|
51
|
+
</svg>
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
|
|
55
|
+
{isExpanded && (
|
|
56
|
+
<div className="ml-2 border-l border-white/10 pl-2">
|
|
57
|
+
{pages.map((page) => (
|
|
58
|
+
<button
|
|
59
|
+
key={page.id}
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => onNavigate(page, section)}
|
|
62
|
+
className={`flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
|
|
63
|
+
currentPageId === page.id
|
|
64
|
+
? 'bg-indigo-500/20 text-indigo-300 font-medium'
|
|
65
|
+
: 'text-white/60 hover:text-white/90 hover:bg-white/5'
|
|
66
|
+
}`}
|
|
67
|
+
>
|
|
68
|
+
<span className="truncate">{page.title}</span>
|
|
69
|
+
</button>
|
|
70
|
+
))}
|
|
71
|
+
{children.map((child) => renderSection(child, depth + 1))}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<nav className="flex flex-col gap-0.5 py-4">
|
|
80
|
+
<div className="px-4 pb-3 text-xs font-semibold uppercase tracking-widest text-white/40">
|
|
81
|
+
Documentation
|
|
82
|
+
</div>
|
|
83
|
+
{topLevel.map((s) => renderSection(s, 0))}
|
|
84
|
+
</nav>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { DocPage, DocSection, TocItem, BreadcrumbItem } from '@geenius-docs/shared'
|
|
3
|
+
import { DocSidebar } from './DocSidebar'
|
|
4
|
+
import { TableOfContents } from './TableOfContents'
|
|
5
|
+
import { Breadcrumbs } from './Breadcrumbs'
|
|
6
|
+
|
|
7
|
+
interface DocsLayoutProps {
|
|
8
|
+
sections: (DocSection & { pages?: DocPage[] })[]
|
|
9
|
+
currentPage?: DocPage
|
|
10
|
+
toc?: TocItem[]
|
|
11
|
+
activeHeadingId?: string
|
|
12
|
+
breadcrumbs?: BreadcrumbItem[]
|
|
13
|
+
currentPageId?: string
|
|
14
|
+
onNavigate: (page: DocPage, section: DocSection) => void
|
|
15
|
+
children: React.ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DocsLayout({
|
|
19
|
+
sections,
|
|
20
|
+
currentPage,
|
|
21
|
+
toc,
|
|
22
|
+
activeHeadingId,
|
|
23
|
+
breadcrumbs,
|
|
24
|
+
currentPageId,
|
|
25
|
+
onNavigate,
|
|
26
|
+
children,
|
|
27
|
+
}: DocsLayoutProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex min-h-screen bg-[#090a0f] text-white">
|
|
30
|
+
{/* Sidebar */}
|
|
31
|
+
<aside className="sticky top-0 hidden h-screen w-[260px] shrink-0 overflow-y-auto border-r border-white/5 bg-[#0b0c12] lg:block">
|
|
32
|
+
<DocSidebar
|
|
33
|
+
sections={sections}
|
|
34
|
+
currentPageId={currentPageId}
|
|
35
|
+
onNavigate={onNavigate}
|
|
36
|
+
/>
|
|
37
|
+
</aside>
|
|
38
|
+
|
|
39
|
+
{/* Main content */}
|
|
40
|
+
<main className="flex-1 overflow-hidden">
|
|
41
|
+
<div className="mx-auto flex max-w-6xl gap-8 px-6 py-8 lg:px-10">
|
|
42
|
+
{/* Content area */}
|
|
43
|
+
<div className="min-w-0 flex-1">
|
|
44
|
+
{breadcrumbs && breadcrumbs.length > 0 && (
|
|
45
|
+
<div className="mb-6">
|
|
46
|
+
<Breadcrumbs items={breadcrumbs} />
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
{children}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Table of contents */}
|
|
53
|
+
{toc && toc.length > 0 && (
|
|
54
|
+
<aside className="hidden w-[220px] shrink-0 xl:block">
|
|
55
|
+
<TableOfContents toc={toc} activeId={activeHeadingId} />
|
|
56
|
+
</aside>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
</main>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|