@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,21 @@
1
+ import React from 'react'
2
+ import type { DocPage as DocPageType } from '@geenius-docs/shared'
3
+ import { slugify } from '@geenius-docs/shared'
4
+
5
+ export function DocPage({ page }: { page: DocPageType }) {
6
+ const lines = page.content.split('\n'); const parts: string[] = []; let i = 0, inCode = false, codeLang = '', codeLines: string[] = []
7
+ const esc = (t: string) => t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
8
+
9
+ while (i < lines.length) {
10
+ const line = lines[i]
11
+ if (line.startsWith('```')) { if (!inCode) { inCode=true; codeLang=line.slice(3).trim(); codeLines=[] } else { parts.push(`<div style="margin:1rem 0"><div style="display:flex;justify-content:space-between;background:oklch(1 0 0/0.05);padding:0.5rem 1rem;border-radius:var(--docs-radius) var(--docs-radius) 0 0;font-size:0.75rem;color:oklch(1 0 0/0.5)">${codeLang||'code'}</div><pre style="overflow-x:auto;background:var(--docs-code-bg);padding:1rem;border-radius:0 0 var(--docs-radius) var(--docs-radius);font-size:0.875rem;line-height:1.65"><code>${esc(codeLines.join('\n'))}</code></pre></div>`); inCode=false }; i++; continue }
12
+ if (inCode) { codeLines.push(line); i++; continue }
13
+ const hm = line.match(/^(#{1,4})\s+(.+)$/); if (hm) { const lvl=hm[1].length,text=hm[2],id=slugify(text); parts.push(`<h${lvl} id="${id}" style="scroll-margin-top:5rem"><a href="#${id}">${esc(text)}</a></h${lvl}>`); i++; continue }
14
+ if (line.startsWith('>')) { const ql:string[]=[]; while(i<lines.length&&lines[i].startsWith('>')){ql.push(lines[i].replace(/^>\s?/,'')); i++}; parts.push(`<blockquote>${esc(ql.join(' '))}</blockquote>`); continue }
15
+ if (!line.trim()) { i++; continue }
16
+ const fmt=esc(line).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>').replace(/`(.+?)`/g,'<code>$1</code>').replace(/\[(.+?)\]\((.+?)\)/g,'<a href="$2">$1</a>')
17
+ parts.push(`<p>${fmt}</p>`); i++
18
+ }
19
+
20
+ return <article className="docs__content-main" dangerouslySetInnerHTML={{ __html: parts.join('') }} />
21
+ }
@@ -0,0 +1,55 @@
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[]; query: string; onQuery: (q: string) => void
7
+ onSelect: (result: SearchResult) => void; isOpen: boolean; onClose: () => void
8
+ }
9
+
10
+ export function DocSearch({ results, query, onQuery, onSelect, isOpen, onClose }: DocSearchProps) {
11
+ const [activeIndex, setActiveIndex] = useState(0)
12
+ const inputRef = useRef<HTMLInputElement>(null)
13
+ useEffect(() => { if (isOpen) { inputRef.current?.focus(); setActiveIndex(0) } }, [isOpen])
14
+ useEffect(() => { setActiveIndex(0) }, [results])
15
+
16
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
17
+ if (e.key === 'ArrowDown') { e.preventDefault(); setActiveIndex(i => Math.min(i + 1, results.length - 1)) }
18
+ else if (e.key === 'ArrowUp') { e.preventDefault(); setActiveIndex(i => Math.max(i - 1, 0)) }
19
+ else if (e.key === 'Enter' && results[activeIndex]) { e.preventDefault(); onSelect(results[activeIndex]) }
20
+ else if (e.key === 'Escape') { e.preventDefault(); onClose() }
21
+ }, [results, activeIndex, onSelect, onClose])
22
+
23
+ if (!isOpen) return null
24
+
25
+ return (
26
+ <div className="docs__search-modal" onClick={onClose}>
27
+ <div className="docs__search-backdrop" />
28
+ <div className="docs__search-panel" onClick={e => e.stopPropagation()}>
29
+ <div className="docs__search-input-wrapper">
30
+ <svg style={{ width: 20, height: 20, flexShrink: 0, color: 'oklch(1 0 0/0.3)' }} viewBox="0 0 20 20" fill="currentColor">
31
+ <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" />
32
+ </svg>
33
+ <input ref={inputRef} type="text" value={query} onChange={e => onQuery(e.target.value)} onKeyDown={handleKeyDown}
34
+ placeholder="Search documentation…" className="docs__search-input" />
35
+ <kbd style={{ padding: '2px 6px', border: '1px solid oklch(1 0 0/0.1)', borderRadius: 4, background: 'oklch(1 0 0/0.03)', fontSize: 11, color: 'oklch(1 0 0/0.3)' }}>ESC</kbd>
36
+ </div>
37
+ <div className="docs__search-results">
38
+ {results.length === 0 && query.trim() && <div className="docs__search-empty">No results for "{query}"</div>}
39
+ {results.length === 0 && !query.trim() && <div className="docs__search-empty">Start typing to search…</div>}
40
+ {results.map((result, idx) => (
41
+ <button key={result.pageId} type="button" onClick={() => onSelect(result)}
42
+ className={`docs__search-result ${idx === activeIndex ? 'docs__search-result--active' : ''}`}>
43
+ <div className="docs__search-result-header">
44
+ <span className="docs__search-result-badge">{result.sectionTitle}</span>
45
+ <span className="docs__search-result-title">{result.pageTitle}</span>
46
+ </div>
47
+ <p className="docs__search-result-snippet">{highlightMatch(result.highlight, query)}</p>
48
+ </button>
49
+ ))}
50
+ </div>
51
+ <div className="docs__search-footer"><span>↑↓ navigate</span><span>↵ select</span><span>esc close</span></div>
52
+ </div>
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,56 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import type { DocPage, DocSection } from '@geenius-docs/shared'
3
+ import './styles.css'
4
+
5
+ interface DocSidebarProps {
6
+ sections: (DocSection & { pages?: DocPage[]; pageCount?: number })[]
7
+ currentPageId?: string
8
+ onNavigate: (page: DocPage, section: DocSection) => void
9
+ }
10
+
11
+ export function DocSidebar({ sections, currentPageId, onNavigate }: DocSidebarProps) {
12
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set())
13
+ const toggle = useCallback((id: string) => {
14
+ setExpandedIds(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n })
15
+ }, [])
16
+
17
+ const topLevel = sections.filter(s => !s.parentId)
18
+ const childrenOf = (parentId: string) => sections.filter(s => s.parentId === parentId)
19
+
20
+ const renderSection = (section: DocSection & { pages?: DocPage[]; pageCount?: number }, depth: number) => {
21
+ const children = childrenOf(section.id)
22
+ const isExpanded = expandedIds.has(section.id)
23
+ const pages = section.pages ?? []
24
+
25
+ return (
26
+ <div key={section.id} className={`docs__sidebar-section ${isExpanded ? 'docs__sidebar-section--active' : ''}`} style={{ paddingLeft: depth * 12 }}>
27
+ <button type="button" className="docs__sidebar-section-btn" onClick={() => toggle(section.id)}>
28
+ {section.icon && <span className="docs__sidebar-section-icon">{section.icon}</span>}
29
+ <span className="docs__sidebar-section-title">{section.title}</span>
30
+ {section.pageCount != null && <span className="docs__sidebar-section-count">{section.pageCount}</span>}
31
+ <svg className={`docs__sidebar-section-chevron ${isExpanded ? 'docs__sidebar-section-chevron--open' : ''}`} viewBox="0 0 16 16">
32
+ <path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="2" fill="none" />
33
+ </svg>
34
+ </button>
35
+ {isExpanded && (
36
+ <div className="docs__sidebar-pages">
37
+ {pages.map(page => (
38
+ <button key={page.id} type="button" onClick={() => onNavigate(page, section)}
39
+ className={`docs__sidebar-page ${currentPageId === page.id ? 'docs__sidebar-page--active' : ''}`}>
40
+ {page.title}
41
+ </button>
42
+ ))}
43
+ {children.map(child => renderSection(child, depth + 1))}
44
+ </div>
45
+ )}
46
+ </div>
47
+ )
48
+ }
49
+
50
+ return (
51
+ <nav className="docs__sidebar">
52
+ <div className="docs__sidebar-header">Documentation</div>
53
+ {topLevel.map(s => renderSection(s, 0))}
54
+ </nav>
55
+ )
56
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react'
2
+ import type { DocPage, DocSection, TocItem, BreadcrumbItem } from '@geenius-docs/shared'
3
+ import { DocSidebar } from './DocSidebar'
4
+ import { TableOfContents, Breadcrumbs } from './common'
5
+
6
+ interface DocsLayoutProps {
7
+ sections: (DocSection & { pages?: DocPage[] })[]
8
+ toc?: TocItem[]; activeHeadingId?: string; breadcrumbs?: BreadcrumbItem[]
9
+ currentPageId?: string; onNavigate: (page: DocPage, section: DocSection) => void
10
+ children: React.ReactNode
11
+ }
12
+
13
+ export function DocsLayout({ sections, toc, activeHeadingId, breadcrumbs, currentPageId, onNavigate, children }: DocsLayoutProps) {
14
+ return (
15
+ <div className="docs__layout">
16
+ <DocSidebar sections={sections} currentPageId={currentPageId} onNavigate={onNavigate} />
17
+ <div className="docs__content">
18
+ <div className="docs__content-inner">
19
+ <div className="docs__content-main">
20
+ {breadcrumbs && breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
21
+ {children}
22
+ </div>
23
+ {toc && toc.length > 0 && <TableOfContents toc={toc} activeId={activeHeadingId} />}
24
+ </div>
25
+ </div>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,93 @@
1
+ import React from 'react'
2
+ import type { TocItem, BreadcrumbItem } from '@geenius-docs/shared'
3
+
4
+ export function TableOfContents({ toc, activeId }: { toc: TocItem[]; activeId?: string }) {
5
+ if (toc.length === 0) return null
6
+ return (
7
+ <nav className="docs__toc">
8
+ <h4 className="docs__toc-heading">On this page</h4>
9
+ <div className="docs__toc-list">
10
+ {toc.map(item => (
11
+ <React.Fragment key={item.id}>
12
+ <a href={`#${item.id}`} className={`docs__toc-item ${activeId === item.id ? 'docs__toc-item--active' : ''}`}>{item.text}</a>
13
+ {item.children.map(child => (
14
+ <a key={child.id} href={`#${child.id}`} className={`docs__toc-item docs__toc-item--h3 ${activeId === child.id ? 'docs__toc-item--active' : ''}`}>{child.text}</a>
15
+ ))}
16
+ </React.Fragment>
17
+ ))}
18
+ </div>
19
+ </nav>
20
+ )
21
+ }
22
+
23
+ export function Breadcrumbs({ items }: { items: BreadcrumbItem[] }) {
24
+ if (items.length === 0) return null
25
+ return (
26
+ <nav className="docs__breadcrumbs" aria-label="Breadcrumb">
27
+ {items.map((item, idx) => (
28
+ <React.Fragment key={idx}>
29
+ {idx > 0 && (
30
+ <svg className="docs__breadcrumb-separator" viewBox="0 0 16 16" fill="none"><path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" /></svg>
31
+ )}
32
+ {idx === items.length - 1
33
+ ? <span className="docs__breadcrumb-item docs__breadcrumb-item--current">{item.title}</span>
34
+ : <a href={item.href} className="docs__breadcrumb-item">{item.title}</a>
35
+ }
36
+ </React.Fragment>
37
+ ))}
38
+ </nav>
39
+ )
40
+ }
41
+
42
+ export function PageNavigation({ prev, next }: { prev?: { title: string; href: string }; next?: { title: string; href: string } }) {
43
+ if (!prev && !next) return null
44
+ return (
45
+ <div className="docs__page-nav">
46
+ {prev ? (
47
+ <a href={prev.href} className="docs__page-nav-prev">
48
+ <span className="docs__page-nav-label">← Previous</span>
49
+ <span className="docs__page-nav-title">{prev.title}</span>
50
+ </a>
51
+ ) : <div style={{ flex: 1 }} />}
52
+ {next ? (
53
+ <a href={next.href} className="docs__page-nav-next">
54
+ <span className="docs__page-nav-label">Next →</span>
55
+ <span className="docs__page-nav-title">{next.title}</span>
56
+ </a>
57
+ ) : <div style={{ flex: 1 }} />}
58
+ </div>
59
+ )
60
+ }
61
+
62
+ export function EditButton({ pageSlug, editUrl }: { pageSlug: string; editUrl?: string }) {
63
+ if (!editUrl) return null
64
+ return (
65
+ <a href={`${editUrl.replace(/\/$/, '')}/${pageSlug}.mdx`} target="_blank" rel="noopener noreferrer" className="docs__edit-btn">
66
+ <svg className="docs__edit-btn-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"><path d="M11.5 1.5l3 3-9 9H2.5v-3l9-9z" strokeWidth="1.5" strokeLinejoin="round" /></svg>
67
+ Edit this page
68
+ </a>
69
+ )
70
+ }
71
+
72
+ export function VersionSelector({ versions, current, onSelect }: { versions: string[]; current: string; onSelect: (v: string) => void }) {
73
+ const [isOpen, setIsOpen] = React.useState(false)
74
+ if (versions.length <= 1) return null
75
+ return (
76
+ <div className="docs__version-selector">
77
+ <button type="button" className="docs__version-btn" onClick={() => setIsOpen(!isOpen)}>
78
+ v{current}
79
+ <svg style={{ width: 12, height: 12, transform: isOpen ? 'rotate(180deg)' : '', transition: 'transform 0.2s' }} viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="2" fill="none" /></svg>
80
+ </button>
81
+ {isOpen && (
82
+ <div className="docs__version-dropdown">
83
+ {versions.map(v => (
84
+ <button key={v} type="button" className={`docs__version-option ${v === current ? 'docs__version-option--active' : ''}`}
85
+ onClick={() => { onSelect(v); setIsOpen(false) }}>
86
+ v{v}{v === current && <span style={{ marginLeft: 'auto', fontSize: 10 }}>✓</span>}
87
+ </button>
88
+ ))}
89
+ </div>
90
+ )}
91
+ </div>
92
+ )
93
+ }
@@ -0,0 +1,5 @@
1
+ export { DocSidebar } from './DocSidebar'
2
+ export { DocPage } from './DocPage'
3
+ export { DocSearch } from './DocSearch'
4
+ export { DocsLayout } from './DocsLayout'
5
+ export { TableOfContents, Breadcrumbs, PageNavigation, EditButton, VersionSelector } from './common'
@@ -0,0 +1,2 @@
1
+ export { useDocs, useDocSearch, useDocsAdmin, useTableOfContents } from '@geenius-docs/react'
2
+ export type { DocsState, DocSearchState, DocsAdminActions, TableOfContentsState } from '@geenius-docs/react'
@@ -0,0 +1,6 @@
1
+ export { DocsProvider, useDocs } from './DocsProvider'
2
+ export type { DocsProviderProps } from './DocsProvider'
3
+
4
+ export { DocsLayout } from './DocsLayout'
5
+ export { RouterDocsLayout, type DocItem } from './RouterDocsLayout'
6
+ export { RouterDocsContent } from './RouterDocsContent'
@@ -0,0 +1,3 @@
1
+ // @geenius-docs/react-css — src/index.tsx
2
+ // This file is intentionally empty.
3
+ // The canonical package entry point is src/index.ts (tsup entry).
@@ -0,0 +1,78 @@
1
+ import React, { useEffect, useMemo } from 'react'
2
+ import type { DocPage as DocPageType, DocSection } from '@geenius-docs/shared'
3
+ import { buildBreadcrumbs } from '@geenius-docs/shared'
4
+ import { useDocs, useTableOfContents } from '../hooks'
5
+ import { DocsLayout } from '../components/DocsLayout'
6
+ import { DocPage } from '../components/DocPage'
7
+ import { EditButton, PageNavigation } from '../components/common'
8
+ import '../styles.css'
9
+
10
+ interface DocViewPageProps {
11
+ tree: (DocSection & { pages: DocPageType[]; pageCount: number })[] | undefined
12
+ page: DocPageType | null | undefined
13
+ editPageUrl?: string
14
+ onNavigate: (page: DocPageType, section: DocSection) => void
15
+ onIncrementView?: (pageId: string) => void
16
+ }
17
+
18
+ export function DocViewPage({ tree, page, editPageUrl, onNavigate, onIncrementView }: DocViewPageProps) {
19
+ const docs = useDocs(tree)
20
+ const { toc, activeId } = useTableOfContents(page?.content)
21
+
22
+ useEffect(() => { if (page?.id && onIncrementView) onIncrementView(page.id) }, [page?.id, onIncrementView])
23
+
24
+ const breadcrumbs = useMemo(() => page ? buildBreadcrumbs(docs.sections, page.sectionId, page.slug) : [], [docs.sections, page])
25
+
26
+ const { prev, next } = useMemo(() => {
27
+ if (!page) return { prev: undefined, next: undefined }
28
+ const all = docs.flatPages; const idx = all.findIndex(p => p.id === page.id); const sec = docs.sections.find(s => s.id === page.sectionId)
29
+ return {
30
+ prev: idx > 0 ? { title: all[idx - 1].title, href: `/docs/${sec?.slug ?? ''}/${all[idx - 1].slug}` } : undefined,
31
+ next: idx < all.length - 1 ? { title: all[idx + 1].title, href: `/docs/${sec?.slug ?? ''}/${all[idx + 1].slug}` } : undefined,
32
+ }
33
+ }, [page, docs.flatPages, docs.sections])
34
+
35
+ if (docs.isLoading || page === undefined) return (
36
+ <div className="docs__layout">
37
+ <div className="docs__sidebar" style={{ padding: '1rem' }}>
38
+ {Array.from({ length: 8 }).map((_, i) => <div key={i} className="docs__skeleton" style={{ height: 32, marginBottom: 8 }} />)}
39
+ </div>
40
+ <div className="docs__content" style={{ padding: '2rem' }}>
41
+ <div className="docs__skeleton" style={{ width: 384, height: 40, marginBottom: 16 }} />
42
+ {Array.from({ length: 10 }).map((_, i) => <div key={i} className="docs__skeleton" style={{ height: 16, marginBottom: 12, width: `${60 + Math.random() * 40}%` }} />)}
43
+ </div>
44
+ </div>
45
+ )
46
+
47
+ if (!page) return (
48
+ <div className="docs__empty" style={{ minHeight: '100vh', background: 'var(--docs-bg)' }}>
49
+ <div className="docs__empty-icon">🔍</div>
50
+ <h2 className="docs__empty-title">Page not found</h2>
51
+ </div>
52
+ )
53
+
54
+ return (
55
+ <DocsLayout sections={docs.sections} toc={toc} activeHeadingId={activeId} breadcrumbs={breadcrumbs} currentPageId={page.id} onNavigate={onNavigate}>
56
+ <div className="docs__page-header">
57
+ <h1 className="docs__page-title">{page.title}</h1>
58
+ <div className="docs__reading-meta">
59
+ <span className="docs__reading-meta-author">
60
+ {page.author.avatar
61
+ ? <img src={page.author.avatar} alt="" className="docs__reading-meta-avatar" />
62
+ : <span className="docs__reading-meta-avatar-placeholder">{page.author.name[0]}</span>}
63
+ {page.author.name}
64
+ </span>
65
+ {page.readingTime > 0 && <span>{page.readingTime} min read</span>}
66
+ {page.viewCount > 0 && <span>{page.viewCount.toLocaleString()} views</span>}
67
+ </div>
68
+ {page.tags.length > 0 && <div className="docs__tags">{page.tags.map(t => <span key={t} className="docs__tag">{t}</span>)}</div>}
69
+ </div>
70
+ <DocPage page={page} />
71
+ <div style={{ marginTop: '2.5rem', paddingTop: '1.5rem', borderTop: '1px solid var(--docs-border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
72
+ <EditButton pageSlug={page.slug} editUrl={editPageUrl} />
73
+ {page.version && <span style={{ fontSize: '0.6875rem', color: 'oklch(1 0 0/0.25)' }}>v{page.version}</span>}
74
+ </div>
75
+ <PageNavigation prev={prev} next={next} />
76
+ </DocsLayout>
77
+ )
78
+ }
@@ -0,0 +1,101 @@
1
+ import React, { useState, useMemo } from 'react'
2
+ import type { DocPage, DocSection } from '@geenius-docs/shared'
3
+ import type { DocsAdminActions } from '@geenius-docs/react'
4
+ import '../styles.css'
5
+
6
+ interface DocsAdminPageProps {
7
+ tree: (DocSection & { pages: DocPage[]; pageCount: number })[] | undefined
8
+ allPages?: DocPage[]
9
+ admin: DocsAdminActions
10
+ }
11
+
12
+ export function DocsAdminPage({ tree, allPages, admin }: DocsAdminPageProps) {
13
+ const sections = useMemo(() => tree ?? [], [tree])
14
+ const [selectedId, setSelectedId] = useState<string | null>(null)
15
+ const [showSF, setShowSF] = useState(false)
16
+ const [showPF, setShowPF] = useState(false)
17
+ const [sf, setSf] = useState({ title: '', slug: '', description: '', icon: '', access: 'team' as const })
18
+ const [pf, setPf] = useState({ title: '', slug: '', content: '', access: 'team' as const, tags: '' })
19
+ const selected = sections.find(s => s.id === selectedId)
20
+ const pages = useMemo(() => { if (!selected) return []; if (allPages) return allPages.filter(p => p.sectionId === selectedId); return selected.pages ?? [] }, [selected, allPages, selectedId])
21
+
22
+ if (tree === undefined) return (
23
+ <div style={{ minHeight: '100vh', background: 'var(--docs-bg)', padding: '3rem 1.5rem' }}>
24
+ <div style={{ maxWidth: '72rem', margin: '0 auto' }}>
25
+ <div className="docs__skeleton" style={{ width: 192, height: 40, marginBottom: 32 }} />
26
+ <div className="docs__admin-grid"><div className="docs__skeleton" style={{ height: 384 }} /><div className="docs__skeleton" style={{ height: 384 }} /></div>
27
+ </div>
28
+ </div>
29
+ )
30
+
31
+ return (
32
+ <div style={{ minHeight: '100vh', background: 'var(--docs-bg)', color: 'var(--docs-text)' }}>
33
+ <div style={{ maxWidth: '72rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
34
+ <h1 style={{ fontSize: '1.5rem', fontWeight: 700, marginBottom: '2rem' }}>Docs Admin</h1>
35
+ <div className="docs__admin-grid">
36
+ <div className="docs__admin-panel">
37
+ <div className="docs__admin-panel-header">
38
+ <h2 className="docs__admin-panel-title">Sections</h2>
39
+ <button type="button" className="docs__admin-add-btn" onClick={() => setShowSF(!showSF)}>+ Add</button>
40
+ </div>
41
+ {showSF && (
42
+ <div className="docs__admin-form">
43
+ <input className="docs__admin-input" placeholder="Title" value={sf.title} onChange={e => setSf({...sf, title: e.target.value})} />
44
+ <input className="docs__admin-input" placeholder="Slug" value={sf.slug} onChange={e => setSf({...sf, slug: e.target.value})} />
45
+ <input className="docs__admin-input" placeholder="Icon" value={sf.icon} onChange={e => setSf({...sf, icon: e.target.value})} />
46
+ <input className="docs__admin-input" placeholder="Description" value={sf.description} onChange={e => setSf({...sf, description: e.target.value})} />
47
+ <div className="docs__admin-form-actions">
48
+ <button type="button" className="docs__admin-add-btn" onClick={async () => { await admin.createSection({...sf, order: sections.length, icon: sf.icon||undefined, description: sf.description||undefined}); setSf({title:'',slug:'',description:'',icon:'',access:'team'}); setShowSF(false) }}>Create</button>
49
+ <button type="button" style={{padding:'0.375rem 0.75rem',border:'1px solid var(--docs-border)',borderRadius:'var(--docs-radius)',background:'transparent',color:'var(--docs-text)',fontSize:'0.6875rem',cursor:'pointer'}} onClick={() => setShowSF(false)}>Cancel</button>
50
+ </div>
51
+ </div>
52
+ )}
53
+ {sections.length === 0 && <p style={{padding:'2rem',textAlign:'center',fontSize:'0.875rem',color:'var(--docs-text-muted)'}}>No sections</p>}
54
+ {sections.map(s => (
55
+ <div key={s.id} className={`docs__admin-section-item ${selectedId === s.id ? 'docs__admin-section-item--selected' : ''}`} onClick={() => setSelectedId(s.id)}>
56
+ {s.icon && <span>{s.icon}</span>}
57
+ <span style={{flex:1,fontSize:'0.875rem',fontWeight:500,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{s.title}</span>
58
+ <span style={{fontSize:'0.6875rem',opacity:0.4}}>{s.pageCount ?? 0}</span>
59
+ <button type="button" style={{background:'transparent',border:'none',color:'oklch(1 0 0/0.2)',cursor:'pointer',padding:4}} onClick={e => { e.stopPropagation(); if (confirm(`Delete "${s.title}"?`)) admin.deleteSection(s.id) }}>✕</button>
60
+ </div>
61
+ ))}
62
+ </div>
63
+ <div className="docs__admin-panel">
64
+ <div className="docs__admin-panel-header">
65
+ <h2 className="docs__admin-panel-title">{selected ? `Pages — ${selected.title}` : 'Pages'}</h2>
66
+ {selected && <button type="button" className="docs__admin-add-btn" onClick={() => setShowPF(!showPF)}>+ Add</button>}
67
+ </div>
68
+ {!selected && <p style={{padding:'4rem',textAlign:'center',fontSize:'0.875rem',color:'var(--docs-text-muted)'}}>Select a section</p>}
69
+ {showPF && selected && (
70
+ <div className="docs__admin-form">
71
+ <input className="docs__admin-input" placeholder="Title" value={pf.title} onChange={e => setPf({...pf, title: e.target.value})} />
72
+ <input className="docs__admin-input" placeholder="Slug" value={pf.slug} onChange={e => setPf({...pf, slug: e.target.value})} />
73
+ <textarea className="docs__admin-input docs__admin-textarea" placeholder="Content (MDX)" value={pf.content} onChange={e => setPf({...pf, content: e.target.value})} />
74
+ <input className="docs__admin-input" placeholder="Tags (comma)" value={pf.tags} onChange={e => setPf({...pf, tags: e.target.value})} />
75
+ <div className="docs__admin-form-actions">
76
+ <button type="button" className="docs__admin-add-btn" onClick={async () => { await admin.createPage({title:pf.title,slug:pf.slug,content:pf.content,sectionId:selectedId!,access:pf.access,tags:pf.tags?pf.tags.split(',').map(t=>t.trim()):[],order:pages.length}); setPf({title:'',slug:'',content:'',access:'team',tags:''}); setShowPF(false) }}>Create</button>
77
+ <button type="button" style={{padding:'0.375rem 0.75rem',border:'1px solid var(--docs-border)',borderRadius:'var(--docs-radius)',background:'transparent',color:'var(--docs-text)',fontSize:'0.6875rem',cursor:'pointer'}} onClick={() => setShowPF(false)}>Cancel</button>
78
+ </div>
79
+ </div>
80
+ )}
81
+ {selected && pages.map(page => (
82
+ <div key={page.id} className="docs__admin-page-item">
83
+ <div className="docs__admin-page-info">
84
+ <p className="docs__admin-page-title">{page.title}</p>
85
+ <p className="docs__admin-page-slug">/{page.slug}</p>
86
+ </div>
87
+ <span className={`docs__status-badge docs__status-badge--${page.status}`}>{page.status}</span>
88
+ <div className="docs__admin-actions">
89
+ {page.status === 'draft' && <button type="button" className="docs__admin-action-btn docs__admin-action-btn--publish" onClick={() => admin.publishPage(page.id)}>Publish</button>}
90
+ {page.status === 'published' && <button type="button" className="docs__admin-action-btn docs__admin-action-btn--archive" onClick={() => admin.archivePage(page.id)}>Archive</button>}
91
+ <button type="button" className="docs__admin-action-btn docs__admin-action-btn--delete" onClick={() => { if (confirm(`Delete "${page.title}"?`)) admin.deletePage(page.id) }}>✕</button>
92
+ </div>
93
+ </div>
94
+ ))}
95
+ {selected && pages.length === 0 && !showPF && <p style={{padding:'3rem',textAlign:'center',fontSize:'0.875rem',color:'var(--docs-text-muted)'}}>No pages</p>}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ )
101
+ }
@@ -0,0 +1,68 @@
1
+ import React, { useState, useEffect, useCallback } from 'react'
2
+ import type { DocPage, DocSection, SearchResult } from '@geenius-docs/shared'
3
+ import { buildDocsIndex, searchDocs } from '@geenius-docs/shared'
4
+ import { useDocs, useDocSearch } from '../hooks'
5
+ import { DocSearch } from '../components/DocSearch'
6
+ import '../styles.css'
7
+
8
+ interface DocsIndexPageProps {
9
+ tree: (DocSection & { pages: DocPage[]; pageCount: number })[] | undefined
10
+ onSelectPage?: (page: DocPage, section: DocSection) => void
11
+ }
12
+
13
+ export function DocsIndexPage({ tree, onSelectPage }: DocsIndexPageProps) {
14
+ const docs = useDocs(tree)
15
+ const [isSearchOpen, setIsSearchOpen] = useState(false)
16
+ const searchFn = useCallback((q: string): SearchResult[] => { const idx = buildDocsIndex(docs.flatPages, docs.sections); return searchDocs(q, idx) }, [docs.flatPages, docs.sections])
17
+ const search = useDocSearch(searchFn)
18
+
19
+ useEffect(() => { const h = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setIsSearchOpen(true) } }; document.addEventListener('keydown', h); return () => document.removeEventListener('keydown', h) }, [])
20
+
21
+ if (docs.isLoading) return (
22
+ <div style={{ minHeight: '100vh', background: 'var(--docs-bg)', padding: '4rem 1.5rem' }}>
23
+ <div style={{ maxWidth: '64rem', margin: '0 auto' }}>
24
+ <div className="docs__skeleton" style={{ width: 256, height: 40, marginBottom: 40 }} />
25
+ <div className="docs__skeleton" style={{ maxWidth: 560, height: 48, marginBottom: 32 }} />
26
+ <div className="docs__section-grid">{Array.from({length: 6}).map((_,i) => <div key={i} className="docs__skeleton" style={{ height: 144 }} />)}</div>
27
+ </div>
28
+ </div>
29
+ )
30
+
31
+ if (docs.sections.length === 0) return (
32
+ <div className="docs__empty" style={{ minHeight: '100vh', background: 'var(--docs-bg)' }}>
33
+ <div className="docs__empty-icon">📚</div>
34
+ <h2 className="docs__empty-title">No documentation yet</h2>
35
+ <p className="docs__empty-desc">Create your first section and pages to get started.</p>
36
+ </div>
37
+ )
38
+
39
+ return (
40
+ <div style={{ minHeight: '100vh', background: 'var(--docs-bg)', color: 'var(--docs-text)' }}>
41
+ <div style={{ maxWidth: '64rem', margin: '0 auto', padding: '4rem 1.5rem' }}>
42
+ <h1 style={{ fontSize: '2.25rem', fontWeight: 700, letterSpacing: '-0.02em', marginBottom: '0.5rem' }}>Documentation</h1>
43
+ <p style={{ fontSize: '1.125rem', color: 'var(--docs-text-muted)', marginBottom: '2.5rem' }}>Browse guides, API references, and tutorials.</p>
44
+
45
+ <button type="button" className="docs__search-trigger" onClick={() => setIsSearchOpen(true)}>
46
+ <svg className="docs__search-trigger-icon" viewBox="0 0 20 20" fill="currentColor"><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" /></svg>
47
+ <span className="docs__search-trigger-text">Search documentation…</span>
48
+ <kbd className="docs__search-trigger-kbd">⌘K</kbd>
49
+ </button>
50
+
51
+ <div className="docs__section-grid">
52
+ {docs.sections.filter(s => !s.parentId).map(section => (
53
+ <button key={section.id} type="button" className="docs__section-card"
54
+ onClick={() => { const fp = section.pages?.[0]; if (fp && onSelectPage) onSelectPage(fp, section) }}>
55
+ {section.icon && <div className="docs__section-card-icon">{section.icon}</div>}
56
+ <h3 className="docs__section-card-title">{section.title}</h3>
57
+ {section.description && <p className="docs__section-card-desc">{section.description}</p>}
58
+ <div className="docs__section-card-meta">{section.pageCount ?? 0} pages</div>
59
+ </button>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ <DocSearch results={search.results} query={search.query} onQuery={search.setQuery}
64
+ onSelect={r => { setIsSearchOpen(false); search.clearSearch(); const sec = docs.sections.find(s => s.slug === r.sectionSlug); const pg = docs.flatPages.find(p => p.id === r.pageId); if (pg && sec && onSelectPage) onSelectPage(pg, sec) }}
65
+ isOpen={isSearchOpen} onClose={() => setIsSearchOpen(false)} />
66
+ </div>
67
+ )
68
+ }
@@ -0,0 +1,3 @@
1
+ export { DocsIndexPage } from './DocsIndexPage'
2
+ export { DocViewPage } from './DocViewPage'
3
+ export { DocsAdminPage } from './DocsAdminPage'