@brainfish-ai/devdoc 0.1.21 → 0.1.23
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/package.json +1 -1
- package/renderer/app/api/collections/route.ts +2 -16
- package/renderer/app/api/docs/route.ts +10 -0
- package/renderer/app/api/schema/route.ts +11 -3
- package/renderer/app/globals.css +88 -0
- package/renderer/components/docs/mdx/index.ts +33 -0
- package/renderer/components/docs/mdx/landing.tsx +684 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +81 -14
- package/renderer/components/docs-viewer/index.tsx +203 -59
- package/renderer/components/docs-viewer/sidebar/index.tsx +3 -95
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +2 -16
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +2 -16
- package/renderer/lib/api-docs/factories.ts +45 -26
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +1 -1
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +1 -0
- package/renderer/lib/docs/config/schema.ts +11 -1
- package/renderer/lib/docs/mdx/frontmatter.ts +11 -0
- package/renderer/lib/utils/icons.ts +48 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +0 -21
|
@@ -15,7 +15,6 @@ import { CollectionTree } from './collection-tree'
|
|
|
15
15
|
import { SidebarSection } from './sidebar-section'
|
|
16
16
|
import { SidebarItem, SlidingIndicatorProvider } from './sidebar-item'
|
|
17
17
|
import { SidebarGroup } from './sidebar-group'
|
|
18
|
-
import { extractMarkdownHeadings, type MarkdownHeading } from '@/lib/api-docs/utils'
|
|
19
18
|
import { useMobile } from '@/lib/api-docs/mobile-context'
|
|
20
19
|
import { cn } from '@/lib/utils'
|
|
21
20
|
|
|
@@ -84,92 +83,8 @@ export function DocsSidebar({
|
|
|
84
83
|
}
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
// Extract documentation headings from collection description
|
|
88
|
-
const docHeadings = useMemo(() => {
|
|
89
|
-
return collection.description
|
|
90
|
-
? extractMarkdownHeadings(collection.description)
|
|
91
|
-
: []
|
|
92
|
-
}, [collection.description])
|
|
93
|
-
|
|
94
86
|
// Get doc groups from collection
|
|
95
87
|
const docGroups = collection.docGroups || []
|
|
96
|
-
|
|
97
|
-
const handleDocClick = (headingId: string) => {
|
|
98
|
-
handleDocClickWithClose(headingId)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Determine if Introduction should be highlighted
|
|
102
|
-
const isIntroductionSelected = !selectedRequest && !selectedDocSection && !selectedDocPage
|
|
103
|
-
|
|
104
|
-
// Check if a heading or any of its children is selected
|
|
105
|
-
const isHeadingSelected = (heading: MarkdownHeading): boolean => {
|
|
106
|
-
if (selectedDocSection === heading.id) return true
|
|
107
|
-
if (heading.children?.some(child => selectedDocSection === child.id)) return true
|
|
108
|
-
return false
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Get the selected child heading ID for a heading group
|
|
112
|
-
const getSelectedChildHeading = (heading: MarkdownHeading): string | null => {
|
|
113
|
-
if (selectedDocSection === heading.id) return heading.id
|
|
114
|
-
const selected = heading.children?.find(child => selectedDocSection === child.id)
|
|
115
|
-
return selected?.id || null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Render a documentation heading item (may have children)
|
|
119
|
-
const renderHeading = (heading: MarkdownHeading, index: number) => {
|
|
120
|
-
const hasChildren = heading.children && heading.children.length > 0
|
|
121
|
-
const isSelected = selectedDocSection === heading.id
|
|
122
|
-
|
|
123
|
-
// If heading is "Introduction", handle it specially
|
|
124
|
-
if (heading.text === 'Introduction') {
|
|
125
|
-
return (
|
|
126
|
-
<SidebarItem
|
|
127
|
-
key={`${heading.id}-${index}`}
|
|
128
|
-
itemId={`heading-introduction`}
|
|
129
|
-
selected={isIntroductionSelected}
|
|
130
|
-
onClick={() => handleDocClick('introduction')}
|
|
131
|
-
>
|
|
132
|
-
Introduction
|
|
133
|
-
</SidebarItem>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (hasChildren) {
|
|
138
|
-
return (
|
|
139
|
-
<SidebarGroup
|
|
140
|
-
key={`${heading.id}-${index}`}
|
|
141
|
-
title={heading.text}
|
|
142
|
-
defaultOpen={isHeadingSelected(heading)}
|
|
143
|
-
selected={isSelected}
|
|
144
|
-
selectedChildSlug={getSelectedChildHeading(heading)}
|
|
145
|
-
onClick={() => handleDocClick(heading.id)}
|
|
146
|
-
>
|
|
147
|
-
{heading.children!.map((child, childIndex) => (
|
|
148
|
-
<SidebarItem
|
|
149
|
-
key={`${child.id}-${childIndex}`}
|
|
150
|
-
itemId={`heading-${child.id}`}
|
|
151
|
-
selected={selectedDocSection === child.id}
|
|
152
|
-
indent={1}
|
|
153
|
-
onClick={() => handleDocClick(child.id)}
|
|
154
|
-
>
|
|
155
|
-
{child.text}
|
|
156
|
-
</SidebarItem>
|
|
157
|
-
))}
|
|
158
|
-
</SidebarGroup>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return (
|
|
163
|
-
<SidebarItem
|
|
164
|
-
key={`${heading.id}-${index}`}
|
|
165
|
-
itemId={`heading-${heading.id}`}
|
|
166
|
-
selected={isSelected}
|
|
167
|
-
onClick={() => handleDocClick(heading.id)}
|
|
168
|
-
>
|
|
169
|
-
{heading.text}
|
|
170
|
-
</SidebarItem>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
88
|
|
|
174
89
|
// Check if any child in a group is selected
|
|
175
90
|
const isChildSelected = (page: BrainfishDocPage): boolean => {
|
|
@@ -300,14 +215,7 @@ export function DocsSidebar({
|
|
|
300
215
|
|
|
301
216
|
{/* Scrollable content with sliding indicator */}
|
|
302
217
|
<SlidingIndicatorProvider className="docs-sidebar-content flex-1 overflow-y-auto overflow-x-hidden custom-scroll">
|
|
303
|
-
{/* Documentation Section (from
|
|
304
|
-
{activeTab === 'api-reference' && docHeadings.length > 0 && (
|
|
305
|
-
<SidebarSection title="Documentation">
|
|
306
|
-
{docHeadings.map((heading, index) => renderHeading(heading, index))}
|
|
307
|
-
</SidebarSection>
|
|
308
|
-
)}
|
|
309
|
-
|
|
310
|
-
{/* Documentation Pages Section (from docs.json) */}
|
|
218
|
+
{/* Documentation Pages Section (from docs.json groups) */}
|
|
311
219
|
{docGroups.length > 0 && (
|
|
312
220
|
<>
|
|
313
221
|
{docGroups.map((group, index) => (
|
|
@@ -315,7 +223,7 @@ export function DocsSidebar({
|
|
|
315
223
|
key={group.id}
|
|
316
224
|
title={group.title}
|
|
317
225
|
icon={group.icon}
|
|
318
|
-
className={index > 0
|
|
226
|
+
className={index > 0 ? '' : ''}
|
|
319
227
|
>
|
|
320
228
|
{group.pages.map(renderDocPage)}
|
|
321
229
|
</SidebarSection>
|
|
@@ -327,7 +235,7 @@ export function DocsSidebar({
|
|
|
327
235
|
{(collection.folders.length > 0 || collection.requests.length > 0) && (
|
|
328
236
|
<SidebarSection
|
|
329
237
|
title="Endpoints"
|
|
330
|
-
className={
|
|
238
|
+
className={docGroups.length > 0 ? 'border-t border-sidebar-border' : ''}
|
|
331
239
|
>
|
|
332
240
|
<CollectionTree
|
|
333
241
|
collection={collection}
|
|
@@ -2,21 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ReactNode, useRef, useEffect, useLayoutEffect, useState, createContext, useContext } from 'react'
|
|
4
4
|
import { cn } from '@/lib/utils'
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
// Helper to get Phosphor icon component from name
|
|
8
|
-
function getIcon(iconName?: string): React.ComponentType<{ className?: string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }> | null {
|
|
9
|
-
if (!iconName) return null
|
|
10
|
-
|
|
11
|
-
// Convert kebab-case to PascalCase for Phosphor icons
|
|
12
|
-
const pascalCase = iconName
|
|
13
|
-
.split('-')
|
|
14
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
15
|
-
.join('')
|
|
16
|
-
|
|
17
|
-
const IconComponent = (PhosphorIcons as Record<string, unknown>)[pascalCase] as React.ComponentType<{ className?: string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }> | undefined
|
|
18
|
-
return IconComponent || null
|
|
19
|
-
}
|
|
5
|
+
import { getPhosphorIcon } from '@/lib/utils/icons'
|
|
20
6
|
|
|
21
7
|
// Context for the sliding indicator
|
|
22
8
|
interface SlidingIndicatorContextType {
|
|
@@ -152,7 +138,7 @@ export function SidebarItem({
|
|
|
152
138
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
153
139
|
const context = useContext(SlidingIndicatorContext)
|
|
154
140
|
const id = itemId || String(children)
|
|
155
|
-
const IconComponent =
|
|
141
|
+
const IconComponent = getPhosphorIcon(icon)
|
|
156
142
|
|
|
157
143
|
// Register this item with the sliding indicator provider
|
|
158
144
|
useEffect(() => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ReactNode } from 'react'
|
|
4
4
|
import { cn } from '@/lib/utils'
|
|
5
|
-
import
|
|
5
|
+
import { getPhosphorIcon } from '@/lib/utils/icons'
|
|
6
6
|
|
|
7
7
|
interface SidebarSectionProps {
|
|
8
8
|
title: string
|
|
@@ -12,25 +12,11 @@ interface SidebarSectionProps {
|
|
|
12
12
|
icon?: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// Map of common icon names to Phosphor components
|
|
16
|
-
function getIcon(iconName?: string): React.ComponentType<{ className?: string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }> | null {
|
|
17
|
-
if (!iconName) return null
|
|
18
|
-
|
|
19
|
-
// Convert kebab-case to PascalCase for Phosphor icons
|
|
20
|
-
const pascalCase = iconName
|
|
21
|
-
.split('-')
|
|
22
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
23
|
-
.join('')
|
|
24
|
-
|
|
25
|
-
const IconComponent = (PhosphorIcons as Record<string, unknown>)[pascalCase] as React.ComponentType<{ className?: string; weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone" }> | undefined
|
|
26
|
-
return IconComponent || null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
15
|
/**
|
|
30
16
|
* Sidebar Section component - groups items under a title with optional icon
|
|
31
17
|
*/
|
|
32
18
|
export function SidebarSection({ title, children, className = '', icon }: SidebarSectionProps) {
|
|
33
|
-
const IconComponent =
|
|
19
|
+
const IconComponent = getPhosphorIcon(icon)
|
|
34
20
|
|
|
35
21
|
return (
|
|
36
22
|
<div className={cn('docs-sidebar-section flex flex-col', className)}>
|
|
@@ -11,39 +11,61 @@ import type {
|
|
|
11
11
|
} from './types'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Generates a
|
|
15
|
-
*
|
|
14
|
+
* Generates a URL-friendly slug from a string
|
|
15
|
+
* Converts to lowercase, replaces spaces and special chars with hyphens
|
|
16
16
|
*/
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
function slugify(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[{}]/g, '') // Remove braces from path params
|
|
21
|
+
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
|
22
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
23
|
+
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates a human-readable, deterministic ID for requests
|
|
28
|
+
* Uses operationId if available, otherwise method + path
|
|
29
|
+
*/
|
|
30
|
+
function generateRequestId(method: string, path: string, operationId?: string): string {
|
|
31
|
+
if (operationId) {
|
|
32
|
+
// Use operationId directly (it's already unique per spec)
|
|
33
|
+
return slugify(operationId)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Generate from method + path: "get-users-id" for "GET /users/{id}"
|
|
37
|
+
const pathSlug = slugify(path)
|
|
38
|
+
return `${method.toLowerCase()}-${pathSlug}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generates a unique ID for collections
|
|
43
|
+
* If a seed is provided, generates a deterministic slug-based ID
|
|
44
|
+
*/
|
|
45
|
+
function generateCollectionId(name?: string): string {
|
|
46
|
+
if (name) {
|
|
47
|
+
return `coll-${slugify(name)}`
|
|
29
48
|
}
|
|
30
|
-
return
|
|
49
|
+
return `coll_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
/**
|
|
34
53
|
* Creates a BrainfishRESTRequest with defaults
|
|
35
|
-
*
|
|
54
|
+
* Generates human-readable, deterministic IDs based on operationId or method+path
|
|
36
55
|
*/
|
|
37
56
|
export function makeBrainfishRESTRequest(
|
|
38
|
-
request: Omit<BrainfishRESTRequest, 'id'>
|
|
57
|
+
request: Omit<BrainfishRESTRequest, 'id'> & { operationId?: string }
|
|
39
58
|
): BrainfishRESTRequest {
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
?
|
|
43
|
-
:
|
|
59
|
+
// Extract path from endpoint (remove base URL)
|
|
60
|
+
const path = request.endpoint
|
|
61
|
+
? request.endpoint.replace(/^https?:\/\/[^/]+/, '') || '/'
|
|
62
|
+
: '/'
|
|
63
|
+
|
|
64
|
+
// Generate human-readable ID
|
|
65
|
+
const id = generateRequestId(request.method || 'GET', path, request.operationId)
|
|
44
66
|
|
|
45
67
|
return {
|
|
46
|
-
id
|
|
68
|
+
id,
|
|
47
69
|
name: request.name || 'Untitled Request',
|
|
48
70
|
description: request.description ?? null,
|
|
49
71
|
method: request.method || 'GET',
|
|
@@ -65,11 +87,8 @@ export function makeBrainfishRESTRequest(
|
|
|
65
87
|
export function makeBrainfishCollection(
|
|
66
88
|
collection: Omit<BrainfishCollection, 'id'>
|
|
67
89
|
): BrainfishCollection {
|
|
68
|
-
// Generate deterministic ID from collection name
|
|
69
|
-
const idSeed = collection.name ? `collection:${collection.name}` : undefined
|
|
70
|
-
|
|
71
90
|
return {
|
|
72
|
-
id:
|
|
91
|
+
id: generateCollectionId(collection.name),
|
|
73
92
|
name: collection.name || 'Untitled Collection',
|
|
74
93
|
description: collection.description ?? null,
|
|
75
94
|
folders: collection.folders || [],
|
|
@@ -238,7 +238,7 @@ function extractOperations(
|
|
|
238
238
|
const fields = rootType.getFields()
|
|
239
239
|
|
|
240
240
|
return Object.values(fields).map(field => ({
|
|
241
|
-
id: `${operationType}-${field.name}
|
|
241
|
+
id: `${operationType}-${field.name}`.toLowerCase(),
|
|
242
242
|
name: field.name,
|
|
243
243
|
description: field.description || null,
|
|
244
244
|
operationType,
|
|
@@ -58,6 +58,16 @@ const openapiTabSchema = z.object({
|
|
|
58
58
|
path: z.string().optional(),
|
|
59
59
|
versions: z.array(openapiVersionSchema).optional(),
|
|
60
60
|
spec: z.string().optional(),
|
|
61
|
+
groups: z.array(groupSchema).optional(), // Doc groups alongside API reference
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const graphqlTabSchema = z.object({
|
|
65
|
+
tab: z.string(),
|
|
66
|
+
type: z.literal('graphql'),
|
|
67
|
+
path: z.string().optional(),
|
|
68
|
+
schema: z.string(), // GraphQL schema file path
|
|
69
|
+
endpoint: z.string().optional(), // GraphQL endpoint URL
|
|
70
|
+
groups: z.array(groupSchema).optional(), // Doc groups alongside API reference
|
|
61
71
|
})
|
|
62
72
|
|
|
63
73
|
const changelogTabSchema = z.object({
|
|
@@ -66,7 +76,7 @@ const changelogTabSchema = z.object({
|
|
|
66
76
|
path: z.string().optional(),
|
|
67
77
|
})
|
|
68
78
|
|
|
69
|
-
const tabSchema = z.union([docsTabSchema, openapiTabSchema, changelogTabSchema])
|
|
79
|
+
const tabSchema = z.union([docsTabSchema, openapiTabSchema, graphqlTabSchema, changelogTabSchema])
|
|
70
80
|
|
|
71
81
|
const navigationSchema = z.object({
|
|
72
82
|
tabs: z.array(tabSchema).optional(),
|
|
@@ -17,8 +17,19 @@ export const frontmatterSchema = z.object({
|
|
|
17
17
|
icon: z.string().optional(),
|
|
18
18
|
|
|
19
19
|
// Page behavior
|
|
20
|
+
// - 'default': Standard documentation page with max-width container
|
|
21
|
+
// - 'wide': Wide container for content that needs more space
|
|
22
|
+
// - 'custom': Full-width custom layout (no container, no header) - for landing pages
|
|
20
23
|
mode: z.enum(['default', 'wide', 'custom']).optional(),
|
|
21
24
|
|
|
25
|
+
// Custom page options (only used when mode: 'custom')
|
|
26
|
+
// Hides the default page header (title & description)
|
|
27
|
+
hideHeader: z.boolean().optional(),
|
|
28
|
+
// Full-width layout without container constraints
|
|
29
|
+
fullWidth: z.boolean().optional(),
|
|
30
|
+
// Custom background color or gradient
|
|
31
|
+
background: z.string().optional(),
|
|
32
|
+
|
|
22
33
|
// OpenAPI integration
|
|
23
34
|
openapi: z.string().optional(),
|
|
24
35
|
api: z.string().optional(),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as PhosphorIcons from '@phosphor-icons/react'
|
|
2
|
+
|
|
3
|
+
export type PhosphorIconWeight = 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone'
|
|
4
|
+
|
|
5
|
+
export type PhosphorIconComponent = React.ComponentType<{
|
|
6
|
+
className?: string
|
|
7
|
+
weight?: PhosphorIconWeight
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get a Phosphor icon component by its kebab-case name
|
|
12
|
+
*
|
|
13
|
+
* @param iconName - The icon name in kebab-case (e.g., "arrow-right", "github-logo")
|
|
14
|
+
* @returns The Phosphor icon component or null if not found
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const Icon = getPhosphorIcon('arrow-right')
|
|
18
|
+
* if (Icon) {
|
|
19
|
+
* return <Icon className="h-5 w-5" weight="bold" />
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export function getPhosphorIcon(iconName?: string): PhosphorIconComponent | null {
|
|
23
|
+
if (!iconName) return null
|
|
24
|
+
|
|
25
|
+
// Convert kebab-case to PascalCase for Phosphor icons
|
|
26
|
+
const pascalCase = iconName
|
|
27
|
+
.split('-')
|
|
28
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
29
|
+
.join('')
|
|
30
|
+
|
|
31
|
+
const IconComponent = (PhosphorIcons as Record<string, unknown>)[pascalCase]
|
|
32
|
+
|
|
33
|
+
if (typeof IconComponent === 'function') {
|
|
34
|
+
return IconComponent as PhosphorIconComponent
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a Phosphor icon exists by name
|
|
42
|
+
*
|
|
43
|
+
* @param iconName - The icon name in kebab-case
|
|
44
|
+
* @returns true if the icon exists
|
|
45
|
+
*/
|
|
46
|
+
export function hasPhosphorIcon(iconName?: string): boolean {
|
|
47
|
+
return getPhosphorIcon(iconName) !== null
|
|
48
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { MarkdownRenderer } from '../shared/markdown-renderer'
|
|
4
|
-
import type { BrainfishCollection } from '@/lib/api-docs/types'
|
|
5
|
-
|
|
6
|
-
interface IntroductionProps {
|
|
7
|
-
collection: BrainfishCollection
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function Introduction({ collection }: IntroductionProps) {
|
|
11
|
-
return (
|
|
12
|
-
<div className="docs-introduction docs-content max-w-4xl mx-auto px-4 py-6 sm:px-8 sm:py-8">
|
|
13
|
-
<h1 className="docs-content-title text-2xl sm:text-3xl font-bold mb-4 sm:mb-6 text-foreground">{collection.name}</h1>
|
|
14
|
-
{collection.description && (
|
|
15
|
-
<div className="docs-prose prose prose-sm max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-strong:text-foreground prose-code:text-foreground prose-pre:bg-muted prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-pre:overflow-x-auto prose-table:w-full prose-th:text-left prose-th:p-3 prose-th:bg-muted prose-td:p-3 prose-td:border-b prose-td:border-border">
|
|
16
|
-
<MarkdownRenderer content={collection.description} />
|
|
17
|
-
</div>
|
|
18
|
-
)}
|
|
19
|
-
</div>
|
|
20
|
-
)
|
|
21
|
-
}
|