@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,62 @@
|
|
|
1
|
+
// @geenius-docs/solidjs — src/createDocs.ts
|
|
2
|
+
|
|
3
|
+
import { createSignal, createMemo, onMount } from 'solid-js'
|
|
4
|
+
import type { DocsPage, DocsSidebar, DocsSearchResult, DocsProvider } from '@geenius-docs/shared'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* createDocs — SolidJS primitive wrapping a DocsProvider for docs rendering.
|
|
8
|
+
* SolidJS equivalent of useDocs hook.
|
|
9
|
+
*/
|
|
10
|
+
export function createDocs(provider: DocsProvider) {
|
|
11
|
+
const [sidebar, setSidebar] = createSignal<DocsSidebar | null>(null)
|
|
12
|
+
const [currentPage, setCurrentPage] = createSignal<DocsPage | null>(null)
|
|
13
|
+
const [searchResults, setSearchResults] = createSignal<DocsSearchResult[]>([])
|
|
14
|
+
const [isLoading, setIsLoading] = createSignal(false)
|
|
15
|
+
const [searchQuery, setSearchQuery] = createSignal('')
|
|
16
|
+
|
|
17
|
+
// Load sidebar on mount
|
|
18
|
+
onMount(async () => {
|
|
19
|
+
const sb = await provider.getSidebar()
|
|
20
|
+
setSidebar(sb)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const loadPage = async (slug: string) => {
|
|
24
|
+
setIsLoading(true)
|
|
25
|
+
try {
|
|
26
|
+
const page = await provider.getPage(slug)
|
|
27
|
+
setCurrentPage(page)
|
|
28
|
+
} finally {
|
|
29
|
+
setIsLoading(false)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const search = async (query: string) => {
|
|
34
|
+
setSearchQuery(query)
|
|
35
|
+
if (!query.trim()) {
|
|
36
|
+
setSearchResults([])
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const results = await provider.search(query)
|
|
40
|
+
setSearchResults(results)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const navigation = createMemo(() => {
|
|
44
|
+
const page = currentPage()
|
|
45
|
+
if (!page) return { prev: null, next: null }
|
|
46
|
+
return {
|
|
47
|
+
prev: page.prev ?? null,
|
|
48
|
+
next: page.next ?? null,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
sidebar,
|
|
54
|
+
currentPage,
|
|
55
|
+
navigation,
|
|
56
|
+
isLoading,
|
|
57
|
+
searchResults,
|
|
58
|
+
searchQuery,
|
|
59
|
+
loadPage,
|
|
60
|
+
search,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Primitives
|
|
2
|
+
export { createDocs } from './primitives/createDocs'
|
|
3
|
+
export { createDocSearch } from './primitives/createDocSearch'
|
|
4
|
+
export { createDocsAdmin } from './primitives/createDocsAdmin'
|
|
5
|
+
export { createTableOfContents } from './primitives/createTableOfContents'
|
|
6
|
+
|
|
7
|
+
// Components
|
|
8
|
+
export { DocSidebar } from './components/DocSidebar'
|
|
9
|
+
export { DocPage } from './components/DocPage'
|
|
10
|
+
export { TableOfContents } from './components/TableOfContents'
|
|
11
|
+
export { Breadcrumbs } from './components/Breadcrumbs'
|
|
12
|
+
export { PageNavigation } from './components/PageNavigation'
|
|
13
|
+
export { DocSearch } from './components/DocSearch'
|
|
14
|
+
export { VersionSelector } from './components/VersionSelector'
|
|
15
|
+
export { EditButton } from './components/EditButton'
|
|
16
|
+
export { DocsLayout } from './components/DocsLayout'
|
|
17
|
+
|
|
18
|
+
// Pages
|
|
19
|
+
export { DocsIndexPage } from './pages/DocsIndexPage'
|
|
20
|
+
export { DocViewPage } from './pages/DocViewPage'
|
|
21
|
+
export { DocSearchPage } from './pages/DocSearchPage'
|
|
22
|
+
export { DocsAdminPage } from './pages/DocsAdminPage'
|
|
23
|
+
|
|
24
|
+
// Re-export shared types
|
|
25
|
+
export type {
|
|
26
|
+
DocPage as DocPageType, DocSection, SearchResult, TocItem,
|
|
27
|
+
BreadcrumbItem, DocsConfig, DocAccess, DocStatus,
|
|
28
|
+
} from '@geenius-docs/shared'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createMemo, Show, For } from 'solid-js'
|
|
2
|
+
import type { DocPage, DocSection, SearchResult } from '@geenius-docs/shared'
|
|
3
|
+
import { buildDocsIndex, searchDocs, highlightMatch } from '@geenius-docs/shared'
|
|
4
|
+
import { createDocSearch } from '../primitives/createDocSearch'
|
|
5
|
+
|
|
6
|
+
interface DocSearchPageProps {
|
|
7
|
+
tree: () => (DocSection & { pages: DocPage[]; pageCount: number })[] | undefined
|
|
8
|
+
onSelectPage?: (page: DocPage, section: DocSection) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DocSearchPage(props: DocSearchPageProps) {
|
|
12
|
+
const sections = createMemo(() => props.tree() ?? [])
|
|
13
|
+
const flatPages = createMemo(() => sections().flatMap(s => s.pages ?? []))
|
|
14
|
+
|
|
15
|
+
const searchFn = (q: string): SearchResult[] => {
|
|
16
|
+
const index = buildDocsIndex(flatPages(), sections())
|
|
17
|
+
return searchDocs(q, index)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { results, isSearching, query, setQuery } = createDocSearch(searchFn)
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Show when={props.tree() !== undefined} fallback={
|
|
24
|
+
<div class="min-h-screen bg-[#090a0f] px-6 py-16"><div class="mx-auto max-w-2xl"><div class="mb-8 h-12 animate-pulse rounded-xl bg-white/5" /></div></div>
|
|
25
|
+
}>
|
|
26
|
+
<div class="min-h-screen bg-[#090a0f] text-white">
|
|
27
|
+
<div class="mx-auto max-w-2xl px-6 py-16">
|
|
28
|
+
<h1 class="mb-8 text-2xl font-bold">Search Documentation</h1>
|
|
29
|
+
<div class="relative mb-8">
|
|
30
|
+
<input type="text" value={query()} onInput={e => setQuery(e.currentTarget.value)} placeholder="Type to search…" autofocus
|
|
31
|
+
class="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40" />
|
|
32
|
+
<Show when={isSearching()}>
|
|
33
|
+
<div class="absolute right-4 top-1/2 -translate-y-1/2"><div class="h-4 w-4 animate-spin rounded-full border-2 border-white/10 border-t-indigo-400" /></div>
|
|
34
|
+
</Show>
|
|
35
|
+
</div>
|
|
36
|
+
<Show when={results().length > 0}>
|
|
37
|
+
<div class="space-y-2">
|
|
38
|
+
<For each={results()}>
|
|
39
|
+
{(result) => {
|
|
40
|
+
const section = () => sections().find(s => s.slug === result.sectionSlug)
|
|
41
|
+
const page = () => flatPages().find(p => p.id === result.pageId)
|
|
42
|
+
return (
|
|
43
|
+
<button type="button" onClick={() => { const p = page(); const s = section(); if (p && s && props.onSelectPage) props.onSelectPage(p, s) }}
|
|
44
|
+
class="flex w-full flex-col gap-1.5 rounded-xl border border-white/5 bg-white/[0.02] p-4 text-left transition-all hover:border-indigo-500/20 hover:bg-white/[0.05]">
|
|
45
|
+
<div class="flex items-center gap-2">
|
|
46
|
+
<span class="rounded bg-indigo-500/20 px-2 py-0.5 text-[10px] font-medium text-indigo-300">{result.sectionTitle}</span>
|
|
47
|
+
<span class="text-sm font-medium text-white/90">{result.pageTitle}</span>
|
|
48
|
+
</div>
|
|
49
|
+
<p class="text-xs leading-relaxed text-white/40">{highlightMatch(result.highlight, query())}</p>
|
|
50
|
+
</button>
|
|
51
|
+
)
|
|
52
|
+
}}
|
|
53
|
+
</For>
|
|
54
|
+
</div>
|
|
55
|
+
</Show>
|
|
56
|
+
<Show when={query().trim() && results().length === 0 && !isSearching()}>
|
|
57
|
+
<div class="flex flex-col items-center py-16 text-center">
|
|
58
|
+
<div class="mb-4 text-5xl opacity-30">🔍</div>
|
|
59
|
+
<h3 class="text-lg font-medium text-white/60">No results found</h3>
|
|
60
|
+
</div>
|
|
61
|
+
</Show>
|
|
62
|
+
<Show when={!query().trim()}>
|
|
63
|
+
<div class="flex flex-col items-center py-16 text-center">
|
|
64
|
+
<div class="mb-4 text-5xl opacity-20">📖</div>
|
|
65
|
+
<p class="text-sm text-white/30">Start typing to search across all documentation</p>
|
|
66
|
+
</div>
|
|
67
|
+
</Show>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</Show>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createEffect, createMemo, Show } from 'solid-js'
|
|
2
|
+
import type { DocPage as DocPageType, DocSection } from '@geenius-docs/shared'
|
|
3
|
+
import { buildBreadcrumbs } from '@geenius-docs/shared'
|
|
4
|
+
import { createDocs } from '../primitives/createDocs'
|
|
5
|
+
import { createTableOfContents } from '../primitives/createTableOfContents'
|
|
6
|
+
import { DocsLayout } from '../components/DocsLayout'
|
|
7
|
+
import { DocPage } from '../components/DocPage'
|
|
8
|
+
import { EditButton } from '../components/EditButton'
|
|
9
|
+
import { PageNavigation } from '../components/PageNavigation'
|
|
10
|
+
|
|
11
|
+
interface DocViewPageProps {
|
|
12
|
+
tree: () => (DocSection & { pages: DocPageType[]; pageCount: number })[] | undefined
|
|
13
|
+
page: () => DocPageType | null | undefined
|
|
14
|
+
editPageUrl?: string
|
|
15
|
+
onNavigate: (page: DocPageType, section: DocSection) => void
|
|
16
|
+
onIncrementView?: (pageId: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function DocViewPage(props: DocViewPageProps) {
|
|
20
|
+
const docs = createDocs(props.tree)
|
|
21
|
+
const { toc, activeId } = createTableOfContents(() => props.page()?.content)
|
|
22
|
+
|
|
23
|
+
createEffect(() => {
|
|
24
|
+
const p = props.page()
|
|
25
|
+
if (p?.id && props.onIncrementView) props.onIncrementView(p.id)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const breadcrumbs = createMemo(() => {
|
|
29
|
+
const p = props.page()
|
|
30
|
+
if (!p) return []
|
|
31
|
+
return buildBreadcrumbs(docs.sections(), p.sectionId, p.slug)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const nav = createMemo(() => {
|
|
35
|
+
const p = props.page()
|
|
36
|
+
if (!p) return { prev: undefined, next: undefined }
|
|
37
|
+
const all = docs.flatPages()
|
|
38
|
+
const idx = all.findIndex(pg => pg.id === p.id)
|
|
39
|
+
const section = docs.sections().find(s => s.id === p.sectionId)
|
|
40
|
+
return {
|
|
41
|
+
prev: idx > 0 ? { title: all[idx - 1].title, href: `/docs/${section?.slug ?? ''}/${all[idx - 1].slug}` } : undefined,
|
|
42
|
+
next: idx < all.length - 1 ? { title: all[idx + 1].title, href: `/docs/${section?.slug ?? ''}/${all[idx + 1].slug}` } : undefined,
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Show when={!docs.isLoading() && props.page() !== undefined} fallback={
|
|
48
|
+
<div class="flex min-h-screen bg-[#090a0f]">
|
|
49
|
+
<div class="hidden w-[260px] shrink-0 border-r border-white/5 bg-[#0b0c12] lg:block"><div class="space-y-3 p-4">{Array.from({ length: 8 }).map(() => <div class="h-8 animate-pulse rounded-lg bg-white/5" />)}</div></div>
|
|
50
|
+
<div class="flex-1 px-10 py-8"><div class="mx-auto max-w-3xl space-y-4"><div class="h-10 w-96 animate-pulse rounded bg-white/5" /></div></div>
|
|
51
|
+
</div>
|
|
52
|
+
}>
|
|
53
|
+
<Show when={props.page()} fallback={
|
|
54
|
+
<div class="flex min-h-screen flex-col items-center justify-center bg-[#090a0f] text-center">
|
|
55
|
+
<div class="mb-4 text-6xl opacity-30">🔍</div>
|
|
56
|
+
<h2 class="mb-2 text-xl font-semibold text-white/80">Page not found</h2>
|
|
57
|
+
</div>
|
|
58
|
+
}>
|
|
59
|
+
{(page) => (
|
|
60
|
+
<DocsLayout sections={docs.sections()} toc={toc()} activeHeadingId={activeId()} breadcrumbs={breadcrumbs()}
|
|
61
|
+
currentPageId={page().id} onNavigate={props.onNavigate}>
|
|
62
|
+
<div class="mb-8">
|
|
63
|
+
<h1 class="mb-3 text-3xl font-bold tracking-tight">{page().title}</h1>
|
|
64
|
+
<div class="flex flex-wrap items-center gap-4 text-sm text-white/40">
|
|
65
|
+
<span>{page().author.name}</span>
|
|
66
|
+
<Show when={page().readingTime > 0}><span>{page().readingTime} min read</span></Show>
|
|
67
|
+
<Show when={page().viewCount > 0}><span>{page().viewCount.toLocaleString()} views</span></Show>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<DocPage page={page()} />
|
|
71
|
+
<div class="mt-10 flex items-center justify-between border-t border-white/5 pt-6">
|
|
72
|
+
<EditButton pageSlug={page().slug} editUrl={props.editPageUrl} />
|
|
73
|
+
</div>
|
|
74
|
+
<PageNavigation prev={nav().prev} next={nav().next} />
|
|
75
|
+
</DocsLayout>
|
|
76
|
+
)}
|
|
77
|
+
</Show>
|
|
78
|
+
</Show>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createSignal, createMemo, Show, For } from 'solid-js'
|
|
2
|
+
import type { DocPage, DocSection } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
interface DocsAdminPageProps {
|
|
5
|
+
tree: () => (DocSection & { pages: DocPage[]; pageCount: number })[] | undefined
|
|
6
|
+
allPages?: DocPage[]
|
|
7
|
+
admin: {
|
|
8
|
+
createSection: (d: Record<string, unknown>) => Promise<void>
|
|
9
|
+
deleteSection: (id: string) => Promise<void>
|
|
10
|
+
createPage: (d: Record<string, unknown>) => Promise<void>
|
|
11
|
+
publishPage: (id: string) => Promise<void>
|
|
12
|
+
archivePage: (id: string) => Promise<void>
|
|
13
|
+
deletePage: (id: string) => Promise<void>
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function DocsAdminPage(props: DocsAdminPageProps) {
|
|
18
|
+
const sections = createMemo(() => props.tree?.() ?? [])
|
|
19
|
+
const [selectedId, setSelectedId] = createSignal<string | null>(null)
|
|
20
|
+
const [showSF, setShowSF] = createSignal(false)
|
|
21
|
+
const [showPF, setShowPF] = createSignal(false)
|
|
22
|
+
const [sf, setSf] = createSignal({ title: '', slug: '', description: '', icon: '', access: 'team' })
|
|
23
|
+
const [pf, setPf] = createSignal({ title: '', slug: '', content: '', access: 'team', tags: '' })
|
|
24
|
+
|
|
25
|
+
const selectedSection = createMemo(() => sections().find(s => s.id === selectedId()))
|
|
26
|
+
const sectionPages = createMemo(() => {
|
|
27
|
+
const sec = selectedSection()
|
|
28
|
+
if (!sec) return []
|
|
29
|
+
if (props.allPages) return props.allPages.filter(p => p.sectionId === selectedId())
|
|
30
|
+
return sec.pages ?? []
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Show when={props.tree?.() !== undefined} fallback={
|
|
35
|
+
<div class="min-h-screen bg-[#090a0f] px-6 py-12"><div class="mx-auto max-w-6xl"><div class="mb-8 h-10 w-48 animate-pulse rounded bg-white/5" /></div></div>
|
|
36
|
+
}>
|
|
37
|
+
<div class="min-h-screen bg-[#090a0f] text-white">
|
|
38
|
+
<div class="mx-auto max-w-6xl px-6 py-12">
|
|
39
|
+
<h1 class="mb-8 text-2xl font-bold">Docs Admin</h1>
|
|
40
|
+
<div class="grid gap-6 md:grid-cols-2">
|
|
41
|
+
{/* Sections */}
|
|
42
|
+
<div class="rounded-xl border border-white/8 bg-white/[0.02] p-5">
|
|
43
|
+
<div class="mb-4 flex items-center justify-between">
|
|
44
|
+
<h2 class="text-lg font-semibold text-white/80">Sections</h2>
|
|
45
|
+
<button type="button" onClick={() => setShowSF(!showSF())} class="rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium hover:bg-indigo-500">+ Add</button>
|
|
46
|
+
</div>
|
|
47
|
+
<Show when={showSF()}>
|
|
48
|
+
<div class="mb-4 space-y-2 rounded-lg border border-white/10 bg-white/5 p-4">
|
|
49
|
+
<input type="text" placeholder="Title" value={sf().title} onInput={e => setSf({...sf(), title: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
50
|
+
<input type="text" placeholder="Slug" value={sf().slug} onInput={e => setSf({...sf(), slug: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
51
|
+
<input type="text" placeholder="Icon" value={sf().icon} onInput={e => setSf({...sf(), icon: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
52
|
+
<input type="text" placeholder="Description" value={sf().description} onInput={e => setSf({...sf(), description: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
53
|
+
<div class="flex gap-2">
|
|
54
|
+
<button type="button" onClick={async () => { await props.admin.createSection({...sf(), order: sections().length}); setSf({title:'',slug:'',description:'',icon:'',access:'team'}); setShowSF(false) }} class="rounded-lg bg-indigo-600 px-4 py-2 text-xs font-medium hover:bg-indigo-500">Create</button>
|
|
55
|
+
<button type="button" onClick={() => setShowSF(false)} class="rounded-lg border border-white/10 px-4 py-2 text-xs hover:bg-white/5">Cancel</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</Show>
|
|
59
|
+
<div class="space-y-1">
|
|
60
|
+
<Show when={sections().length === 0}><p class="py-8 text-center text-sm text-white/30">No sections</p></Show>
|
|
61
|
+
<For each={sections()}>
|
|
62
|
+
{(section) => (
|
|
63
|
+
<div class={`group flex items-center gap-2 rounded-lg px-3 py-2.5 cursor-pointer ${selectedId() === section.id ? 'bg-indigo-500/15 text-indigo-300' : 'hover:bg-white/5 text-white/70'}`}
|
|
64
|
+
onClick={() => setSelectedId(section.id)}>
|
|
65
|
+
<Show when={section.icon}><span>{section.icon}</span></Show>
|
|
66
|
+
<span class="flex-1 text-sm font-medium truncate">{section.title}</span>
|
|
67
|
+
<span class="text-[11px] tabular-nums opacity-40">{section.pageCount ?? 0}</span>
|
|
68
|
+
<button type="button" onClick={(e) => { e.stopPropagation(); if (confirm(`Delete "${section.title}"?`)) props.admin.deleteSection(section.id) }}
|
|
69
|
+
class="rounded p-1 text-white/20 opacity-0 group-hover:opacity-100 hover:text-red-400">✕</button>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</For>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
{/* Pages */}
|
|
76
|
+
<div class="rounded-xl border border-white/8 bg-white/[0.02] p-5">
|
|
77
|
+
<div class="mb-4 flex items-center justify-between">
|
|
78
|
+
<h2 class="text-lg font-semibold text-white/80">{selectedSection() ? `Pages — ${selectedSection()!.title}` : 'Pages'}</h2>
|
|
79
|
+
<Show when={selectedSection()}><button type="button" onClick={() => setShowPF(!showPF())} class="rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-medium hover:bg-indigo-500">+ Add</button></Show>
|
|
80
|
+
</div>
|
|
81
|
+
<Show when={!selectedSection()}><p class="py-16 text-center text-sm text-white/30">Select a section</p></Show>
|
|
82
|
+
<Show when={showPF() && selectedSection()}>
|
|
83
|
+
<div class="mb-4 space-y-2 rounded-lg border border-white/10 bg-white/5 p-4">
|
|
84
|
+
<input type="text" placeholder="Title" value={pf().title} onInput={e => setPf({...pf(), title: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
85
|
+
<input type="text" placeholder="Slug" value={pf().slug} onInput={e => setPf({...pf(), slug: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
86
|
+
<textarea placeholder="Content (MDX)" value={pf().content} onInput={e => setPf({...pf(), content: e.currentTarget.value})} rows={4} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40 resize-y" />
|
|
87
|
+
<input type="text" placeholder="Tags (comma)" value={pf().tags} onInput={e => setPf({...pf(), tags: e.currentTarget.value})} class="w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm outline-none focus:border-indigo-500/40" />
|
|
88
|
+
<div class="flex gap-2">
|
|
89
|
+
<button type="button" onClick={async () => { await props.admin.createPage({title:pf().title,slug:pf().slug,content:pf().content,sectionId:selectedId()!,access:pf().access as 'public'|'team'|'admin',tags:pf().tags?pf().tags.split(',').map(t=>t.trim()):[],order:sectionPages().length}); setPf({title:'',slug:'',content:'',access:'team',tags:''}); setShowPF(false) }} class="rounded-lg bg-indigo-600 px-4 py-2 text-xs font-medium hover:bg-indigo-500">Create</button>
|
|
90
|
+
<button type="button" onClick={() => setShowPF(false)} class="rounded-lg border border-white/10 px-4 py-2 text-xs hover:bg-white/5">Cancel</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</Show>
|
|
94
|
+
<Show when={selectedSection() && sectionPages().length > 0}>
|
|
95
|
+
<div class="space-y-1">
|
|
96
|
+
<For each={sectionPages()}>
|
|
97
|
+
{(page) => (
|
|
98
|
+
<div class="group flex items-center gap-3 rounded-lg px-3 py-2.5 hover:bg-white/5">
|
|
99
|
+
<div class="flex-1 min-w-0">
|
|
100
|
+
<p class="text-sm font-medium text-white/80 truncate">{page.title}</p>
|
|
101
|
+
<p class="text-[11px] text-white/30">/{page.slug}</p>
|
|
102
|
+
</div>
|
|
103
|
+
<span class={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium ${page.status === 'published' ? 'bg-emerald-500/15 text-emerald-400' : page.status === 'archived' ? 'bg-white/5 text-white/30' : 'bg-amber-500/15 text-amber-400'}`}>{page.status}</span>
|
|
104
|
+
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100">
|
|
105
|
+
<Show when={page.status === 'draft'}><button type="button" onClick={() => props.admin.publishPage(page.id)} class="rounded px-2 py-1 text-[11px] text-emerald-400 hover:bg-emerald-500/10">Publish</button></Show>
|
|
106
|
+
<Show when={page.status === 'published'}><button type="button" onClick={() => props.admin.archivePage(page.id)} class="rounded px-2 py-1 text-[11px] text-amber-400 hover:bg-amber-500/10">Archive</button></Show>
|
|
107
|
+
<button type="button" onClick={() => { if (confirm(`Delete "${page.title}"?`)) props.admin.deletePage(page.id) }} class="rounded p-1 text-white/20 hover:text-red-400">✕</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</For>
|
|
112
|
+
</div>
|
|
113
|
+
</Show>
|
|
114
|
+
<Show when={selectedSection() && sectionPages().length === 0 && !showPF()}>
|
|
115
|
+
<p class="py-12 text-center text-sm text-white/30">No pages in this section</p>
|
|
116
|
+
</Show>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</Show>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createSignal, createEffect, createMemo, For, Show, onCleanup } from 'solid-js'
|
|
2
|
+
import type { DocPage, DocSection, SearchResult } from '@geenius-docs/shared'
|
|
3
|
+
import { buildDocsIndex, searchDocs } from '@geenius-docs/shared'
|
|
4
|
+
import { createDocs } from '../primitives/createDocs'
|
|
5
|
+
import { createDocSearch } from '../primitives/createDocSearch'
|
|
6
|
+
import { DocSearch } from '../components/DocSearch'
|
|
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(props: DocsIndexPageProps) {
|
|
14
|
+
const docs = createDocs(props.tree)
|
|
15
|
+
const [isSearchOpen, setIsSearchOpen] = createSignal(false)
|
|
16
|
+
|
|
17
|
+
const searchFn = (q: string): SearchResult[] => {
|
|
18
|
+
const index = buildDocsIndex(docs.flatPages(), docs.sections())
|
|
19
|
+
return searchDocs(q, index)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const search = createDocSearch(searchFn)
|
|
23
|
+
|
|
24
|
+
createEffect(() => {
|
|
25
|
+
const handler = (e: KeyboardEvent) => {
|
|
26
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setIsSearchOpen(true) }
|
|
27
|
+
}
|
|
28
|
+
document.addEventListener('keydown', handler)
|
|
29
|
+
onCleanup(() => document.removeEventListener('keydown', handler))
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Show when={!docs.isLoading()} fallback={
|
|
34
|
+
<div class="min-h-screen bg-[#090a0f] px-6 py-16">
|
|
35
|
+
<div class="mx-auto max-w-5xl">
|
|
36
|
+
<div class="mb-10 h-10 w-64 animate-pulse rounded-lg bg-white/5" />
|
|
37
|
+
<div class="mb-8 h-12 w-full max-w-xl animate-pulse rounded-xl bg-white/5" />
|
|
38
|
+
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
39
|
+
<For each={Array(6)}>{() => <div class="h-36 animate-pulse rounded-xl bg-white/5" />}</For>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
}>
|
|
44
|
+
<Show when={docs.sections().length > 0} fallback={
|
|
45
|
+
<div class="flex min-h-screen flex-col items-center justify-center bg-[#090a0f] text-center">
|
|
46
|
+
<div class="mb-4 text-6xl opacity-30">📚</div>
|
|
47
|
+
<h2 class="mb-2 text-xl font-semibold text-white/80">No documentation yet</h2>
|
|
48
|
+
<p class="max-w-sm text-sm text-white/40">Create your first section and pages to get started.</p>
|
|
49
|
+
</div>
|
|
50
|
+
}>
|
|
51
|
+
<div class="min-h-screen bg-[#090a0f] text-white">
|
|
52
|
+
<div class="mx-auto max-w-5xl px-6 py-16">
|
|
53
|
+
<h1 class="mb-2 text-4xl font-bold tracking-tight">Documentation</h1>
|
|
54
|
+
<p class="mb-10 text-lg text-white/50">Browse guides, API references, and tutorials.</p>
|
|
55
|
+
<button type="button" onClick={() => setIsSearchOpen(true)}
|
|
56
|
+
class="mb-12 flex w-full max-w-xl items-center gap-3 rounded-xl border border-white/10 bg-white/5 px-5 py-3.5 text-left text-sm text-white/30 transition-colors hover:border-white/20">
|
|
57
|
+
<svg class="h-4.5 w-4.5 shrink-0" viewBox="0 0 20 20" fill="currentColor"><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" /></svg>
|
|
58
|
+
<span class="flex-1">Search documentation…</span>
|
|
59
|
+
<kbd class="rounded border border-white/10 bg-white/5 px-2 py-0.5 text-[11px]">⌘K</kbd>
|
|
60
|
+
</button>
|
|
61
|
+
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
62
|
+
<For each={docs.sections().filter(s => !s.parentId)}>
|
|
63
|
+
{(section) => (
|
|
64
|
+
<button type="button" onClick={() => { const fp = section.pages?.[0]; if (fp && props.onSelectPage) props.onSelectPage(fp, section) }}
|
|
65
|
+
class="group flex flex-col rounded-xl border border-white/8 bg-white/[0.03] p-5 text-left transition-all hover:border-indigo-500/30 hover:bg-white/[0.06]">
|
|
66
|
+
<Show when={section.icon}><span class="mb-3 text-2xl">{section.icon}</span></Show>
|
|
67
|
+
<h3 class="mb-1.5 text-base font-semibold text-white/90 group-hover:text-white">{section.title}</h3>
|
|
68
|
+
<Show when={section.description}><p class="mb-3 flex-1 text-sm leading-relaxed text-white/40">{section.description}</p></Show>
|
|
69
|
+
<div class="text-xs text-white/25"><span>{section.pageCount ?? 0} pages</span></div>
|
|
70
|
+
</button>
|
|
71
|
+
)}
|
|
72
|
+
</For>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<DocSearch results={search.results()} query={search.query()} onQuery={search.setQuery}
|
|
76
|
+
onSelect={(result) => { setIsSearchOpen(false); search.clearSearch();
|
|
77
|
+
const section = docs.sections().find(s => s.slug === result.sectionSlug)
|
|
78
|
+
const page = docs.flatPages().find(p => p.id === result.pageId)
|
|
79
|
+
if (page && section && props.onSelectPage) props.onSelectPage(page, section)
|
|
80
|
+
}} isOpen={isSearchOpen()} onClose={() => setIsSearchOpen(false)} />
|
|
81
|
+
</div>
|
|
82
|
+
</Show>
|
|
83
|
+
</Show>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup } from 'solid-js'
|
|
2
|
+
import type { SearchResult } from '@geenius-docs/shared'
|
|
3
|
+
|
|
4
|
+
export function createDocSearch(
|
|
5
|
+
searchFn: (query: string) => SearchResult[] | Promise<SearchResult[]>,
|
|
6
|
+
debounceMs = 250
|
|
7
|
+
) {
|
|
8
|
+
const [query, setQuery] = createSignal('')
|
|
9
|
+
const [results, setResults] = createSignal<SearchResult[]>([])
|
|
10
|
+
const [isSearching, setIsSearching] = createSignal(false)
|
|
11
|
+
|
|
12
|
+
createEffect(() => {
|
|
13
|
+
const q = query()
|
|
14
|
+
if (!q.trim()) {
|
|
15
|
+
setResults([])
|
|
16
|
+
setIsSearching(false)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setIsSearching(true)
|
|
21
|
+
const timer = setTimeout(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const r = await searchFn(q)
|
|
24
|
+
setResults(r)
|
|
25
|
+
} catch {
|
|
26
|
+
setResults([])
|
|
27
|
+
} finally {
|
|
28
|
+
setIsSearching(false)
|
|
29
|
+
}
|
|
30
|
+
}, debounceMs)
|
|
31
|
+
|
|
32
|
+
onCleanup(() => clearTimeout(timer))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const clearSearch = () => {
|
|
36
|
+
setQuery('')
|
|
37
|
+
setResults([])
|
|
38
|
+
setIsSearching(false)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { results, isSearching, query, setQuery, clearSearch }
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createSignal, createMemo } from 'solid-js'
|
|
2
|
+
import type { DocPage, DocSection, DocsConfig } from '@geenius-docs/shared'
|
|
3
|
+
import { defaultDocsConfig } from '@geenius-docs/shared'
|
|
4
|
+
|
|
5
|
+
export function createDocs(
|
|
6
|
+
tree: () => (DocSection & { pages: DocPage[]; pageCount: number })[] | undefined,
|
|
7
|
+
config?: Partial<DocsConfig>
|
|
8
|
+
) {
|
|
9
|
+
const [currentSection, setSection] = createSignal<DocSection | null>(null)
|
|
10
|
+
const [currentPage, setPage] = createSignal<DocPage | null>(null)
|
|
11
|
+
const [searchQuery, setSearchQuery] = createSignal('')
|
|
12
|
+
|
|
13
|
+
const mergedConfig = createMemo(() => ({ ...defaultDocsConfig, ...config }))
|
|
14
|
+
const sections = createMemo(() => tree() ?? [])
|
|
15
|
+
const flatPages = createMemo(() => sections().flatMap((s) => s.pages ?? []))
|
|
16
|
+
const isLoading = createMemo(() => tree() === undefined)
|
|
17
|
+
|
|
18
|
+
const handleSetSection = (section: DocSection | null) => {
|
|
19
|
+
setSection(section)
|
|
20
|
+
setPage(null)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
sections,
|
|
25
|
+
currentSection,
|
|
26
|
+
setSection: handleSetSection,
|
|
27
|
+
currentPage,
|
|
28
|
+
setPage,
|
|
29
|
+
searchQuery,
|
|
30
|
+
setSearchQuery,
|
|
31
|
+
isLoading,
|
|
32
|
+
config: mergedConfig,
|
|
33
|
+
flatPages,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function createDocsAdmin(mutations: {
|
|
2
|
+
createSection: (args: Record<string, unknown>) => Promise<unknown>
|
|
3
|
+
updateSection: (args: Record<string, unknown>) => Promise<unknown>
|
|
4
|
+
deleteSection: (args: Record<string, unknown>) => Promise<unknown>
|
|
5
|
+
reorderSections: (args: Record<string, unknown>) => Promise<unknown>
|
|
6
|
+
createPage: (args: Record<string, unknown>) => Promise<unknown>
|
|
7
|
+
updatePage: (args: Record<string, unknown>) => Promise<unknown>
|
|
8
|
+
publishPage: (args: Record<string, unknown>) => Promise<unknown>
|
|
9
|
+
archivePage: (args: Record<string, unknown>) => Promise<unknown>
|
|
10
|
+
deletePage: (args: Record<string, unknown>) => Promise<unknown>
|
|
11
|
+
reorderPages: (args: Record<string, unknown>) => Promise<unknown>
|
|
12
|
+
movePage: (args: Record<string, unknown>) => Promise<unknown>
|
|
13
|
+
}) {
|
|
14
|
+
return {
|
|
15
|
+
createSection: async (data: {
|
|
16
|
+
title: string; slug: string; parentId?: string; order: number
|
|
17
|
+
icon?: string; description?: string; access: 'public' | 'team' | 'admin'
|
|
18
|
+
}) => { await mutations.createSection(data) },
|
|
19
|
+
|
|
20
|
+
updateSection: async (sectionId: string, data: Record<string, unknown>) => {
|
|
21
|
+
await mutations.updateSection({ sectionId, ...data })
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
deleteSection: async (sectionId: string) => {
|
|
25
|
+
await mutations.deleteSection({ sectionId })
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
reorderSections: async (sectionIds: string[]) => {
|
|
29
|
+
await mutations.reorderSections({ sectionIds })
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
createPage: async (data: {
|
|
33
|
+
title: string; slug: string; content: string; sectionId: string
|
|
34
|
+
access: 'public' | 'team' | 'admin'; tags?: string[]; version?: string
|
|
35
|
+
status?: 'draft' | 'published' | 'archived'; author?: { name: string; avatar?: string }
|
|
36
|
+
excerpt?: string; order?: number
|
|
37
|
+
}) => { await mutations.createPage(data) },
|
|
38
|
+
|
|
39
|
+
updatePage: async (pageId: string, data: Record<string, unknown>) => {
|
|
40
|
+
await mutations.updatePage({ pageId, ...data })
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
publishPage: async (pageId: string) => {
|
|
44
|
+
await mutations.publishPage({ pageId })
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
archivePage: async (pageId: string) => {
|
|
48
|
+
await mutations.archivePage({ pageId })
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
deletePage: async (pageId: string) => {
|
|
52
|
+
await mutations.deletePage({ pageId })
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
reorderPages: async (pageIds: string[]) => {
|
|
56
|
+
await mutations.reorderPages({ pageIds })
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
movePage: async (pageId: string, newSectionId: string, newOrder: number) => {
|
|
60
|
+
await mutations.movePage({ pageId, newSectionId, newOrder })
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createSignal, createEffect, createMemo, onCleanup } from 'solid-js'
|
|
2
|
+
import type { TocItem } from '@geenius-docs/shared'
|
|
3
|
+
import { extractToc } from '@geenius-docs/shared'
|
|
4
|
+
|
|
5
|
+
export function createTableOfContents(mdxContent: () => string | undefined) {
|
|
6
|
+
const [activeId, setActiveId] = createSignal('')
|
|
7
|
+
|
|
8
|
+
const toc = createMemo<TocItem[]>(() => {
|
|
9
|
+
const content = mdxContent()
|
|
10
|
+
if (!content) return []
|
|
11
|
+
return extractToc(content)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const flatIds = createMemo(() => {
|
|
15
|
+
const ids: string[] = []
|
|
16
|
+
const collect = (items: TocItem[]) => {
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
ids.push(item.id)
|
|
19
|
+
collect(item.children)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
collect(toc())
|
|
23
|
+
return ids
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
createEffect(() => {
|
|
27
|
+
const ids = flatIds()
|
|
28
|
+
if (ids.length === 0) return
|
|
29
|
+
|
|
30
|
+
const observer = new IntersectionObserver(
|
|
31
|
+
(entries) => {
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.isIntersecting) {
|
|
34
|
+
setActiveId(entry.target.id)
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{ rootMargin: '-80px 0px -70% 0px', threshold: 0 }
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
for (const id of ids) {
|
|
43
|
+
const el = document.getElementById(id)
|
|
44
|
+
if (el) observer.observe(el)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onCleanup(() => observer.disconnect())
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return { toc, activeId, setActiveId }
|
|
51
|
+
}
|