@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 { createSignal, type JSX } from 'solid-js'
|
|
4
|
+
import { Link, useLocation } from '@tanstack/solid-router'
|
|
5
|
+
import { Book, Menu, X, Search, ChevronRight } from 'lucide-solid'
|
|
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: JSX.Element
|
|
17
|
+
docs: DocItem[]
|
|
18
|
+
currentSlug?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DocsLayout: Component<DocsLayoutProps> = ({
|
|
22
|
+
children,
|
|
23
|
+
docs,
|
|
24
|
+
currentSlug,
|
|
25
|
+
}) => {
|
|
26
|
+
const [isSidebarOpen, setIsSidebarOpen] = createSignal(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 class="min-h-screen bg-bg">
|
|
55
|
+
{/* Mobile Header */}
|
|
56
|
+
<div class="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="/" class="flex items-center gap-2 text-primary">
|
|
58
|
+
<Book class="w-5 h-5" />
|
|
59
|
+
<span class="font-bold">Documentation</span>
|
|
60
|
+
</Link>
|
|
61
|
+
<button
|
|
62
|
+
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
|
63
|
+
class="p-2 rounded-lg hover:bg-bg-muted transition-colors"
|
|
64
|
+
>
|
|
65
|
+
{isSidebarOpen ? (
|
|
66
|
+
<X class="w-5 h-5" />
|
|
67
|
+
) : (
|
|
68
|
+
<Menu class="w-5 h-5" />
|
|
69
|
+
)}
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="flex">
|
|
74
|
+
{/* Sidebar */}
|
|
75
|
+
<aside
|
|
76
|
+
class={`
|
|
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 class="p-6">
|
|
84
|
+
{/* Logo */}
|
|
85
|
+
<Link
|
|
86
|
+
to="/"
|
|
87
|
+
class="hidden lg:flex items-center gap-2 text-primary mb-8"
|
|
88
|
+
>
|
|
89
|
+
<Book class="w-6 h-6" />
|
|
90
|
+
<span class="font-bold text-lg">Documentation</span>
|
|
91
|
+
</Link>
|
|
92
|
+
|
|
93
|
+
{/* Search */}
|
|
94
|
+
<div class="relative mb-6">
|
|
95
|
+
<Search class="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
|
+
class="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 class="space-y-6">
|
|
105
|
+
{sortedCategories.map(([category, categoryDocs]) => (
|
|
106
|
+
<div>
|
|
107
|
+
<h3 class="text-xs font-bold text-text-muted uppercase tracking-wider mb-3">
|
|
108
|
+
{category}
|
|
109
|
+
</h3>
|
|
110
|
+
<ul class="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>
|
|
117
|
+
<Link
|
|
118
|
+
to="/docs/$slug"
|
|
119
|
+
params={{ slug: doc.slug }}
|
|
120
|
+
activeOptions={{ exact: true }}
|
|
121
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
122
|
+
class={`
|
|
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
|
+
class={`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
|
+
class="fixed inset-0 bg-black/50 z-20 lg:hidden"
|
|
150
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Main Content */}
|
|
155
|
+
<main class="flex-1 min-w-0">
|
|
156
|
+
<div class="max-w-4xl mx-auto px-6 py-12">{children}</div>
|
|
157
|
+
</main>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js'
|
|
2
|
+
import type { BreadcrumbItem } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
export function Breadcrumbs(props: { items: BreadcrumbItem[] }) {
|
|
5
|
+
return (
|
|
6
|
+
<Show when={props.items.length > 0}>
|
|
7
|
+
<nav class="flex items-center gap-1.5 text-sm" aria-label="Breadcrumb">
|
|
8
|
+
<For each={props.items}>
|
|
9
|
+
{(item, idx) => (
|
|
10
|
+
<>
|
|
11
|
+
<Show when={idx() > 0}>
|
|
12
|
+
<svg class="h-3.5 w-3.5 shrink-0 text-white/20" viewBox="0 0 16 16" fill="none">
|
|
13
|
+
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="1.5" />
|
|
14
|
+
</svg>
|
|
15
|
+
</Show>
|
|
16
|
+
<Show when={idx() === props.items.length - 1} fallback={
|
|
17
|
+
<a href={item.href} class="truncate text-white/40 transition-colors hover:text-white/70">{item.title}</a>
|
|
18
|
+
}>
|
|
19
|
+
<span class="truncate text-white/60">{item.title}</span>
|
|
20
|
+
</Show>
|
|
21
|
+
</>
|
|
22
|
+
)}
|
|
23
|
+
</For>
|
|
24
|
+
</nav>
|
|
25
|
+
</Show>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { DocPage as DocPageType } from '@geenius-docs/shared'
|
|
2
|
+
import { slugify } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
interface DocPageProps {
|
|
5
|
+
page: DocPageType
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function DocPage(props: DocPageProps) {
|
|
9
|
+
const renderContent = () => {
|
|
10
|
+
const lines = props.page.content.split('\n')
|
|
11
|
+
const elements: string[] = []
|
|
12
|
+
let i = 0
|
|
13
|
+
let inCode = false
|
|
14
|
+
let codeLang = ''
|
|
15
|
+
let codeLines: string[] = []
|
|
16
|
+
|
|
17
|
+
while (i < lines.length) {
|
|
18
|
+
const line = lines[i]
|
|
19
|
+
|
|
20
|
+
if (line.startsWith('```')) {
|
|
21
|
+
if (!inCode) {
|
|
22
|
+
inCode = true
|
|
23
|
+
codeLang = line.slice(3).trim()
|
|
24
|
+
codeLines = []
|
|
25
|
+
} else {
|
|
26
|
+
elements.push(
|
|
27
|
+
`<div class="group relative my-4"><div class="flex items-center justify-between rounded-t-lg bg-white/5 px-4 py-2 text-xs text-white/50"><span>${codeLang || 'code'}</span></div><pre class="overflow-x-auto rounded-b-lg bg-black/30 p-4 text-sm leading-relaxed"><code>${escapeHtml(codeLines.join('\n'))}</code></pre></div>`
|
|
28
|
+
)
|
|
29
|
+
inCode = false
|
|
30
|
+
codeLang = ''
|
|
31
|
+
}
|
|
32
|
+
i++
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (inCode) { codeLines.push(line); i++; continue }
|
|
37
|
+
|
|
38
|
+
const calloutMatch = line.match(/^>\s*\[!(NOTE|WARNING|TIP|IMPORTANT|CAUTION)\]/)
|
|
39
|
+
if (calloutMatch) {
|
|
40
|
+
const type = calloutMatch[1]
|
|
41
|
+
const contentLines: string[] = []
|
|
42
|
+
i++
|
|
43
|
+
while (i < lines.length && lines[i].startsWith('>')) {
|
|
44
|
+
contentLines.push(lines[i].replace(/^>\s?/, ''))
|
|
45
|
+
i++
|
|
46
|
+
}
|
|
47
|
+
const styles: Record<string, { bg: string; border: string; icon: string; label: string }> = {
|
|
48
|
+
NOTE: { bg: 'bg-blue-500/10', border: 'border-blue-500/30', icon: 'ℹ️', label: 'Note' },
|
|
49
|
+
WARNING: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', icon: '⚠️', label: 'Warning' },
|
|
50
|
+
TIP: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', icon: '💡', label: 'Tip' },
|
|
51
|
+
IMPORTANT: { bg: 'bg-purple-500/10', border: 'border-purple-500/30', icon: '❗', label: 'Important' },
|
|
52
|
+
CAUTION: { bg: 'bg-red-500/10', border: 'border-red-500/30', icon: '🔴', label: 'Caution' },
|
|
53
|
+
}
|
|
54
|
+
const s = styles[type] ?? styles.NOTE
|
|
55
|
+
elements.push(
|
|
56
|
+
`<div class="${s.bg} ${s.border} my-4 rounded-lg border-l-4 p-4"><p class="mb-1 text-sm font-semibold">${s.icon} ${s.label}</p><p class="text-sm leading-relaxed text-white/80">${escapeHtml(contentLines.join(' '))}</p></div>`
|
|
57
|
+
)
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const headingMatch = line.match(/^(#{1,4})\s+(.+)$/)
|
|
62
|
+
if (headingMatch) {
|
|
63
|
+
const level = headingMatch[1].length
|
|
64
|
+
const text = headingMatch[2]
|
|
65
|
+
const id = slugify(text)
|
|
66
|
+
const classes: Record<number, string> = {
|
|
67
|
+
1: 'text-3xl font-bold mt-8 mb-4',
|
|
68
|
+
2: 'text-2xl font-bold mt-8 mb-3 pb-2 border-b border-white/10',
|
|
69
|
+
3: 'text-xl font-semibold mt-6 mb-2',
|
|
70
|
+
4: 'text-lg font-medium mt-4 mb-2',
|
|
71
|
+
}
|
|
72
|
+
elements.push(`<h${level} id="${id}" class="${classes[level]} scroll-mt-20"><a href="#${id}" class="hover:text-indigo-400 transition-colors">${escapeHtml(text)}</a></h${level}>`)
|
|
73
|
+
i++
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (line.startsWith('>')) {
|
|
78
|
+
const quoteLines: string[] = []
|
|
79
|
+
while (i < lines.length && lines[i].startsWith('>')) {
|
|
80
|
+
quoteLines.push(lines[i].replace(/^>\s?/, ''))
|
|
81
|
+
i++
|
|
82
|
+
}
|
|
83
|
+
elements.push(`<blockquote class="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">${escapeHtml(quoteLines.join(' '))}</blockquote>`)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!line.trim()) { i++; continue }
|
|
88
|
+
|
|
89
|
+
const formatted = escapeHtml(line)
|
|
90
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
91
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
92
|
+
.replace(/`(.+?)`/g, '<code class="rounded bg-white/10 px-1.5 py-0.5 text-sm text-indigo-300">$1</code>')
|
|
93
|
+
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" class="text-indigo-400 hover:text-indigo-300 underline underline-offset-2">$1</a>')
|
|
94
|
+
|
|
95
|
+
elements.push(`<p class="my-3 leading-relaxed text-white/80">${formatted}</p>`)
|
|
96
|
+
i++
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return elements.join('')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return <article class="prose-custom max-w-none" innerHTML={renderContent()} />
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function escapeHtml(text: string): string {
|
|
106
|
+
return text
|
|
107
|
+
.replace(/&/g, '&')
|
|
108
|
+
.replace(/</g, '<')
|
|
109
|
+
.replace(/>/g, '>')
|
|
110
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createSignal, createEffect, For, Show, onCleanup } from 'solid-js'
|
|
2
|
+
import type { SearchResult } from '@geenius-docs/shared'
|
|
3
|
+
import { highlightMatch } from '@geenius-docs/shared'
|
|
4
|
+
|
|
5
|
+
interface DocSearchProps {
|
|
6
|
+
results: SearchResult[]; query: string; onQuery: (q: string) => void
|
|
7
|
+
onSelect: (result: SearchResult) => void; isOpen: boolean; onClose: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DocSearch(props: DocSearchProps) {
|
|
11
|
+
const [activeIndex, setActiveIndex] = createSignal(0)
|
|
12
|
+
let inputRef: HTMLInputElement | undefined
|
|
13
|
+
|
|
14
|
+
createEffect(() => {
|
|
15
|
+
if (props.isOpen) { inputRef?.focus(); setActiveIndex(0) }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
createEffect(() => { props.results; setActiveIndex(0) })
|
|
19
|
+
|
|
20
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
21
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex((i) => Math.min(i + 1, props.results.length - 1)) }
|
|
22
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex((i) => Math.max(i - 1, 0)) }
|
|
23
|
+
else if (e.key === 'Enter' && props.results[activeIndex()]) { e.preventDefault(); props.onSelect(props.results[activeIndex()]) }
|
|
24
|
+
else if (e.key === 'Escape') { e.preventDefault(); props.onClose() }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Show when={props.isOpen}>
|
|
29
|
+
<div class="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]" onClick={() => props.onClose()}>
|
|
30
|
+
<div class="fixed inset-0 bg-black/60 backdrop-blur-sm" />
|
|
31
|
+
<div class="relative z-10 w-full max-w-xl overflow-hidden rounded-2xl border border-white/10 bg-[#0d0e14] shadow-2xl" onClick={(e) => e.stopPropagation()}>
|
|
32
|
+
<div class="flex items-center gap-3 border-b border-white/10 px-5 py-4">
|
|
33
|
+
<svg class="h-5 w-5 shrink-0 text-white/30" viewBox="0 0 20 20" fill="currentColor">
|
|
34
|
+
<path fill-rule="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" clip-rule="evenodd" />
|
|
35
|
+
</svg>
|
|
36
|
+
<input
|
|
37
|
+
ref={inputRef}
|
|
38
|
+
type="text"
|
|
39
|
+
value={props.query}
|
|
40
|
+
onInput={(e) => props.onQuery(e.currentTarget.value)}
|
|
41
|
+
onKeyDown={handleKeyDown}
|
|
42
|
+
placeholder="Search documentation…"
|
|
43
|
+
class="flex-1 bg-transparent text-sm text-white placeholder-white/30 outline-none"
|
|
44
|
+
/>
|
|
45
|
+
<kbd class="rounded border border-white/10 bg-white/5 px-1.5 py-0.5 text-[11px] text-white/30">ESC</kbd>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="max-h-80 overflow-y-auto p-2">
|
|
48
|
+
<Show when={props.results.length === 0 && props.query.trim()}>
|
|
49
|
+
<div class="flex flex-col items-center py-10 text-center text-white/30">
|
|
50
|
+
<p class="text-sm">No results for "{props.query}"</p>
|
|
51
|
+
</div>
|
|
52
|
+
</Show>
|
|
53
|
+
<Show when={props.results.length === 0 && !props.query.trim()}>
|
|
54
|
+
<div class="py-8 text-center text-sm text-white/30">Start typing to search…</div>
|
|
55
|
+
</Show>
|
|
56
|
+
<For each={props.results}>
|
|
57
|
+
{(result, idx) => (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
onClick={() => props.onSelect(result)}
|
|
61
|
+
class={`flex w-full flex-col gap-1 rounded-xl px-4 py-3 text-left transition-colors ${
|
|
62
|
+
idx() === activeIndex() ? 'bg-indigo-500/15 text-white' : 'text-white/70 hover:bg-white/5'
|
|
63
|
+
}`}
|
|
64
|
+
>
|
|
65
|
+
<div class="flex items-center gap-2">
|
|
66
|
+
<span class="rounded bg-indigo-500/20 px-1.5 py-0.5 text-[10px] font-medium text-indigo-300">{result.sectionTitle}</span>
|
|
67
|
+
<span class="text-sm font-medium">{result.pageTitle}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<p class="truncate text-xs text-white/40">{highlightMatch(result.highlight, props.query)}</p>
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</For>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="flex items-center gap-4 border-t border-white/5 px-5 py-2.5 text-[11px] text-white/20">
|
|
75
|
+
<span>↑↓ navigate</span><span>↵ select</span><span>esc close</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</Show>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { createSignal, For, Show } from 'solid-js'
|
|
2
|
+
import type { DocPage, DocSection } 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(props: DocSidebarProps) {
|
|
11
|
+
const [expandedIds, setExpandedIds] = createSignal<Set<string>>(new Set())
|
|
12
|
+
|
|
13
|
+
const toggle = (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 = () => props.sections.filter((s) => !s.parentId)
|
|
23
|
+
const childrenOf = (parentId: string) => props.sections.filter((s) => s.parentId === parentId)
|
|
24
|
+
|
|
25
|
+
const renderSection = (section: DocSection & { pages?: DocPage[]; pageCount?: number }, depth: number) => {
|
|
26
|
+
const children = () => childrenOf(section.id)
|
|
27
|
+
const isExpanded = () => expandedIds().has(section.id)
|
|
28
|
+
const pages = () => section.pages ?? []
|
|
29
|
+
const hasContent = () => children().length > 0 || pages().length > 0
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div style={{ "padding-left": `${depth * 12}px` }} class="mb-1">
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={() => toggle(section.id)}
|
|
36
|
+
class="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"
|
|
37
|
+
>
|
|
38
|
+
<Show when={section.icon}>
|
|
39
|
+
<span class="text-base">{section.icon}</span>
|
|
40
|
+
</Show>
|
|
41
|
+
<span class="flex-1 truncate">{section.title}</span>
|
|
42
|
+
<Show when={section.pageCount != null}>
|
|
43
|
+
<span class="text-xs tabular-nums opacity-50">{section.pageCount}</span>
|
|
44
|
+
</Show>
|
|
45
|
+
<Show when={hasContent()}>
|
|
46
|
+
<svg
|
|
47
|
+
class={`h-3.5 w-3.5 shrink-0 transition-transform ${isExpanded() ? 'rotate-90' : ''}`}
|
|
48
|
+
viewBox="0 0 16 16"
|
|
49
|
+
fill="currentColor"
|
|
50
|
+
>
|
|
51
|
+
<path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="2" fill="none" />
|
|
52
|
+
</svg>
|
|
53
|
+
</Show>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
<Show when={isExpanded()}>
|
|
57
|
+
<div class="ml-2 border-l border-white/10 pl-2">
|
|
58
|
+
<For each={pages()}>
|
|
59
|
+
{(page) => (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => props.onNavigate(page, section)}
|
|
63
|
+
class={`flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
|
|
64
|
+
props.currentPageId === page.id
|
|
65
|
+
? 'bg-indigo-500/20 text-indigo-300 font-medium'
|
|
66
|
+
: 'text-white/60 hover:text-white/90 hover:bg-white/5'
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
<span class="truncate">{page.title}</span>
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</For>
|
|
73
|
+
<For each={children()}>
|
|
74
|
+
{(child) => renderSection(child, depth + 1)}
|
|
75
|
+
</For>
|
|
76
|
+
</div>
|
|
77
|
+
</Show>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<nav class="flex flex-col gap-0.5 py-4">
|
|
84
|
+
<div class="px-4 pb-3 text-xs font-semibold uppercase tracking-widest text-white/40">
|
|
85
|
+
Documentation
|
|
86
|
+
</div>
|
|
87
|
+
<For each={topLevel()}>
|
|
88
|
+
{(s) => renderSection(s, 0)}
|
|
89
|
+
</For>
|
|
90
|
+
</nav>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Show } from 'solid-js'
|
|
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
|
+
import type { JSX } from 'solid-js'
|
|
7
|
+
|
|
8
|
+
interface DocsLayoutProps {
|
|
9
|
+
sections: (DocSection & { pages?: DocPage[] })[]
|
|
10
|
+
toc?: TocItem[]; activeHeadingId?: string; breadcrumbs?: BreadcrumbItem[]
|
|
11
|
+
currentPageId?: string; onNavigate: (page: DocPage, section: DocSection) => void
|
|
12
|
+
children: JSX.Element
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function DocsLayout(props: DocsLayoutProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div class="flex min-h-screen bg-[#090a0f] text-white">
|
|
18
|
+
<aside class="sticky top-0 hidden h-screen w-[260px] shrink-0 overflow-y-auto border-r border-white/5 bg-[#0b0c12] lg:block">
|
|
19
|
+
<DocSidebar sections={props.sections} currentPageId={props.currentPageId} onNavigate={props.onNavigate} />
|
|
20
|
+
</aside>
|
|
21
|
+
<main class="flex-1 overflow-hidden">
|
|
22
|
+
<div class="mx-auto flex max-w-6xl gap-8 px-6 py-8 lg:px-10">
|
|
23
|
+
<div class="min-w-0 flex-1">
|
|
24
|
+
<Show when={props.breadcrumbs && props.breadcrumbs.length > 0}>
|
|
25
|
+
<div class="mb-6"><Breadcrumbs items={props.breadcrumbs!} /></div>
|
|
26
|
+
</Show>
|
|
27
|
+
{props.children}
|
|
28
|
+
</div>
|
|
29
|
+
<Show when={props.toc && props.toc.length > 0}>
|
|
30
|
+
<aside class="hidden w-[220px] shrink-0 xl:block">
|
|
31
|
+
<TableOfContents toc={props.toc!} activeId={props.activeHeadingId} />
|
|
32
|
+
</aside>
|
|
33
|
+
</Show>
|
|
34
|
+
</div>
|
|
35
|
+
</main>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Show } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export function EditButton(props: { pageSlug: string; editUrl?: string }) {
|
|
4
|
+
return (
|
|
5
|
+
<Show when={props.editUrl}>
|
|
6
|
+
<a href={`${props.editUrl!.replace(/\/$/, '')}/${props.pageSlug}.mdx`} target="_blank" rel="noopener noreferrer"
|
|
7
|
+
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 px-3 py-1.5 text-xs text-white/40 transition-colors hover:border-white/20 hover:text-white/60">
|
|
8
|
+
<svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor">
|
|
9
|
+
<path d="M11.5 1.5l3 3-9 9H2.5v-3l9-9z" stroke-width="1.5" stroke-linejoin="round" />
|
|
10
|
+
</svg>
|
|
11
|
+
Edit this page
|
|
12
|
+
</a>
|
|
13
|
+
</Show>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Show } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
interface PageNavigationProps {
|
|
4
|
+
prev?: { title: string; href: string }
|
|
5
|
+
next?: { title: string; href: string }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function PageNavigation(props: PageNavigationProps) {
|
|
9
|
+
return (
|
|
10
|
+
<Show when={props.prev || props.next}>
|
|
11
|
+
<div class="mt-12 flex items-stretch gap-4 border-t border-white/10 pt-8">
|
|
12
|
+
<Show when={props.prev} fallback={<div class="flex-1" />}>
|
|
13
|
+
{(prev) => (
|
|
14
|
+
<a href={prev().href} class="group flex flex-1 flex-col rounded-xl border border-white/10 p-4 transition-all hover:border-indigo-500/40 hover:bg-white/5">
|
|
15
|
+
<span class="text-xs text-white/40 group-hover:text-indigo-400 transition-colors">← Previous</span>
|
|
16
|
+
<span class="mt-1 text-sm font-medium text-white/80 group-hover:text-white transition-colors truncate">{prev().title}</span>
|
|
17
|
+
</a>
|
|
18
|
+
)}
|
|
19
|
+
</Show>
|
|
20
|
+
<Show when={props.next} fallback={<div class="flex-1" />}>
|
|
21
|
+
{(next) => (
|
|
22
|
+
<a href={next().href} class="group flex flex-1 flex-col items-end rounded-xl border border-white/10 p-4 text-right transition-all hover:border-indigo-500/40 hover:bg-white/5">
|
|
23
|
+
<span class="text-xs text-white/40 group-hover:text-indigo-400 transition-colors">Next →</span>
|
|
24
|
+
<span class="mt-1 text-sm font-medium text-white/80 group-hover:text-white transition-colors truncate">{next().title}</span>
|
|
25
|
+
</a>
|
|
26
|
+
)}
|
|
27
|
+
</Show>
|
|
28
|
+
</div>
|
|
29
|
+
</Show>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js'
|
|
2
|
+
import type { TocItem } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
interface TableOfContentsProps { toc: TocItem[]; activeId?: string }
|
|
5
|
+
|
|
6
|
+
function TocLink(props: { item: TocItem; activeId?: string; depth?: number }) {
|
|
7
|
+
const depth = () => props.depth ?? 0
|
|
8
|
+
return (
|
|
9
|
+
<>
|
|
10
|
+
<a
|
|
11
|
+
href={`#${props.item.id}`}
|
|
12
|
+
class={`block truncate border-l-2 py-1 text-[13px] transition-all duration-200 ${
|
|
13
|
+
props.activeId === props.item.id
|
|
14
|
+
? 'border-indigo-500 text-indigo-300 font-medium'
|
|
15
|
+
: 'border-transparent text-white/40 hover:text-white/70 hover:border-white/20'
|
|
16
|
+
}`}
|
|
17
|
+
style={{ "padding-left": `${12 + depth() * 12}px` }}
|
|
18
|
+
>
|
|
19
|
+
{props.item.text}
|
|
20
|
+
</a>
|
|
21
|
+
<For each={props.item.children}>
|
|
22
|
+
{(child) => <TocLink item={child} activeId={props.activeId} depth={depth() + 1} />}
|
|
23
|
+
</For>
|
|
24
|
+
</>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function TableOfContents(props: TableOfContentsProps) {
|
|
29
|
+
return (
|
|
30
|
+
<Show when={props.toc.length > 0}>
|
|
31
|
+
<nav class="sticky top-24">
|
|
32
|
+
<h4 class="mb-3 text-xs font-semibold uppercase tracking-widest text-white/40">On this page</h4>
|
|
33
|
+
<div class="flex flex-col">
|
|
34
|
+
<For each={props.toc}>
|
|
35
|
+
{(item) => <TocLink item={item} activeId={props.activeId} />}
|
|
36
|
+
</For>
|
|
37
|
+
</div>
|
|
38
|
+
</nav>
|
|
39
|
+
</Show>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createSignal, Show, For } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
interface VersionSelectorProps { versions: string[]; current: string; onSelect: (v: string) => void }
|
|
4
|
+
|
|
5
|
+
export function VersionSelector(props: VersionSelectorProps) {
|
|
6
|
+
const [isOpen, setIsOpen] = createSignal(false)
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Show when={props.versions.length > 1}>
|
|
10
|
+
<div class="relative">
|
|
11
|
+
<button type="button" onClick={() => setIsOpen(!isOpen())} class="flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-xs font-medium text-white/60 transition-colors hover:border-indigo-500/30">
|
|
12
|
+
<span>v{props.current}</span>
|
|
13
|
+
<svg class={`h-3 w-3 transition-transform ${isOpen() ? 'rotate-180' : ''}`} viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" /></svg>
|
|
14
|
+
</button>
|
|
15
|
+
<Show when={isOpen()}>
|
|
16
|
+
<div class="absolute right-0 top-full z-20 mt-1 min-w-[120px] overflow-hidden rounded-lg border border-white/10 bg-[#111218] shadow-xl">
|
|
17
|
+
<For each={props.versions}>
|
|
18
|
+
{(v) => (
|
|
19
|
+
<button type="button" onClick={() => { props.onSelect(v); setIsOpen(false) }}
|
|
20
|
+
class={`flex w-full items-center px-3 py-2 text-xs transition-colors ${v === props.current ? 'bg-indigo-500/15 text-indigo-300' : 'text-white/60 hover:bg-white/5'}`}>
|
|
21
|
+
v{v}{v === props.current && <span class="ml-auto text-[10px]">✓</span>}
|
|
22
|
+
</button>
|
|
23
|
+
)}
|
|
24
|
+
</For>
|
|
25
|
+
</div>
|
|
26
|
+
</Show>
|
|
27
|
+
</div>
|
|
28
|
+
</Show>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { DocSidebar } from './DocSidebar'
|
|
2
|
+
export { DocPage } from './DocPage'
|
|
3
|
+
export { TableOfContents } from './TableOfContents'
|
|
4
|
+
export { Breadcrumbs } from './Breadcrumbs'
|
|
5
|
+
export { PageNavigation } from './PageNavigation'
|
|
6
|
+
export { DocSearch } from './DocSearch'
|
|
7
|
+
export { VersionSelector } from './VersionSelector'
|
|
8
|
+
export { EditButton } from './EditButton'
|
|
9
|
+
export { DocsLayout } from './DocsLayout'
|