Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions examples/basic/chronicle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ theme:
search:
enabled: true
placeholder: Search documentation...
footer:
copyright: "© 2024 Chronicle. All rights reserved."
links:
- label: GitHub
href: https://github.com/raystack/chronicle
- label: Documentation
href: /
4 changes: 4 additions & 0 deletions packages/chronicle/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"devDependencies": {
"@biomejs/biome": "^2.3.13",
"@raystack/tools-config": "0.56.0",
"@types/lodash": "^4.17.23",
"@types/mdx": "^2.0.13",
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
Expand All @@ -21,13 +22,16 @@
"typescript": "5.9.3"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@raystack/apsara": "^0.56.0",
"chalk": "^5.6.2",
"commander": "^14.0.2",
"fumadocs-core": "^16.4.9",
"fumadocs-mdx": "^14.2.6",
"lodash": "^4.17.23",
"next": "16.1.6",
"react": "^19.0.0",
"react-device-detect": "^2.2.3",
"react-dom": "^19.0.0",
"remark-attr": "^0.11.1",
"yaml": "^2.8.2",
Expand Down
1 change: 1 addition & 0 deletions packages/chronicle/src/app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default async function DocsPage({ params }: PageProps) {
toc: data.toc ?? [],
}}
config={config}
tree={tree}
/>
</Layout>
)
Expand Down
40 changes: 40 additions & 0 deletions packages/chronicle/src/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { source } from '../../../lib/source'
import { createSearchAPI, type AdvancedIndex } from 'fumadocs-core/search/server'
import type { StructuredData } from 'fumadocs-core/mdx-plugins'

interface PageData {
title?: string
description?: string
structuredData?: StructuredData
load?: () => Promise<{ structuredData?: StructuredData }>
}

export const { GET } = createSearchAPI('advanced', {
indexes: async (): Promise<AdvancedIndex[]> => {
const pages = source.getPages()
const indexes = await Promise.all(
pages.map(async (page): Promise<AdvancedIndex> => {
const data = page.data as PageData
let structuredData: StructuredData | undefined = data.structuredData

if (!structuredData && data.load) {
try {
const loaded = await data.load()
structuredData = loaded.structuredData
} catch (error) {
console.error(`Failed to load structured data for ${page.url}:`, error)
}
}

return {
id: page.url,
url: page.url,
title: data.title ?? '',
description: data.description ?? '',
structuredData: structuredData ?? { headings: [], contents: [] },
}
})
)
return indexes
},
})
2 changes: 1 addition & 1 deletion packages/chronicle/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function RootLayout({
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<body suppressHydrationWarning>
<Providers>{children}</Providers>
</body>
</html>
Expand Down
72 changes: 72 additions & 0 deletions packages/chronicle/src/components/ui/breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client'

import { Breadcrumb } from '@raystack/apsara'
import type { PageTree, PageTreeItem } from '../../types'

interface BreadcrumbsProps {
slug: string[]
tree: PageTree
}

function findInTree(items: PageTreeItem[], targetPath: string): PageTreeItem | undefined {
for (const item of items) {
const itemUrl = item.url || `/${item.name.toLowerCase().replace(/\s+/g, '-')}`
if (itemUrl === targetPath || itemUrl === `/${targetPath}`) {
return item
}
if (item.children) {
const found = findInTree(item.children, targetPath)
if (found) return found
}
}
return undefined
}

function getFirstPageUrl(item: PageTreeItem): string | undefined {
if (item.type === 'page' && item.url) {
return item.url
}
if (item.children) {
for (const child of item.children) {
const url = getFirstPageUrl(child)
if (url) return url
}
}
return undefined
}

export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) {
const items: { label: string; href: string }[] = []

for (let i = 0; i < slug.length; i++) {
const currentPath = `/${slug.slice(0, i + 1).join('/')}`
const node = findInTree(tree.children, currentPath)
const href = node?.url || (node && getFirstPageUrl(node)) || currentPath
const label = node?.name ?? slug[i]
items.push({
label: label.charAt(0).toUpperCase() + label.slice(1),
href,
})
}

return (
<Breadcrumb size="small">
{items.flatMap((item, index) => {
const breadcrumbItem = (
<Breadcrumb.Item
key={`item-${index}`}
href={item.href}
current={index === items.length - 1}
>
{item.label}
</Breadcrumb.Item>
)
if (index === 0) return [breadcrumbItem]
return [
<Breadcrumb.Separator key={`sep-${index}`} style={{ display: 'flex' }} />,
breadcrumbItem,
]
})}
</Breadcrumb>
)
}
27 changes: 27 additions & 0 deletions packages/chronicle/src/components/ui/footer.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.footer {
border-top: 1px solid var(--rs-color-border-base-primary);
padding: var(--rs-space-5) var(--rs-space-7);
}

.container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
}

.copyright {
color: var(--rs-color-foreground-base-secondary);
}

.links {
flex-wrap: wrap;
}

.link {
color: var(--rs-color-foreground-base-secondary);
font-size: 14px;
}

.link:hover {
color: var(--rs-color-foreground-base-primary);
}
30 changes: 30 additions & 0 deletions packages/chronicle/src/components/ui/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Flex, Link, Text } from "@raystack/apsara";
import type { FooterConfig } from "../../types";
import styles from "./footer.module.css";

interface FooterProps {
config?: FooterConfig;
}

export function Footer({ config }: FooterProps) {
return (
<footer className={styles.footer}>
<Flex align="center" justify="between" className={styles.container}>
{config?.copyright && (
<Text size={2} className={styles.copyright}>
{config.copyright}
</Text>
)}
{config?.links && config.links.length > 0 && (
<Flex gap="medium" className={styles.links}>
{config.links.map((link) => (
<Link key={link.href} href={link.href} className={styles.link}>
{link.label}
</Link>
))}
</Flex>
)}
</Flex>
</footer>
);
}
111 changes: 111 additions & 0 deletions packages/chronicle/src/components/ui/search.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
.trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 6px;
border: 1px solid var(--rs-color-border-base-primary);
background: transparent;
color: var(--rs-color-foreground-base-secondary);
font-size: 14px;
cursor: pointer;
}

.kbd {
padding: 2px 6px;
border-radius: 4px;
border: 1px solid var(--rs-color-border-base-primary);
font-size: 12px;
}

.dialogContent {
max-width: 600px;
padding: 0;
min-height: 0;
position: fixed;
top: 20%;
left: 50%;
transform: translateX(-50%);
border-radius: var(--rs-radius-4);
}

.input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 16px;
color: var(--rs-color-foreground-base-primary);
}

.list {
max-height: 300px;
overflow: auto;
padding: 0;
}

.visuallyHidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.item {
padding: 12px 16px;
cursor: pointer;
border-radius: 6px;
}

.item[data-selected="true"] {
background: var(--rs-color-background-base-secondary);
color: var(--rs-color-foreground-accent-primary-hover);
}

.itemContent {
display: flex;
align-items: center;
gap: 12px;
}

.resultText {
display: flex;
align-items: center;
gap: 8px;
}

.headingText {
color: var(--rs-color-foreground-base-secondary);
}

.item[data-selected="true"] .headingText {
color: var(--rs-color-foreground-accent-primary-hover);
}

.separator {
color: var(--rs-color-foreground-base-secondary);
}

.pageText {
color: var(--rs-color-foreground-base-primary);
}

.item[data-selected="true"] .pageText {
color: var(--rs-color-foreground-accent-primary-hover);
}

.icon {
width: 18px;
height: 18px;
color: var(--rs-color-foreground-base-secondary);
flex-shrink: 0;
}

.item[data-selected="true"] .icon {
color: var(--rs-color-foreground-accent-primary-hover);
}
Loading