@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.
Files changed (139) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  6. package/.github/dependabot.yml +11 -0
  7. package/.github/workflows/ci.yml +23 -0
  8. package/.github/workflows/release.yml +29 -0
  9. package/.nvmrc +1 -0
  10. package/.project/ACCOUNT.yaml +4 -0
  11. package/.project/IDEAS.yaml +7 -0
  12. package/.project/PROJECT.yaml +11 -0
  13. package/.project/ROADMAP.yaml +15 -0
  14. package/CHANGELOG.md +11 -0
  15. package/CODE_OF_CONDUCT.md +16 -0
  16. package/CONTRIBUTING.md +26 -0
  17. package/LICENSE +21 -0
  18. package/README.md +1 -0
  19. package/SECURITY.md +15 -0
  20. package/SUPPORT.md +8 -0
  21. package/package.json +58 -0
  22. package/packages/convex/README.md +1 -0
  23. package/packages/convex/package.json +12 -0
  24. package/packages/convex/src/convex.config.ts +3 -0
  25. package/packages/convex/src/index.ts +3 -0
  26. package/packages/convex/src/mutations.ts +270 -0
  27. package/packages/convex/src/queries.ts +175 -0
  28. package/packages/convex/src/schema.ts +55 -0
  29. package/packages/react/README.md +1 -0
  30. package/packages/react/package.json +36 -0
  31. package/packages/react/src/DocsLayout.tsx +116 -0
  32. package/packages/react/src/DocsProvider.tsx +93 -0
  33. package/packages/react/src/RouterDocsContent.tsx +148 -0
  34. package/packages/react/src/RouterDocsLayout.tsx +161 -0
  35. package/packages/react/src/components/Breadcrumbs.tsx +34 -0
  36. package/packages/react/src/components/DocPage.tsx +191 -0
  37. package/packages/react/src/components/DocSearch.tsx +140 -0
  38. package/packages/react/src/components/DocSidebar.tsx +86 -0
  39. package/packages/react/src/components/DocsLayout.tsx +62 -0
  40. package/packages/react/src/components/EditButton.tsx +26 -0
  41. package/packages/react/src/components/PageNavigation.tsx +45 -0
  42. package/packages/react/src/components/TableOfContents.tsx +46 -0
  43. package/packages/react/src/components/VersionSelector.tsx +60 -0
  44. package/packages/react/src/components/index.ts +9 -0
  45. package/packages/react/src/hooks/index.ts +8 -0
  46. package/packages/react/src/hooks/useDocSearch.ts +55 -0
  47. package/packages/react/src/hooks/useDocs.ts +57 -0
  48. package/packages/react/src/hooks/useDocsAdmin.ts +151 -0
  49. package/packages/react/src/hooks/useTableOfContents.ts +66 -0
  50. package/packages/react/src/index.ts +38 -0
  51. package/packages/react/src/pages/DocSearchPage.tsx +129 -0
  52. package/packages/react/src/pages/DocViewPage.tsx +158 -0
  53. package/packages/react/src/pages/DocsAdminPage.tsx +330 -0
  54. package/packages/react/src/pages/DocsIndexPage.tsx +172 -0
  55. package/packages/react/src/pages/index.ts +4 -0
  56. package/packages/react/src/useDocs.ts +58 -0
  57. package/packages/react/tsup.config.ts +12 -0
  58. package/packages/react-css/README.md +1 -0
  59. package/packages/react-css/package.json +37 -0
  60. package/packages/react-css/src/DocsLayout.tsx +117 -0
  61. package/packages/react-css/src/DocsProvider.tsx +93 -0
  62. package/packages/react-css/src/RouterDocsContent.tsx +60 -0
  63. package/packages/react-css/src/RouterDocsLayout.tsx +101 -0
  64. package/packages/react-css/src/components/DocPage.tsx +21 -0
  65. package/packages/react-css/src/components/DocSearch.tsx +55 -0
  66. package/packages/react-css/src/components/DocSidebar.tsx +56 -0
  67. package/packages/react-css/src/components/DocsLayout.tsx +28 -0
  68. package/packages/react-css/src/components/common.tsx +93 -0
  69. package/packages/react-css/src/components/index.ts +5 -0
  70. package/packages/react-css/src/hooks/index.ts +2 -0
  71. package/packages/react-css/src/index.ts +6 -0
  72. package/packages/react-css/src/index.tsx +3 -0
  73. package/packages/react-css/src/pages/DocViewPage.tsx +78 -0
  74. package/packages/react-css/src/pages/DocsAdminPage.tsx +101 -0
  75. package/packages/react-css/src/pages/DocsIndexPage.tsx +68 -0
  76. package/packages/react-css/src/pages/index.ts +3 -0
  77. package/packages/react-css/src/styles.css +1271 -0
  78. package/packages/react-css/src/useDocs.ts +58 -0
  79. package/packages/react-css/tsconfig.json +19 -0
  80. package/packages/react-css/tsup.config.ts +10 -0
  81. package/packages/shared/README.md +1 -0
  82. package/packages/shared/package.json +31 -0
  83. package/packages/shared/src/__tests__/docs.test.ts +69 -0
  84. package/packages/shared/src/config.ts +80 -0
  85. package/packages/shared/src/index.ts +179 -0
  86. package/packages/shared/src/providers/astro.ts +94 -0
  87. package/packages/shared/src/providers/fumadocs.ts +116 -0
  88. package/packages/shared/src/providers/internal.ts +80 -0
  89. package/packages/shared/src/types.ts +73 -0
  90. package/packages/shared/tsconfig.json +18 -0
  91. package/packages/shared/tsup.config.ts +12 -0
  92. package/packages/shared/vitest.config.ts +4 -0
  93. package/packages/solidjs/README.md +1 -0
  94. package/packages/solidjs/package.json +33 -0
  95. package/packages/solidjs/src/DocsLayout.tsx +87 -0
  96. package/packages/solidjs/src/DocsProvider.tsx +95 -0
  97. package/packages/solidjs/src/RouterDocsContent.tsx +147 -0
  98. package/packages/solidjs/src/RouterDocsLayout.tsx +161 -0
  99. package/packages/solidjs/src/components/Breadcrumbs.tsx +27 -0
  100. package/packages/solidjs/src/components/DocPage.tsx +110 -0
  101. package/packages/solidjs/src/components/DocSearch.tsx +81 -0
  102. package/packages/solidjs/src/components/DocSidebar.tsx +92 -0
  103. package/packages/solidjs/src/components/DocsLayout.tsx +38 -0
  104. package/packages/solidjs/src/components/EditButton.tsx +15 -0
  105. package/packages/solidjs/src/components/PageNavigation.tsx +31 -0
  106. package/packages/solidjs/src/components/TableOfContents.tsx +41 -0
  107. package/packages/solidjs/src/components/VersionSelector.tsx +30 -0
  108. package/packages/solidjs/src/components/index.ts +9 -0
  109. package/packages/solidjs/src/createDocs.ts +62 -0
  110. package/packages/solidjs/src/index.ts +28 -0
  111. package/packages/solidjs/src/pages/DocSearchPage.tsx +72 -0
  112. package/packages/solidjs/src/pages/DocViewPage.tsx +80 -0
  113. package/packages/solidjs/src/pages/DocsAdminPage.tsx +123 -0
  114. package/packages/solidjs/src/pages/DocsIndexPage.tsx +85 -0
  115. package/packages/solidjs/src/pages/index.ts +4 -0
  116. package/packages/solidjs/src/primitives/createDocSearch.ts +42 -0
  117. package/packages/solidjs/src/primitives/createDocs.ts +35 -0
  118. package/packages/solidjs/src/primitives/createDocsAdmin.ts +63 -0
  119. package/packages/solidjs/src/primitives/createTableOfContents.ts +51 -0
  120. package/packages/solidjs/src/primitives/index.ts +4 -0
  121. package/packages/solidjs/tsup.config.ts +12 -0
  122. package/packages/solidjs-css/README.md +1 -0
  123. package/packages/solidjs-css/package.json +36 -0
  124. package/packages/solidjs-css/src/DocsLayout.tsx +106 -0
  125. package/packages/solidjs-css/src/DocsProvider.tsx +95 -0
  126. package/packages/solidjs-css/src/RouterDocsContent.tsx +54 -0
  127. package/packages/solidjs-css/src/RouterDocsLayout.tsx +104 -0
  128. package/packages/solidjs-css/src/createDocs.ts +62 -0
  129. package/packages/solidjs-css/src/index.ts +7 -0
  130. package/packages/solidjs-css/src/index.tsx +17 -0
  131. package/packages/solidjs-css/src/pages/DocViewPage.tsx +111 -0
  132. package/packages/solidjs-css/src/pages/DocsAdminPage.tsx +332 -0
  133. package/packages/solidjs-css/src/pages/DocsIndexPage.tsx +116 -0
  134. package/packages/solidjs-css/src/pages/index.ts +3 -0
  135. package/packages/solidjs-css/src/primitives/index.ts +1 -0
  136. package/packages/solidjs-css/src/styles.css +1271 -0
  137. package/packages/solidjs-css/tsconfig.json +20 -0
  138. package/packages/solidjs-css/tsup.config.ts +10 -0
  139. 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, '&amp;')
108
+ .replace(/</g, '&lt;')
109
+ .replace(/>/g, '&gt;')
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'