@brainfish-ai/devdoc 0.1.21
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/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- package/templates/openapi/vercel.json +6 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useState, useMemo } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { usePathname } from 'next/navigation'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
import { MagnifyingGlass, CaretRight, CaretDown, ArrowSquareOut, X } from '@phosphor-icons/react'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { Button } from '@/components/ui/button'
|
|
10
|
+
import * as PhosphorIcons from '@phosphor-icons/react'
|
|
11
|
+
import type { Navigation, NavGroup, NavItem } from '@/lib/docs/navigation'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Docs Sidebar Navigation Component
|
|
15
|
+
*
|
|
16
|
+
* Matches the API docs sidebar styling for visual consistency
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Dynamic icon resolver
|
|
20
|
+
function getIcon(iconName: string): React.ComponentType<{ className?: string; weight?: string }> | null {
|
|
21
|
+
if (!iconName) return null
|
|
22
|
+
|
|
23
|
+
const pascalCase = iconName
|
|
24
|
+
.split(/[-_]/)
|
|
25
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
26
|
+
.join('')
|
|
27
|
+
|
|
28
|
+
const Icon = (PhosphorIcons as Record<string, unknown>)[pascalCase] as React.ComponentType<{ className?: string; weight?: string }> | undefined
|
|
29
|
+
return Icon || null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Helper to check if item is a NavItem (has href)
|
|
33
|
+
function isNavItem(item: NavItem | NavGroup): item is NavItem {
|
|
34
|
+
return 'href' in item && typeof item.href === 'string'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Recursively check if any item matches the path
|
|
38
|
+
function hasMatchingItem(items: (NavItem | NavGroup)[], path: string): boolean {
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
if (isNavItem(item)) {
|
|
41
|
+
if (item.href === path || path.startsWith(item.href + '/')) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if ('items' in item && Array.isArray(item.items)) {
|
|
46
|
+
if (hasMatchingItem(item.items, path)) {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Filter navigation based on search query
|
|
55
|
+
function filterNavigation(navigation: Navigation, query: string): Navigation {
|
|
56
|
+
if (!query.trim()) return navigation
|
|
57
|
+
|
|
58
|
+
const lowerQuery = query.toLowerCase()
|
|
59
|
+
|
|
60
|
+
const filterItems = (items: (NavItem | NavGroup)[]): (NavItem | NavGroup)[] => {
|
|
61
|
+
return items.reduce<(NavItem | NavGroup)[]>((acc, item) => {
|
|
62
|
+
if (isNavItem(item)) {
|
|
63
|
+
if (item.title.toLowerCase().includes(lowerQuery)) {
|
|
64
|
+
acc.push(item)
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// It's a NavGroup
|
|
68
|
+
const filteredNestedItems = filterItems(item.items)
|
|
69
|
+
if (filteredNestedItems.length > 0 || item.title.toLowerCase().includes(lowerQuery)) {
|
|
70
|
+
acc.push({
|
|
71
|
+
...item,
|
|
72
|
+
items: filteredNestedItems.length > 0 ? filteredNestedItems : item.items,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return acc
|
|
77
|
+
}, [])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const filterGroups = (groups: NavGroup[]): NavGroup[] => {
|
|
81
|
+
return groups.reduce<NavGroup[]>((acc, group) => {
|
|
82
|
+
const filteredItems = filterItems(group.items)
|
|
83
|
+
|
|
84
|
+
if (filteredItems.length > 0 || group.title.toLowerCase().includes(lowerQuery)) {
|
|
85
|
+
acc.push({
|
|
86
|
+
...group,
|
|
87
|
+
items: filteredItems.length > 0 ? filteredItems : group.items,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return acc
|
|
92
|
+
}, [])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
tabs: navigation.tabs?.map(tab => ({
|
|
97
|
+
...tab,
|
|
98
|
+
groups: filterGroups(tab.groups),
|
|
99
|
+
})).filter(tab => tab.groups.length > 0),
|
|
100
|
+
groups: navigation.groups ? filterGroups(navigation.groups) : undefined,
|
|
101
|
+
anchors: navigation.anchors,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface DocsSidebarProps {
|
|
106
|
+
navigation: Navigation
|
|
107
|
+
siteName?: string
|
|
108
|
+
className?: string
|
|
109
|
+
isMobile?: boolean
|
|
110
|
+
isOpen?: boolean
|
|
111
|
+
onClose?: () => void
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function DocsSidebar({
|
|
115
|
+
navigation,
|
|
116
|
+
siteName = 'Documentation',
|
|
117
|
+
className,
|
|
118
|
+
isMobile = false,
|
|
119
|
+
isOpen = true,
|
|
120
|
+
onClose,
|
|
121
|
+
}: DocsSidebarProps) {
|
|
122
|
+
const pathname = usePathname()
|
|
123
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
124
|
+
const [activeTab, setActiveTab] = useState<string>(
|
|
125
|
+
navigation.tabs?.[0]?.slug || ''
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
// Filter navigation based on search
|
|
129
|
+
const filteredNav = useMemo(() =>
|
|
130
|
+
filterNavigation(navigation, searchQuery),
|
|
131
|
+
[navigation, searchQuery]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// Find active tab from current path
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
if (navigation.tabs) {
|
|
137
|
+
for (const tab of navigation.tabs) {
|
|
138
|
+
for (const group of tab.groups) {
|
|
139
|
+
if (hasMatchingItem(group.items, pathname)) {
|
|
140
|
+
setActiveTab(tab.slug)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, [pathname, navigation.tabs])
|
|
147
|
+
|
|
148
|
+
const currentTabData = filteredNav.tabs?.find(t => t.slug === activeTab)
|
|
149
|
+
const groups = currentTabData?.groups || filteredNav.groups || []
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
{/* Mobile overlay backdrop */}
|
|
154
|
+
{isMobile && isOpen && (
|
|
155
|
+
<div
|
|
156
|
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
|
157
|
+
onClick={onClose}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
<aside
|
|
162
|
+
className={cn(
|
|
163
|
+
"flex flex-col border-r bg-sidebar border-sidebar-border overflow-hidden",
|
|
164
|
+
// Desktop: always visible, fixed width
|
|
165
|
+
"lg:relative lg:w-72 lg:h-full",
|
|
166
|
+
// Mobile: drawer behavior
|
|
167
|
+
"fixed inset-y-0 left-0 z-50 w-[280px] h-full",
|
|
168
|
+
"transform transition-transform duration-300 ease-in-out",
|
|
169
|
+
"lg:transform-none lg:translate-x-0",
|
|
170
|
+
isMobile && !isOpen && "-translate-x-full",
|
|
171
|
+
isMobile && isOpen && "translate-x-0",
|
|
172
|
+
className
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
{/* Header */}
|
|
176
|
+
<div className="px-4 h-[41px] flex items-center justify-between border-b border-sidebar-border shrink-0">
|
|
177
|
+
<h2 className="text-sm font-semibold text-sidebar-foreground truncate">
|
|
178
|
+
{siteName}
|
|
179
|
+
</h2>
|
|
180
|
+
|
|
181
|
+
{/* Mobile close button */}
|
|
182
|
+
{isMobile && (
|
|
183
|
+
<Button
|
|
184
|
+
variant="ghost"
|
|
185
|
+
size="icon"
|
|
186
|
+
onClick={onClose}
|
|
187
|
+
className="h-7 w-7 lg:hidden"
|
|
188
|
+
>
|
|
189
|
+
<X className="h-4 w-4" weight="bold" />
|
|
190
|
+
</Button>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Search */}
|
|
195
|
+
<div className="px-3 py-2 border-b border-sidebar-border shrink-0">
|
|
196
|
+
<div className="relative">
|
|
197
|
+
<MagnifyingGlass
|
|
198
|
+
className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/60"
|
|
199
|
+
/>
|
|
200
|
+
<Input
|
|
201
|
+
type="text"
|
|
202
|
+
placeholder="Search..."
|
|
203
|
+
value={searchQuery}
|
|
204
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
205
|
+
className="w-full pl-8 h-8 text-sm bg-muted/40 border-0 focus-visible:ring-1 focus-visible:ring-border placeholder:text-muted-foreground/50"
|
|
206
|
+
/>
|
|
207
|
+
{searchQuery && (
|
|
208
|
+
<button
|
|
209
|
+
onClick={() => setSearchQuery('')}
|
|
210
|
+
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-muted-foreground/60 hover:text-muted-foreground"
|
|
211
|
+
>
|
|
212
|
+
<span className="text-xs">✕</span>
|
|
213
|
+
</button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Tab selector (if multiple tabs) */}
|
|
219
|
+
{filteredNav.tabs && filteredNav.tabs.length > 1 && (
|
|
220
|
+
<div className="flex border-b border-sidebar-border px-2 py-1 gap-1 shrink-0">
|
|
221
|
+
{filteredNav.tabs.map((tab) => (
|
|
222
|
+
<button
|
|
223
|
+
key={tab.slug}
|
|
224
|
+
onClick={() => setActiveTab(tab.slug)}
|
|
225
|
+
className={cn(
|
|
226
|
+
'px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
|
|
227
|
+
activeTab === tab.slug
|
|
228
|
+
? 'bg-sidebar-primary text-sidebar-primary-foreground'
|
|
229
|
+
: 'text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent/50'
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{tab.title}
|
|
233
|
+
</button>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{/* Scrollable content */}
|
|
239
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
240
|
+
{groups.map((group, index) => (
|
|
241
|
+
<DocsSidebarSection
|
|
242
|
+
key={`${group.title}-${index}`}
|
|
243
|
+
title={group.title}
|
|
244
|
+
className={index > 0 ? 'border-t border-sidebar-border' : ''}
|
|
245
|
+
>
|
|
246
|
+
{group.items.map((item, itemIndex) => {
|
|
247
|
+
// Handle nested NavGroup
|
|
248
|
+
if (!isNavItem(item)) {
|
|
249
|
+
return (
|
|
250
|
+
<DocsSidebarNestedGroup
|
|
251
|
+
key={`group-${item.title}-${itemIndex}`}
|
|
252
|
+
group={item}
|
|
253
|
+
pathname={pathname}
|
|
254
|
+
onItemSelect={() => isMobile && onClose?.()}
|
|
255
|
+
/>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<DocsSidebarItem
|
|
261
|
+
key={`${item.href}-${itemIndex}`}
|
|
262
|
+
item={item}
|
|
263
|
+
selected={item.href === pathname}
|
|
264
|
+
onSelect={() => isMobile && onClose?.()}
|
|
265
|
+
/>
|
|
266
|
+
)
|
|
267
|
+
})}
|
|
268
|
+
</DocsSidebarSection>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
{/* Global anchors */}
|
|
273
|
+
{filteredNav.anchors && filteredNav.anchors.length > 0 && (
|
|
274
|
+
<div className="border-t border-sidebar-border px-2 py-2 shrink-0">
|
|
275
|
+
{filteredNav.anchors.map((anchor, index) => (
|
|
276
|
+
<DocsSidebarItem
|
|
277
|
+
key={`anchor-${index}`}
|
|
278
|
+
item={anchor}
|
|
279
|
+
selected={false}
|
|
280
|
+
/>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</aside>
|
|
285
|
+
</>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Section component (matches API docs SidebarSection)
|
|
290
|
+
interface DocsSidebarSectionProps {
|
|
291
|
+
title: string
|
|
292
|
+
children: React.ReactNode
|
|
293
|
+
className?: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function DocsSidebarSection({ title, children, className }: DocsSidebarSectionProps) {
|
|
297
|
+
return (
|
|
298
|
+
<div className={cn('flex flex-col', className)}>
|
|
299
|
+
<div className="px-4 py-2.5">
|
|
300
|
+
<span className="text-[11px] font-semibold text-sidebar-foreground/60 uppercase tracking-wider">
|
|
301
|
+
{title}
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
<ul className="flex flex-col gap-px px-2 pb-2">
|
|
305
|
+
{children}
|
|
306
|
+
</ul>
|
|
307
|
+
</div>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Nested group component for recursive groups
|
|
312
|
+
interface DocsSidebarNestedGroupProps {
|
|
313
|
+
group: NavGroup
|
|
314
|
+
pathname: string
|
|
315
|
+
onItemSelect?: () => void
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function DocsSidebarNestedGroup({ group, pathname, onItemSelect }: DocsSidebarNestedGroupProps) {
|
|
319
|
+
const hasActiveChild = hasMatchingItem(group.items, pathname)
|
|
320
|
+
const [isOpen, setIsOpen] = React.useState(hasActiveChild)
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<li className="mb-1">
|
|
324
|
+
<button
|
|
325
|
+
type="button"
|
|
326
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
327
|
+
className={cn(
|
|
328
|
+
"flex items-center justify-between w-full py-1.5 px-2 text-sm rounded-md transition-colors",
|
|
329
|
+
hasActiveChild
|
|
330
|
+
? "text-sidebar-foreground font-medium"
|
|
331
|
+
: "text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent/50"
|
|
332
|
+
)}
|
|
333
|
+
>
|
|
334
|
+
<span className="flex items-center gap-2">
|
|
335
|
+
{group.icon && (() => {
|
|
336
|
+
const Icon = getIcon(group.icon)
|
|
337
|
+
return Icon ? <Icon className="w-4 h-4" /> : null
|
|
338
|
+
})()}
|
|
339
|
+
{group.title}
|
|
340
|
+
</span>
|
|
341
|
+
<CaretRight className={cn("w-4 h-4 transition-transform", isOpen && "rotate-90")} />
|
|
342
|
+
</button>
|
|
343
|
+
|
|
344
|
+
{isOpen && (
|
|
345
|
+
<ul className="ml-4 pl-2 border-l border-sidebar-border space-y-0.5 mt-1">
|
|
346
|
+
{group.items.map((item, idx) => {
|
|
347
|
+
if (!isNavItem(item)) {
|
|
348
|
+
return (
|
|
349
|
+
<DocsSidebarNestedGroup
|
|
350
|
+
key={`nested-group-${item.title}-${idx}`}
|
|
351
|
+
group={item}
|
|
352
|
+
pathname={pathname}
|
|
353
|
+
onItemSelect={onItemSelect}
|
|
354
|
+
/>
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<DocsSidebarItem
|
|
360
|
+
key={`${item.href}-${idx}`}
|
|
361
|
+
item={item}
|
|
362
|
+
selected={item.href === pathname}
|
|
363
|
+
onSelect={onItemSelect}
|
|
364
|
+
/>
|
|
365
|
+
)
|
|
366
|
+
})}
|
|
367
|
+
</ul>
|
|
368
|
+
)}
|
|
369
|
+
</li>
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Item component (matches API docs SidebarItem)
|
|
374
|
+
interface DocsSidebarItemProps {
|
|
375
|
+
item: NavItem
|
|
376
|
+
selected: boolean
|
|
377
|
+
indent?: number
|
|
378
|
+
onSelect?: () => void
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function DocsSidebarItem({ item, selected, indent = 0, onSelect }: DocsSidebarItemProps) {
|
|
382
|
+
const Icon = item.icon ? getIcon(item.icon) : null
|
|
383
|
+
|
|
384
|
+
const content = (
|
|
385
|
+
<>
|
|
386
|
+
{Icon && (
|
|
387
|
+
<Icon
|
|
388
|
+
className={cn(
|
|
389
|
+
'h-4 w-4 shrink-0 mr-2',
|
|
390
|
+
selected ? 'text-sidebar-primary-foreground' : 'text-sidebar-foreground/60'
|
|
391
|
+
)}
|
|
392
|
+
weight={selected ? 'fill' : 'regular'}
|
|
393
|
+
/>
|
|
394
|
+
)}
|
|
395
|
+
<span className="flex-1 truncate">{item.title}</span>
|
|
396
|
+
{item.external && (
|
|
397
|
+
<ArrowSquareOut className="h-3 w-3 shrink-0 ml-1 text-sidebar-foreground/40" />
|
|
398
|
+
)}
|
|
399
|
+
{item.badge && (
|
|
400
|
+
<span className="ml-2 px-1.5 py-0.5 text-[10px] font-medium rounded bg-sidebar-primary/10 text-sidebar-primary">
|
|
401
|
+
{item.badge}
|
|
402
|
+
</span>
|
|
403
|
+
)}
|
|
404
|
+
{item.deprecated && (
|
|
405
|
+
<span className="ml-2 px-1.5 py-0.5 text-[10px] font-medium rounded bg-amber-500/10 text-amber-600 dark:text-amber-400">
|
|
406
|
+
deprecated
|
|
407
|
+
</span>
|
|
408
|
+
)}
|
|
409
|
+
</>
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
const className = cn(
|
|
413
|
+
'group/button flex items-center rounded-sm px-3 py-1.5 w-full text-left',
|
|
414
|
+
'text-sm leading-5 transition-colors duration-150',
|
|
415
|
+
selected
|
|
416
|
+
? 'bg-sidebar-primary text-sidebar-primary-foreground font-medium'
|
|
417
|
+
: 'text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground'
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
const style = indent > 0 ? { paddingLeft: `${indent * 12 + 12}px` } : undefined
|
|
421
|
+
|
|
422
|
+
if (item.external) {
|
|
423
|
+
return (
|
|
424
|
+
<li className="flex flex-col">
|
|
425
|
+
<a
|
|
426
|
+
href={item.href}
|
|
427
|
+
target="_blank"
|
|
428
|
+
rel="noopener noreferrer"
|
|
429
|
+
className={className}
|
|
430
|
+
style={style}
|
|
431
|
+
>
|
|
432
|
+
{content}
|
|
433
|
+
</a>
|
|
434
|
+
</li>
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<li className="flex flex-col">
|
|
440
|
+
<Link
|
|
441
|
+
href={item.href}
|
|
442
|
+
className={className}
|
|
443
|
+
style={style}
|
|
444
|
+
onClick={onSelect}
|
|
445
|
+
>
|
|
446
|
+
{content}
|
|
447
|
+
</Link>
|
|
448
|
+
</li>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Group component with collapsible (matches API docs SidebarGroup)
|
|
453
|
+
interface DocsSidebarGroupProps {
|
|
454
|
+
title: string
|
|
455
|
+
items: NavItem[]
|
|
456
|
+
icon?: string
|
|
457
|
+
defaultOpen?: boolean
|
|
458
|
+
currentPath: string
|
|
459
|
+
onItemSelect?: () => void
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function DocsSidebarGroup({
|
|
463
|
+
title,
|
|
464
|
+
items,
|
|
465
|
+
icon,
|
|
466
|
+
defaultOpen = false,
|
|
467
|
+
currentPath,
|
|
468
|
+
onItemSelect,
|
|
469
|
+
}: DocsSidebarGroupProps) {
|
|
470
|
+
const hasActiveItem = items.some(item => item.href === currentPath)
|
|
471
|
+
const [isOpen, setIsOpen] = useState(defaultOpen || hasActiveItem)
|
|
472
|
+
|
|
473
|
+
const Icon = icon ? getIcon(icon) : null
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<li className="flex flex-col gap-px">
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
aria-expanded={isOpen}
|
|
480
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
481
|
+
className={cn(
|
|
482
|
+
'group/button flex items-center rounded-sm px-3 py-1.5 w-full text-left',
|
|
483
|
+
'text-sm leading-5 transition-colors duration-150',
|
|
484
|
+
'text-sidebar-foreground hover:bg-sidebar-accent/50 hover:text-sidebar-accent-foreground'
|
|
485
|
+
)}
|
|
486
|
+
>
|
|
487
|
+
<span className="mr-1.5 text-sidebar-foreground/60 group-hover/button:text-sidebar-foreground">
|
|
488
|
+
{isOpen ? (
|
|
489
|
+
<CaretDown weight="bold" className="h-3.5 w-3.5" />
|
|
490
|
+
) : (
|
|
491
|
+
<CaretRight weight="bold" className="h-3.5 w-3.5" />
|
|
492
|
+
)}
|
|
493
|
+
</span>
|
|
494
|
+
{Icon && <Icon className="h-4 w-4 mr-2 text-sidebar-foreground/60" weight="regular" />}
|
|
495
|
+
<span className="flex-1 truncate">{title}</span>
|
|
496
|
+
</button>
|
|
497
|
+
|
|
498
|
+
{isOpen && (
|
|
499
|
+
<ul className="flex flex-col gap-px">
|
|
500
|
+
{items.map((item, index) => (
|
|
501
|
+
<DocsSidebarItem
|
|
502
|
+
key={`${item.href}-${index}`}
|
|
503
|
+
item={item}
|
|
504
|
+
selected={item.href === currentPath}
|
|
505
|
+
indent={1}
|
|
506
|
+
onSelect={onItemSelect}
|
|
507
|
+
/>
|
|
508
|
+
))}
|
|
509
|
+
</ul>
|
|
510
|
+
)}
|
|
511
|
+
</li>
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export { DocsSidebarSection, DocsSidebarItem }
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from 'react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import type { TocItem } from '@/lib/docs/navigation'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Table of Contents Component
|
|
9
|
+
*
|
|
10
|
+
* Displays page headings with active state tracking
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface TableOfContentsProps {
|
|
14
|
+
headings: TocItem[]
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TableOfContents({ headings, className }: TableOfContentsProps) {
|
|
19
|
+
const [activeId, setActiveId] = useState<string>('')
|
|
20
|
+
|
|
21
|
+
// Track which heading is currently in view
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (headings.length === 0) return
|
|
24
|
+
|
|
25
|
+
const observer = new IntersectionObserver(
|
|
26
|
+
(entries) => {
|
|
27
|
+
entries.forEach((entry) => {
|
|
28
|
+
if (entry.isIntersecting) {
|
|
29
|
+
setActiveId(entry.target.id)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
rootMargin: '-80px 0px -80% 0px',
|
|
35
|
+
threshold: 0,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Observe all headings
|
|
40
|
+
headings.forEach((heading) => {
|
|
41
|
+
const element = document.getElementById(heading.id)
|
|
42
|
+
if (element) {
|
|
43
|
+
observer.observe(element)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return () => observer.disconnect()
|
|
48
|
+
}, [headings])
|
|
49
|
+
|
|
50
|
+
if (headings.length === 0) return null
|
|
51
|
+
|
|
52
|
+
// Filter to only show h2 and h3
|
|
53
|
+
const filteredHeadings = headings.filter(h => h.level >= 2 && h.level <= 3)
|
|
54
|
+
|
|
55
|
+
if (filteredHeadings.length === 0) return null
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<nav className={cn('text-sm', className)}>
|
|
59
|
+
<h4 className="font-semibold text-foreground mb-3">On this page</h4>
|
|
60
|
+
<ul className="space-y-2">
|
|
61
|
+
{filteredHeadings.map((heading) => (
|
|
62
|
+
<li
|
|
63
|
+
key={heading.id}
|
|
64
|
+
style={{ paddingLeft: `${(heading.level - 2) * 12}px` }}
|
|
65
|
+
>
|
|
66
|
+
<a
|
|
67
|
+
href={`#${heading.id}`}
|
|
68
|
+
onClick={(e) => {
|
|
69
|
+
e.preventDefault()
|
|
70
|
+
const element = document.getElementById(heading.id)
|
|
71
|
+
if (element) {
|
|
72
|
+
element.scrollIntoView({ behavior: 'smooth' })
|
|
73
|
+
// Update URL without scroll
|
|
74
|
+
window.history.pushState(null, '', `#${heading.id}`)
|
|
75
|
+
setActiveId(heading.id)
|
|
76
|
+
}
|
|
77
|
+
}}
|
|
78
|
+
className={cn(
|
|
79
|
+
'block py-1 transition-colors hover:text-foreground',
|
|
80
|
+
activeId === heading.id
|
|
81
|
+
? 'text-primary font-medium'
|
|
82
|
+
: 'text-muted-foreground'
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
{heading.title}
|
|
86
|
+
</a>
|
|
87
|
+
</li>
|
|
88
|
+
))}
|
|
89
|
+
</ul>
|
|
90
|
+
</nav>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract headings from rendered content
|
|
96
|
+
* Call this on the client after MDX renders
|
|
97
|
+
*/
|
|
98
|
+
export function extractHeadingsFromDOM(): TocItem[] {
|
|
99
|
+
const headings: TocItem[] = []
|
|
100
|
+
const elements = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
|
101
|
+
|
|
102
|
+
elements.forEach((el) => {
|
|
103
|
+
const id = el.id
|
|
104
|
+
const title = el.textContent || ''
|
|
105
|
+
const level = parseInt(el.tagName[1], 10)
|
|
106
|
+
|
|
107
|
+
if (id && title) {
|
|
108
|
+
headings.push({ id, title, level })
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return headings
|
|
113
|
+
}
|