From 1a079ae683aa0220ca69980b83c6a9f15ad0821b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 09:56:26 +0530 Subject: [PATCH 01/10] feat: add breadcumbs --- .../chronicle/src/app/[[...slug]]/page.tsx | 1 + .../src/components/ui/breadcrumbs.tsx | 53 +++++++++++++++++++ .../chronicle/src/themes/default/Page.tsx | 4 +- packages/chronicle/src/types/theme.ts | 1 + 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/chronicle/src/components/ui/breadcrumbs.tsx diff --git a/packages/chronicle/src/app/[[...slug]]/page.tsx b/packages/chronicle/src/app/[[...slug]]/page.tsx index 0591f12..8231f34 100644 --- a/packages/chronicle/src/app/[[...slug]]/page.tsx +++ b/packages/chronicle/src/app/[[...slug]]/page.tsx @@ -46,6 +46,7 @@ export default async function DocsPage({ params }: PageProps) { toc: data.toc ?? [], }} config={config} + tree={tree} /> ) diff --git a/packages/chronicle/src/components/ui/breadcrumbs.tsx b/packages/chronicle/src/components/ui/breadcrumbs.tsx new file mode 100644 index 0000000..7ac1394 --- /dev/null +++ b/packages/chronicle/src/components/ui/breadcrumbs.tsx @@ -0,0 +1,53 @@ +'use client' + +import { Breadcrumb } from '@raystack/apsara' +import type { PageTree, PageTreeItem } from '../../types' + +interface BreadcrumbsProps { + slug: string[] + tree: PageTree +} + +function findInTree(items: PageTreeItem[], segment: string): PageTreeItem | undefined { + for (const item of items) { + const itemSlug = item.url?.split('/').pop() + if (itemSlug === segment) { + return item + } + if (item.children) { + const found = findInTree(item.children, segment) + if (found) return found + } + } + return undefined +} + +export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) { + const items: { label: string; href: string }[] = [] + + let currentPath = '/docs' + for (const segment of slug) { + currentPath = `${currentPath}/${segment}` + const node = findInTree(tree.children, segment) + items.push({ + label: node?.name ?? segment, + href: currentPath, + }) + } + + return ( + + Docs + {items.flatMap((item, index) => [ + , + + {item.label} + , + ])} + + ) +} diff --git a/packages/chronicle/src/themes/default/Page.tsx b/packages/chronicle/src/themes/default/Page.tsx index 1624868..a7d9ecf 100644 --- a/packages/chronicle/src/themes/default/Page.tsx +++ b/packages/chronicle/src/themes/default/Page.tsx @@ -2,13 +2,15 @@ import { Flex } from '@raystack/apsara' import type { ThemePageProps } from '../../types' +import { Breadcrumbs } from '../../components/ui/breadcrumbs' import { Toc } from './Toc' import styles from './Page.module.css' -export function Page({ page }: ThemePageProps) { +export function Page({ page, tree }: ThemePageProps) { return (
+
{page.content}
diff --git a/packages/chronicle/src/types/theme.ts b/packages/chronicle/src/types/theme.ts index aefac8d..18a840f 100644 --- a/packages/chronicle/src/types/theme.ts +++ b/packages/chronicle/src/types/theme.ts @@ -11,6 +11,7 @@ export interface ThemeLayoutProps { export interface ThemePageProps { page: Page config: ChronicleConfig + tree: PageTree } export interface Theme { From 5cd178aae58f852458e14f3700b2d5ba02b581c2 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 13:33:56 +0530 Subject: [PATCH 02/10] feat: add search api --- .../chronicle/src/app/api/search/route.ts | 50 +++++++++++++++++++ .../chronicle/src/themes/default/Layout.tsx | 7 +-- 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 packages/chronicle/src/app/api/search/route.ts diff --git a/packages/chronicle/src/app/api/search/route.ts b/packages/chronicle/src/app/api/search/route.ts new file mode 100644 index 0000000..b29eb6f --- /dev/null +++ b/packages/chronicle/src/app/api/search/route.ts @@ -0,0 +1,50 @@ +import { source } from '../../../lib/source' +import { createSearchAPI } from 'fumadocs-core/search/server' + +interface StructuredDataHeading { + id: string + content: string +} + +interface StructuredDataContent { + heading?: string + content: string +} + +interface StructuredData { + headings?: StructuredDataHeading[] + contents?: StructuredDataContent[] +} + +interface PageData { + title?: string + description?: string + structuredData?: StructuredData + load?: () => Promise<{ structuredData?: StructuredData }> +} + +export const { GET } = createSearchAPI('advanced', { + indexes: async () => { + const pages = source.getPages() + const indexes = await Promise.all( + pages.map(async (page) => { + const data = page.data as PageData + let structuredData = data.structuredData + + if (!structuredData && data.load) { + const loaded = await data.load() + structuredData = loaded.structuredData + } + + return { + id: page.url, + url: page.url, + title: data.title ?? '', + description: data.description ?? '', + structuredData: structuredData ?? { headings: [], contents: [] }, + } + }) + ) + return indexes + }, +}) diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 643edaf..1d5445b 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -3,6 +3,7 @@ import { usePathname } from 'next/navigation' import { Flex, Navbar, Headline, Link, Sidebar, Text } from '@raystack/apsara' import { ClientThemeSwitcher } from '../../components/ui/client-theme-switcher' +import { Search } from '../../components/ui/search' import type { ThemeLayoutProps, PageTreeItem } from '../../types' import styles from './Layout.module.css' @@ -25,11 +26,7 @@ export function Layout({ children, config, tree }: ThemeLayoutProps) { ))} - {config.search?.enabled && ( -
- {/* Search component will be added later */} -
- )} + {config.search?.enabled && } From bf2048f35cd6f6c687a683f23b9bfe659acbb016 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 13:47:55 +0530 Subject: [PATCH 03/10] feat: add search component --- .../src/components/ui/search.module.css | 111 +++++++++++++++ .../chronicle/src/components/ui/search.tsx | 126 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 packages/chronicle/src/components/ui/search.module.css create mode 100644 packages/chronicle/src/components/ui/search.tsx diff --git a/packages/chronicle/src/components/ui/search.module.css b/packages/chronicle/src/components/ui/search.module.css new file mode 100644 index 0000000..b80c60f --- /dev/null +++ b/packages/chronicle/src/components/ui/search.module.css @@ -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); +} diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx new file mode 100644 index 0000000..bb21c07 --- /dev/null +++ b/packages/chronicle/src/components/ui/search.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useRouter } from "next/navigation"; +import { Button, Command, Dialog, Text } from "@raystack/apsara"; +import { useDocsSearch } from "fumadocs-core/search/client"; +import type { SortedResult } from "fumadocs-core/search"; +import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline"; +import styles from "./search.module.css"; + +export function Search() { + const [open, setOpen] = useState(false); + const router = useRouter(); + + const { search, setSearch, query } = useDocsSearch({ + type: "fetch", + api: "/api/search", + delayMs: 100, + allowEmpty: true, + }); + + const onSelect = useCallback( + (url: string) => { + setOpen(false); + router.push(url); + }, + [router], + ); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; + + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + const results = query.data === "empty" ? [] : (query.data ?? []); + + return ( + <> + + + + + + Search documentation + + + + + + {query.isLoading && Loading...} + {!query.isLoading && search.length > 0 && results.length === 0 && ( + No results found. + )} + {!query.isLoading && search.length === 0 && results.length > 0 && ( + + {results.slice(0, 8).map((result: SortedResult) => ( + onSelect(result.url)} + className={styles.item} + > +
+ + {result.content} +
+
+ ))} +
+ )} + {search.length > 0 && + results.map((result: SortedResult) => ( + onSelect(result.url)} + className={styles.item} + > +
+ {result.type === "page" ? : } +
+ {result.type === "heading" ? ( + <> + {result.content} + - + {getPageTitle(result.url)} + + ) : ( + {result.content} + )} +
+
+
+ ))} +
+
+
+
+ + ); +} + +function getPageTitle(url: string): string { + const path = url.split("#")[0]; + const segments = path.split("/").filter(Boolean); + const lastSegment = segments[segments.length - 1] || ""; + return lastSegment + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + From 26414fd22f25560c9ac5a57c1ffbb2c450ae7f52 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 13:48:29 +0530 Subject: [PATCH 04/10] fix: breadcrumb folder link --- packages/chronicle/src/app/layout.tsx | 2 +- .../src/components/ui/breadcrumbs.tsx | 50 +++++++++++++------ 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/chronicle/src/app/layout.tsx b/packages/chronicle/src/app/layout.tsx index 86b02a1..40f38eb 100644 --- a/packages/chronicle/src/app/layout.tsx +++ b/packages/chronicle/src/app/layout.tsx @@ -17,7 +17,7 @@ export default function RootLayout({ }) { return ( - + {children} diff --git a/packages/chronicle/src/components/ui/breadcrumbs.tsx b/packages/chronicle/src/components/ui/breadcrumbs.tsx index 7ac1394..870d98d 100644 --- a/packages/chronicle/src/components/ui/breadcrumbs.tsx +++ b/packages/chronicle/src/components/ui/breadcrumbs.tsx @@ -10,7 +10,7 @@ interface BreadcrumbsProps { function findInTree(items: PageTreeItem[], segment: string): PageTreeItem | undefined { for (const item of items) { - const itemSlug = item.url?.split('/').pop() + const itemSlug = item.url?.split('/').pop() || item.name.toLowerCase().replace(/\s+/g, '-') if (itemSlug === segment) { return item } @@ -22,32 +22,50 @@ function findInTree(items: PageTreeItem[], segment: string): PageTreeItem | unde 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 }[] = [] - let currentPath = '/docs' for (const segment of slug) { - currentPath = `${currentPath}/${segment}` const node = findInTree(tree.children, segment) + const href = node?.url || (node && getFirstPageUrl(node)) || `/${slug.slice(0, slug.indexOf(segment) + 1).join('/')}` + const label = node?.name ?? segment items.push({ - label: node?.name ?? segment, - href: currentPath, + label: label.charAt(0).toUpperCase() + label.slice(1), + href, }) } return ( - Docs - {items.flatMap((item, index) => [ - , - - {item.label} - , - ])} + {items.flatMap((item, index) => { + const breadcrumbItem = ( + + {item.label} + + ) + if (index === 0) return [breadcrumbItem] + return [ + , + breadcrumbItem, + ] + })} ) } From 7937c07eb629833f0b7dfea414587e9fd9820d6d Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 14:19:11 +0530 Subject: [PATCH 05/10] fix: pr comments --- packages/chronicle/src/app/api/search/route.ts | 8 ++++++-- .../chronicle/src/components/ui/breadcrumbs.tsx | 17 +++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/chronicle/src/app/api/search/route.ts b/packages/chronicle/src/app/api/search/route.ts index b29eb6f..784d844 100644 --- a/packages/chronicle/src/app/api/search/route.ts +++ b/packages/chronicle/src/app/api/search/route.ts @@ -32,8 +32,12 @@ export const { GET } = createSearchAPI('advanced', { let structuredData = data.structuredData if (!structuredData && data.load) { - const loaded = await data.load() - structuredData = loaded.structuredData + try { + const loaded = await data.load() + structuredData = loaded.structuredData + } catch (error) { + console.error(`Failed to load structured data for ${page.url}:`, error) + } } return { diff --git a/packages/chronicle/src/components/ui/breadcrumbs.tsx b/packages/chronicle/src/components/ui/breadcrumbs.tsx index 870d98d..fb4c0f2 100644 --- a/packages/chronicle/src/components/ui/breadcrumbs.tsx +++ b/packages/chronicle/src/components/ui/breadcrumbs.tsx @@ -8,14 +8,14 @@ interface BreadcrumbsProps { tree: PageTree } -function findInTree(items: PageTreeItem[], segment: string): PageTreeItem | undefined { +function findInTree(items: PageTreeItem[], targetPath: string): PageTreeItem | undefined { for (const item of items) { - const itemSlug = item.url?.split('/').pop() || item.name.toLowerCase().replace(/\s+/g, '-') - if (itemSlug === segment) { + 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, segment) + const found = findInTree(item.children, targetPath) if (found) return found } } @@ -38,10 +38,11 @@ function getFirstPageUrl(item: PageTreeItem): string | undefined { export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) { const items: { label: string; href: string }[] = [] - for (const segment of slug) { - const node = findInTree(tree.children, segment) - const href = node?.url || (node && getFirstPageUrl(node)) || `/${slug.slice(0, slug.indexOf(segment) + 1).join('/')}` - const label = node?.name ?? segment + 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, From aad8ab4fe7cb795b06e116c1e35973622ab18541 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 14:25:39 +0530 Subject: [PATCH 06/10] fix: pr comments --- packages/chronicle/package.json | 3 +++ .../chronicle/src/app/api/search/route.ts | 24 ++++--------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/chronicle/package.json b/packages/chronicle/package.json index 9ddc9cd..34c420f 100644 --- a/packages/chronicle/package.json +++ b/packages/chronicle/package.json @@ -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", @@ -21,11 +22,13 @@ "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-dom": "^19.0.0", diff --git a/packages/chronicle/src/app/api/search/route.ts b/packages/chronicle/src/app/api/search/route.ts index 784d844..b4e3a93 100644 --- a/packages/chronicle/src/app/api/search/route.ts +++ b/packages/chronicle/src/app/api/search/route.ts @@ -1,20 +1,6 @@ import { source } from '../../../lib/source' -import { createSearchAPI } from 'fumadocs-core/search/server' - -interface StructuredDataHeading { - id: string - content: string -} - -interface StructuredDataContent { - heading?: string - content: string -} - -interface StructuredData { - headings?: StructuredDataHeading[] - contents?: StructuredDataContent[] -} +import { createSearchAPI, type AdvancedIndex } from 'fumadocs-core/search/server' +import type { StructuredData } from 'fumadocs-core/mdx-plugins' interface PageData { title?: string @@ -24,12 +10,12 @@ interface PageData { } export const { GET } = createSearchAPI('advanced', { - indexes: async () => { + indexes: async (): Promise => { const pages = source.getPages() const indexes = await Promise.all( - pages.map(async (page) => { + pages.map(async (page): Promise => { const data = page.data as PageData - let structuredData = data.structuredData + let structuredData: StructuredData | undefined = data.structuredData if (!structuredData && data.load) { try { From c9b49a37c15543013441b791547f8df3e1f01178 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 14:27:43 +0530 Subject: [PATCH 07/10] fix: pr comments --- packages/chronicle/package.json | 1 + .../chronicle/src/components/ui/search.tsx | 6 ++- pnpm-lock.yaml | 49 +++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/chronicle/package.json b/packages/chronicle/package.json index 34c420f..17b7a35 100644 --- a/packages/chronicle/package.json +++ b/packages/chronicle/package.json @@ -31,6 +31,7 @@ "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", diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index bb21c07..a0a1bbd 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -6,6 +6,7 @@ import { Button, Command, Dialog, Text } from "@raystack/apsara"; import { useDocsSearch } from "fumadocs-core/search/client"; import type { SortedResult } from "fumadocs-core/search"; import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline"; +import { isMacOs } from "react-device-detect"; import styles from "./search.module.css"; export function Search() { @@ -43,7 +44,7 @@ export function Search() { return ( <> - @@ -117,7 +118,8 @@ export function Search() { function getPageTitle(url: string): string { const path = url.split("#")[0]; const segments = path.split("/").filter(Boolean); - const lastSegment = segments[segments.length - 1] || ""; + const lastSegment = segments[segments.length - 1]; + if (!lastSegment) return "Home"; return lastSegment .split("-") .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 908b0b3..e1f2289 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: packages/chronicle: dependencies: + '@heroicons/react': + specifier: ^2.2.0 + version: 2.2.0(react@19.2.4) '@raystack/apsara': specifier: ^0.56.0 version: 0.56.0(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -25,12 +28,18 @@ importers: fumadocs-mdx: specifier: ^14.2.6 version: 14.2.6(@types/react@19.2.10)(fumadocs-core@16.4.9(@types/react@19.2.10)(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4) + lodash: + specifier: ^4.17.23 + version: 4.17.23 next: specifier: 16.1.6 version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: ^19.0.0 version: 19.2.4 + react-device-detect: + specifier: ^2.2.3 + version: 2.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) @@ -50,6 +59,9 @@ importers: '@raystack/tools-config': specifier: 0.56.0 version: 0.56.0(@biomejs/biome@2.3.13) + '@types/lodash': + specifier: ^4.17.23 + version: 4.17.23 '@types/mdx': specifier: ^2.0.13 version: 2.0.13 @@ -322,6 +334,11 @@ packages: '@formatjs/intl-localematcher@0.8.0': resolution: {integrity: sha512-zgMYWdUlmEZpX2Io+v3LHrfq9xZ6khpQVf9UAw2xYWhGerGgI9XgH1HvL/A34jWiruUJpYlP5pk4g8nIcaDrXQ==} + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} + peerDependencies: + react: '>= 16 || ^19.0.0-rc' + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -1323,6 +1340,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/lodash@4.17.23': + resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1676,6 +1696,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -1929,6 +1952,12 @@ packages: peerDependencies: react: '>=16.8.0' + react-device-detect@2.2.3: + resolution: {integrity: sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==} + peerDependencies: + react: '>= 0.14.0' + react-dom: '>= 0.14.0' + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -2112,6 +2141,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2345,6 +2378,10 @@ snapshots: '@formatjs/fast-memoize': 3.1.0 tslib: 2.8.1 + '@heroicons/react@2.2.0(react@19.2.4)': + dependencies: + react: 19.2.4 + '@img/colour@1.0.0': optional: true @@ -3381,6 +3418,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/lodash@4.17.23': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -3761,6 +3800,8 @@ snapshots: dependencies: argparse: 2.0.1 + lodash@4.17.23: {} + longest-streak@3.1.0: {} markdown-extensions@2.0.0: {} @@ -4336,6 +4377,12 @@ snapshots: date-fns-jalali: 4.1.0-0 react: 19.2.4 + react-device-detect@2.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + ua-parser-js: 1.0.41 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -4585,6 +4632,8 @@ snapshots: typescript@5.9.3: {} + ua-parser-js@1.0.41: {} + undici-types@7.16.0: {} unified@11.0.5: From af4850528ff8f022601b60ae49bb0a060c2d1a20 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 14:34:58 +0530 Subject: [PATCH 08/10] fix: hydration warning --- .../chronicle/src/components/ui/search.tsx | 87 +++++++++++++------ 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index a0a1bbd..6775aa1 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -9,6 +9,20 @@ import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline"; import { isMacOs } from "react-device-detect"; import styles from "./search.module.css"; +function SearchShortcutKey({ className }: { className?: string }) { + const [key, setKey] = useState("⌘"); + + useEffect(() => { + setKey(isMacOs ? "⌘" : "Ctrl"); + }, []); + + return ( + + {key} K + + ); +} + export function Search() { const [open, setOpen] = useState(false); const router = useRouter(); @@ -44,7 +58,13 @@ export function Search() { return ( <> - @@ -63,26 +83,32 @@ export function Search() { {query.isLoading && Loading...} - {!query.isLoading && search.length > 0 && results.length === 0 && ( - No results found. - )} - {!query.isLoading && search.length === 0 && results.length > 0 && ( - - {results.slice(0, 8).map((result: SortedResult) => ( - onSelect(result.url)} - className={styles.item} - > -
- - {result.content} -
-
- ))} -
- )} + {!query.isLoading && + search.length > 0 && + results.length === 0 && ( + No results found. + )} + {!query.isLoading && + search.length === 0 && + results.length > 0 && ( + + {results.slice(0, 8).map((result: SortedResult) => ( + onSelect(result.url)} + className={styles.item} + > +
+ + + {result.content} + +
+
+ ))} +
+ )} {search.length > 0 && results.map((result: SortedResult) => (
- {result.type === "page" ? : } + {result.type === "page" ? ( + + ) : ( + + )}
{result.type === "heading" ? ( <> - {result.content} + + {result.content} + - - {getPageTitle(result.url)} + + {getPageTitle(result.url)} + ) : ( - {result.content} + + {result.content} + )}
@@ -125,4 +161,3 @@ function getPageTitle(url: string): string { .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); } - From d984f3503396ea8d2beae20184ea3d983ddaed12 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 5 Feb 2026 14:55:23 +0530 Subject: [PATCH 09/10] feat: add footer component --- examples/basic/chronicle.yaml | 7 ++ .../src/components/ui/footer.module.css | 27 +++++++ .../chronicle/src/components/ui/footer.tsx | 30 ++++++++ packages/chronicle/src/lib/config.ts | 6 +- .../src/themes/default/Layout.module.css | 10 --- .../chronicle/src/themes/default/Layout.tsx | 70 +++++++++++-------- 6 files changed, 109 insertions(+), 41 deletions(-) create mode 100644 packages/chronicle/src/components/ui/footer.module.css create mode 100644 packages/chronicle/src/components/ui/footer.tsx diff --git a/examples/basic/chronicle.yaml b/examples/basic/chronicle.yaml index 76a0bdb..c75d3a9 100644 --- a/examples/basic/chronicle.yaml +++ b/examples/basic/chronicle.yaml @@ -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: / diff --git a/packages/chronicle/src/components/ui/footer.module.css b/packages/chronicle/src/components/ui/footer.module.css new file mode 100644 index 0000000..b40bc91 --- /dev/null +++ b/packages/chronicle/src/components/ui/footer.module.css @@ -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); +} diff --git a/packages/chronicle/src/components/ui/footer.tsx b/packages/chronicle/src/components/ui/footer.tsx new file mode 100644 index 0000000..476ce18 --- /dev/null +++ b/packages/chronicle/src/components/ui/footer.tsx @@ -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 ( +
+ + {config?.copyright && ( + + {config.copyright} + + )} + {config?.links && config.links.length > 0 && ( + + {config.links.map((link) => ( + + {link.label} + + ))} + + )} + +
+ ); +} diff --git a/packages/chronicle/src/lib/config.ts b/packages/chronicle/src/lib/config.ts index 6249cdb..8ef729a 100644 --- a/packages/chronicle/src/lib/config.ts +++ b/packages/chronicle/src/lib/config.ts @@ -11,8 +11,9 @@ const defaultConfig: ChronicleConfig = { search: { enabled: true, placeholder: 'Search...' }, } -export function loadConfig(contentDir: string = './content'): ChronicleConfig { - const configPath = path.join(contentDir, CONFIG_FILE) +export function loadConfig(contentDir?: string): ChronicleConfig { + const dir = contentDir ?? process.env.CHRONICLE_CONTENT_DIR ?? './content' + const configPath = path.join(dir, CONFIG_FILE) if (!fs.existsSync(configPath)) { return defaultConfig @@ -29,6 +30,7 @@ export function loadConfig(contentDir: string = './content'): ChronicleConfig { colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors }, }, search: { ...defaultConfig.search, ...userConfig.search }, + footer: userConfig.footer, } } diff --git a/packages/chronicle/src/themes/default/Layout.module.css b/packages/chronicle/src/themes/default/Layout.module.css index 96c83fe..2b97e43 100644 --- a/packages/chronicle/src/themes/default/Layout.module.css +++ b/packages/chronicle/src/themes/default/Layout.module.css @@ -62,13 +62,3 @@ .page { padding: var(--rs-space-2) 0; } - -.footer { - border-top: 1px solid var(--rs-color-border-base-primary); - padding: var(--rs-space-5) var(--rs-space-9); - text-align: center; -} - -.footerText { - color: var(--rs-color-text-base-secondary); -} diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 1d5445b..d6319b5 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -1,15 +1,16 @@ -'use client' +"use client"; -import { usePathname } from 'next/navigation' -import { Flex, Navbar, Headline, Link, Sidebar, Text } from '@raystack/apsara' -import { ClientThemeSwitcher } from '../../components/ui/client-theme-switcher' -import { Search } from '../../components/ui/search' -import type { ThemeLayoutProps, PageTreeItem } from '../../types' -import styles from './Layout.module.css' +import { usePathname } from "next/navigation"; +import { Flex, Navbar, Headline, Link, Sidebar } from "@raystack/apsara"; +import { ClientThemeSwitcher } from "../../components/ui/client-theme-switcher"; +import { Search } from "../../components/ui/search"; +import { Footer } from "../../components/ui/footer"; +import type { ThemeLayoutProps, PageTreeItem } from "../../types"; +import styles from "./Layout.module.css"; export function Layout({ children, config, tree }: ThemeLayoutProps) { - const pathname = usePathname() - + const pathname = usePathname(); + console.log(config); return ( @@ -34,43 +35,54 @@ export function Layout({ children, config, tree }: ThemeLayoutProps) { {tree.children.map((item) => ( - + ))} -
- {children} -
+
{children}
-
- - Built with Chronicle - -
+