diff --git a/.gitignore b/.gitignore index 19e7f26e..63754daf 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,7 @@ web_modules/ # Next.js build output .next out +next-env.d.ts # Fumadocs generated .source diff --git a/packages/chronicle/next-env.d.ts b/packages/chronicle/next-env.d.ts deleted file mode 100644 index c4b7818f..00000000 --- a/packages/chronicle/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./.next/dev/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/chronicle/src/app/[[...slug]]/page.tsx b/packages/chronicle/src/app/[[...slug]]/page.tsx index 8231f34b..077fe87c 100644 --- a/packages/chronicle/src/app/[[...slug]]/page.tsx +++ b/packages/chronicle/src/app/[[...slug]]/page.tsx @@ -2,7 +2,7 @@ import { notFound } from 'next/navigation' import type { MDXContent } from 'mdx/types' import { loadConfig } from '../../lib/config' import { source, buildPageTree } from '../../lib/source' -import { defaultTheme } from '../../themes/default' +import { getTheme } from '../../themes/registry' import { mdxComponents } from '../../components/mdx' interface PageProps { @@ -26,7 +26,7 @@ export default async function DocsPage({ params }: PageProps) { notFound() } - const { Layout, Page } = defaultTheme + const { Layout, Page } = getTheme(config.theme?.name) const data = page.data as PageData const MDXBody = data.body diff --git a/packages/chronicle/src/app/apis/[[...slug]]/layout.module.css b/packages/chronicle/src/app/apis/[[...slug]]/layout.module.css index 6e1094e7..a3c8ffe0 100644 --- a/packages/chronicle/src/app/apis/[[...slug]]/layout.module.css +++ b/packages/chronicle/src/app/apis/[[...slug]]/layout.module.css @@ -14,4 +14,9 @@ .content { height: 100%; overflow-y: auto; + padding-right: 0; +} + +.hiddenSearch { + display: none; } diff --git a/packages/chronicle/src/app/apis/[[...slug]]/layout.tsx b/packages/chronicle/src/app/apis/[[...slug]]/layout.tsx index b27b9c05..75a514d3 100644 --- a/packages/chronicle/src/app/apis/[[...slug]]/layout.tsx +++ b/packages/chronicle/src/app/apis/[[...slug]]/layout.tsx @@ -1,11 +1,13 @@ import { loadConfig } from '../../../lib/config' import { loadApiSpecs } from '../../../lib/openapi' import { buildApiPageTree } from '../../../lib/api-routes' -import { Layout } from '../../../themes/default' +import { getTheme } from '../../../themes/registry' +import { Search } from '../../../components/ui/search' import styles from './layout.module.css' export default function ApiLayout({ children }: { children: React.ReactNode }) { const config = loadConfig() + const { Layout } = getTheme(config.theme?.name) const specs = loadApiSpecs(config.api ?? []) const tree = buildApiPageTree(specs) @@ -16,6 +18,7 @@ export default function ApiLayout({ children }: { children: React.ReactNode }) { sidebar: styles.sidebar, content: styles.content, }}> + {children} ) diff --git a/packages/chronicle/src/components/api/field-row.tsx b/packages/chronicle/src/components/api/field-row.tsx index c7458dfe..1ede3ddc 100644 --- a/packages/chronicle/src/components/api/field-row.tsx +++ b/packages/chronicle/src/components/api/field-row.tsx @@ -63,7 +63,7 @@ export function FieldRow({ field, location, editable, value, onChange }: FieldRo {label} - { + { const newItem = itemChildren ? {} : '' onChange?.(field.name, [...items, newItem]) }}> @@ -105,7 +105,7 @@ export function FieldRow({ field, location, editable, value, onChange }: FieldRo }} /> )} - { + { const updated = items.filter((_, j) => j !== i) onChange?.(field.name, updated) }}> diff --git a/packages/chronicle/src/components/api/key-value-editor.tsx b/packages/chronicle/src/components/api/key-value-editor.tsx index b2960897..ed8ce391 100644 --- a/packages/chronicle/src/components/api/key-value-editor.tsx +++ b/packages/chronicle/src/components/api/key-value-editor.tsx @@ -49,7 +49,7 @@ export function KeyValueEditor({ entries, onChange }: KeyValueEditorProps) { onChange={(e) => updateEntry(i, 'value', e.target.value)} /> - removeEntry(i)}> + removeEntry(i)}> diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index 0f910be0..21f1ef43 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { Button, Command, Dialog, Text } from "@raystack/apsara"; +import { cx } from "class-variance-authority"; import { useDocsSearch } from "fumadocs-core/search/client"; import type { SortedResult } from "fumadocs-core/search"; import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline"; @@ -24,7 +25,11 @@ function SearchShortcutKey({ className }: { className?: string }) { ); } -export function Search() { +interface SearchProps { + className?: string +} + +export function Search({ className }: SearchProps) { const [open, setOpen] = useState(false); const router = useRouter(); @@ -65,7 +70,7 @@ export function Search() { variant="outline" color="neutral" onClick={() => setOpen(true)} - className={styles.trigger} + className={cx(styles.trigger, className)} trailingIcon={} > Search... diff --git a/packages/chronicle/src/themes/paper/ChapterNav.module.css b/packages/chronicle/src/themes/paper/ChapterNav.module.css new file mode 100644 index 00000000..a7abaeb0 --- /dev/null +++ b/packages/chronicle/src/themes/paper/ChapterNav.module.css @@ -0,0 +1,71 @@ +.nav { + display: flex; + flex-direction: column; + gap: var(--rs-space-5); +} + +.chapter { + display: flex; + flex-direction: column; + gap: var(--rs-space-2); +} + +.chapterLabel { + font-size: var(--rs-font-size-small); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--rs-color-foreground-base-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chapterItems { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--rs-space-1); + padding-left: var(--rs-space-4); +} + +.link { + display: flex; + align-items: center; + gap: var(--rs-space-2); + font-size: var(--rs-font-size-small); + color: var(--rs-color-foreground-base-tertiary); + text-decoration: none; + padding: var(--rs-space-1) 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.link:hover { + color: var(--rs-color-foreground-base-primary); +} + +.active { + color: var(--rs-color-foreground-accent-primary); + font-weight: 500; +} + +.icon { + display: flex; + align-items: center; + flex-shrink: 0; +} + +.subLabel { + font-size: var(--rs-font-size-small); + font-weight: 500; + color: var(--rs-color-foreground-base-secondary); + margin-top: var(--rs-space-3); + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/packages/chronicle/src/themes/paper/ChapterNav.tsx b/packages/chronicle/src/themes/paper/ChapterNav.tsx new file mode 100644 index 00000000..4c03c65f --- /dev/null +++ b/packages/chronicle/src/themes/paper/ChapterNav.tsx @@ -0,0 +1,96 @@ +'use client' + +import { usePathname } from 'next/navigation' +import NextLink from 'next/link' +import { MethodBadge } from '../../components/api/method-badge' +import type { PageTree, PageTreeItem } from '../../types' +import styles from './ChapterNav.module.css' + +const iconMap: Record = { + 'method-get': , + 'method-post': , + 'method-put': , + 'method-delete': , + 'method-patch': , +} + +interface ChapterNavProps { + tree: PageTree +} + +function buildChapterIndices(children: PageTreeItem[]): Map { + const indices = new Map() + let index = 0 + for (const item of children) { + if (item.type === 'folder' && item.children) { + index++ + indices.set(item, index) + } + } + return indices +} + +export function ChapterNav({ tree }: ChapterNavProps) { + const pathname = usePathname() + const chapterIndices = buildChapterIndices(tree.children) + + return ( + + ) +} + +function ChapterItem({ item, pathname }: { item: PageTreeItem; pathname: string }) { + if (item.type === 'separator') return null + + if (item.type === 'folder' && item.children) { + return ( +
  • + {item.name} +
      + {item.children.map((child) => ( + + ))} +
    +
  • + ) + } + + const isActive = pathname === item.url + const icon = item.icon ? iconMap[item.icon] : null + + return ( +
  • + + {icon && {icon}} + {item.name} + +
  • + ) +} diff --git a/packages/chronicle/src/themes/paper/Layout.module.css b/packages/chronicle/src/themes/paper/Layout.module.css new file mode 100644 index 00000000..ec9fd548 --- /dev/null +++ b/packages/chronicle/src/themes/paper/Layout.module.css @@ -0,0 +1,33 @@ +.layout { + --paper-sidebar-width: 260px; + + min-height: 100vh; +} + +.body { + flex: 1; +} + +.sidebar { + width: var(--paper-sidebar-width); + padding: var(--rs-space-7) var(--rs-space-5); + background: var(--rs-color-background-neutral-primary); + overflow-y: auto; + font-family: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace; +} + +.title { + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--rs-color-foreground-accent-primary); + font-family: inherit; + font-size: var(--rs-font-size-mono-large); + margin-bottom: var(--rs-space-7); +} + +.content { + flex: 1; + overflow-y: auto; + background: var(--rs-color-background-neutral-primary); + padding-right: var(--paper-sidebar-width); +} diff --git a/packages/chronicle/src/themes/paper/Layout.tsx b/packages/chronicle/src/themes/paper/Layout.tsx new file mode 100644 index 00000000..488af76c --- /dev/null +++ b/packages/chronicle/src/themes/paper/Layout.tsx @@ -0,0 +1,25 @@ +'use client' + +import { Flex, Headline } from '@raystack/apsara' +import { cx } from 'class-variance-authority' +import { Footer } from '../../components/ui/footer' +import { ChapterNav } from './ChapterNav' +import type { ThemeLayoutProps } from '../../types' +import styles from './Layout.module.css' + +export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) { + return ( + + + +
    {children}
    +
    +