@raystack/chronicle 0.1.0-canary.5a2be79

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.
Files changed (107) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +9980 -0
  3. package/next.config.mjs +10 -0
  4. package/package.json +63 -0
  5. package/source.config.ts +50 -0
  6. package/src/app/[[...slug]]/layout.tsx +15 -0
  7. package/src/app/[[...slug]]/page.tsx +57 -0
  8. package/src/app/api/apis-proxy/route.ts +59 -0
  9. package/src/app/api/health/route.ts +3 -0
  10. package/src/app/api/search/route.ts +90 -0
  11. package/src/app/apis/[[...slug]]/layout.module.css +22 -0
  12. package/src/app/apis/[[...slug]]/layout.tsx +26 -0
  13. package/src/app/apis/[[...slug]]/page.tsx +57 -0
  14. package/src/app/layout.tsx +26 -0
  15. package/src/app/llms-full.txt/route.ts +18 -0
  16. package/src/app/llms.txt/route.ts +15 -0
  17. package/src/app/providers.tsx +8 -0
  18. package/src/cli/commands/build.ts +32 -0
  19. package/src/cli/commands/dev.ts +33 -0
  20. package/src/cli/commands/init.ts +155 -0
  21. package/src/cli/commands/serve.ts +53 -0
  22. package/src/cli/commands/start.ts +33 -0
  23. package/src/cli/index.ts +21 -0
  24. package/src/cli/utils/config.ts +43 -0
  25. package/src/cli/utils/index.ts +3 -0
  26. package/src/cli/utils/process.ts +7 -0
  27. package/src/cli/utils/resolve.ts +4 -0
  28. package/src/cli/utils/scaffold.ts +131 -0
  29. package/src/components/api/code-snippets.module.css +7 -0
  30. package/src/components/api/code-snippets.tsx +76 -0
  31. package/src/components/api/endpoint-page.module.css +58 -0
  32. package/src/components/api/endpoint-page.tsx +283 -0
  33. package/src/components/api/field-row.module.css +126 -0
  34. package/src/components/api/field-row.tsx +204 -0
  35. package/src/components/api/field-section.module.css +24 -0
  36. package/src/components/api/field-section.tsx +100 -0
  37. package/src/components/api/index.ts +8 -0
  38. package/src/components/api/json-editor.module.css +9 -0
  39. package/src/components/api/json-editor.tsx +61 -0
  40. package/src/components/api/key-value-editor.module.css +13 -0
  41. package/src/components/api/key-value-editor.tsx +62 -0
  42. package/src/components/api/method-badge.module.css +4 -0
  43. package/src/components/api/method-badge.tsx +29 -0
  44. package/src/components/api/response-panel.module.css +8 -0
  45. package/src/components/api/response-panel.tsx +44 -0
  46. package/src/components/common/breadcrumb.tsx +3 -0
  47. package/src/components/common/button.tsx +3 -0
  48. package/src/components/common/callout.module.css +7 -0
  49. package/src/components/common/callout.tsx +27 -0
  50. package/src/components/common/code-block.tsx +3 -0
  51. package/src/components/common/dialog.tsx +3 -0
  52. package/src/components/common/index.ts +10 -0
  53. package/src/components/common/input-field.tsx +3 -0
  54. package/src/components/common/sidebar.tsx +3 -0
  55. package/src/components/common/switch.tsx +3 -0
  56. package/src/components/common/table.tsx +3 -0
  57. package/src/components/common/tabs.tsx +3 -0
  58. package/src/components/mdx/code.module.css +42 -0
  59. package/src/components/mdx/code.tsx +27 -0
  60. package/src/components/mdx/details.module.css +37 -0
  61. package/src/components/mdx/details.tsx +18 -0
  62. package/src/components/mdx/image.tsx +38 -0
  63. package/src/components/mdx/index.tsx +35 -0
  64. package/src/components/mdx/link.tsx +38 -0
  65. package/src/components/mdx/mermaid.module.css +9 -0
  66. package/src/components/mdx/mermaid.tsx +37 -0
  67. package/src/components/mdx/paragraph.module.css +8 -0
  68. package/src/components/mdx/paragraph.tsx +19 -0
  69. package/src/components/mdx/table.tsx +40 -0
  70. package/src/components/ui/breadcrumbs.tsx +72 -0
  71. package/src/components/ui/client-theme-switcher.tsx +18 -0
  72. package/src/components/ui/footer.module.css +27 -0
  73. package/src/components/ui/footer.tsx +30 -0
  74. package/src/components/ui/search.module.css +104 -0
  75. package/src/components/ui/search.tsx +202 -0
  76. package/src/lib/api-routes.ts +120 -0
  77. package/src/lib/config.ts +55 -0
  78. package/src/lib/get-llm-text.ts +10 -0
  79. package/src/lib/index.ts +2 -0
  80. package/src/lib/openapi.ts +188 -0
  81. package/src/lib/remark-unused-directives.ts +30 -0
  82. package/src/lib/schema.ts +99 -0
  83. package/src/lib/snippet-generators.ts +87 -0
  84. package/src/lib/source.ts +67 -0
  85. package/src/themes/default/Layout.module.css +81 -0
  86. package/src/themes/default/Layout.tsx +133 -0
  87. package/src/themes/default/Page.module.css +46 -0
  88. package/src/themes/default/Page.tsx +21 -0
  89. package/src/themes/default/Toc.module.css +48 -0
  90. package/src/themes/default/Toc.tsx +66 -0
  91. package/src/themes/default/font.ts +6 -0
  92. package/src/themes/default/index.ts +13 -0
  93. package/src/themes/paper/ChapterNav.module.css +71 -0
  94. package/src/themes/paper/ChapterNav.tsx +96 -0
  95. package/src/themes/paper/Layout.module.css +33 -0
  96. package/src/themes/paper/Layout.tsx +25 -0
  97. package/src/themes/paper/Page.module.css +174 -0
  98. package/src/themes/paper/Page.tsx +107 -0
  99. package/src/themes/paper/ReadingProgress.module.css +132 -0
  100. package/src/themes/paper/ReadingProgress.tsx +294 -0
  101. package/src/themes/paper/index.ts +8 -0
  102. package/src/themes/registry.ts +14 -0
  103. package/src/types/config.ts +69 -0
  104. package/src/types/content.ts +35 -0
  105. package/src/types/index.ts +3 -0
  106. package/src/types/theme.ts +22 -0
  107. package/tsconfig.json +30 -0
@@ -0,0 +1,81 @@
1
+ .layout {
2
+ min-height: 100vh;
3
+ }
4
+
5
+ .header {
6
+ border-bottom: 1px solid var(--rs-color-border-base-primary);
7
+ }
8
+
9
+ .search {
10
+ margin-left: var(--rs-space-5);
11
+ }
12
+
13
+ .body {
14
+ flex: 1;
15
+ }
16
+
17
+ .sidebar {
18
+ width: 260px;
19
+ position: sticky;
20
+ top: 0;
21
+ height: 100vh;
22
+ }
23
+
24
+ .content {
25
+ flex: 1;
26
+ padding: var(--rs-space-9);
27
+ }
28
+
29
+ .sidebarList {
30
+ list-style: none;
31
+ padding: 0;
32
+ margin: 0;
33
+ }
34
+
35
+ .separator {
36
+ height: 1px;
37
+ background: var(--rs-color-border-base-primary);
38
+ margin: var(--rs-space-3) 0;
39
+ }
40
+
41
+ .folder {
42
+ margin-bottom: var(--rs-space-3);
43
+ }
44
+
45
+ .folderLabel {
46
+ font-weight: 500;
47
+ font-size: 0.875rem;
48
+ color: var(--rs-color-text-base-secondary);
49
+ text-transform: uppercase;
50
+ letter-spacing: 0.05em;
51
+ }
52
+
53
+ .folder > .sidebarList {
54
+ margin-top: var(--rs-space-2);
55
+ padding-left: var(--rs-space-4);
56
+ }
57
+
58
+ .navButton {
59
+ display: flex;
60
+ align-items: center;
61
+ height: 32px;
62
+ padding: 0 var(--rs-space-4);
63
+ border: 1px solid var(--rs-color-border-base-primary);
64
+ border-radius: var(--rs-radius-2);
65
+ font-size: var(--rs-font-size-small);
66
+ font-weight: var(--rs-font-weight-medium);
67
+ color: var(--rs-color-foreground-base-primary);
68
+ text-decoration: none;
69
+ }
70
+
71
+ .navButton:hover {
72
+ background: var(--rs-color-background-base-primary-hover);
73
+ }
74
+
75
+ .groupItems {
76
+ padding-left: var(--rs-space-4);
77
+ }
78
+
79
+ .page {
80
+ padding: var(--rs-space-2) 0;
81
+ }
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import { useMemo, useEffect, useRef } from "react";
4
+ import { usePathname } from "next/navigation";
5
+ import NextLink from "next/link";
6
+ import { cx } from "class-variance-authority";
7
+ import { Flex, Navbar, Headline, Link, Sidebar, Button } from "@raystack/apsara";
8
+ import { RectangleStackIcon } from "@heroicons/react/24/outline";
9
+ import { ClientThemeSwitcher } from "@/components/ui/client-theme-switcher";
10
+ import { Search } from "@/components/ui/search";
11
+ import { Footer } from "@/components/ui/footer";
12
+ import { MethodBadge } from "@/components/api/method-badge";
13
+ import type { ThemeLayoutProps, PageTreeItem } from "@/types";
14
+ import styles from "./Layout.module.css";
15
+
16
+ const iconMap: Record<string, React.ReactNode> = {
17
+ "rectangle-stack": <RectangleStackIcon width={16} height={16} />,
18
+ "method-get": <MethodBadge method="GET" size="micro" />,
19
+ "method-post": <MethodBadge method="POST" size="micro" />,
20
+ "method-put": <MethodBadge method="PUT" size="micro" />,
21
+ "method-delete": <MethodBadge method="DELETE" size="micro" />,
22
+ "method-patch": <MethodBadge method="PATCH" size="micro" />,
23
+ };
24
+
25
+ let savedScrollTop = 0;
26
+
27
+ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
28
+ const pathname = usePathname();
29
+ const scrollRef = useRef<HTMLDivElement>(null);
30
+
31
+ useEffect(() => {
32
+ const el = scrollRef.current;
33
+ if (!el) return;
34
+ const onScroll = () => { savedScrollTop = el.scrollTop; };
35
+ el.addEventListener('scroll', onScroll);
36
+ return () => el.removeEventListener('scroll', onScroll);
37
+ }, []);
38
+
39
+ useEffect(() => {
40
+ const el = scrollRef.current;
41
+ if (el) requestAnimationFrame(() => { el.scrollTop = savedScrollTop; });
42
+ }, [pathname]);
43
+
44
+ return (
45
+ <Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
46
+ <Navbar className={styles.header}>
47
+ <Navbar.Start>
48
+ <NextLink href="/" style={{ textDecoration: 'none', color: 'inherit' }}>
49
+ <Headline size="small" weight="medium" as="h1">
50
+ {config.title}
51
+ </Headline>
52
+ </NextLink>
53
+ </Navbar.Start>
54
+ <Navbar.End>
55
+ <Flex gap="medium" align="center" className={styles.navActions}>
56
+ {config.api?.map((api) => (
57
+ <NextLink key={api.basePath} href={api.basePath} className={styles.navButton}>
58
+ {api.name} API
59
+ </NextLink>
60
+ ))}
61
+ {config.navigation?.links?.map((link) => (
62
+ <Link key={link.href} href={link.href}>
63
+ {link.label}
64
+ </Link>
65
+ ))}
66
+ {config.search?.enabled && <Search />}
67
+ </Flex>
68
+ <ClientThemeSwitcher size={16} />
69
+ </Navbar.End>
70
+ </Navbar>
71
+ <Flex className={cx(styles.body, classNames?.body)}>
72
+ <Sidebar defaultOpen collapsible={false} className={cx(styles.sidebar, classNames?.sidebar)}>
73
+ <Sidebar.Main ref={scrollRef}>
74
+ {tree.children.map((item) => (
75
+ <SidebarNode
76
+ key={item.url ?? item.name}
77
+ item={item}
78
+ pathname={pathname}
79
+ />
80
+ ))}
81
+ </Sidebar.Main>
82
+ </Sidebar>
83
+ <main className={cx(styles.content, classNames?.content)}>{children}</main>
84
+ </Flex>
85
+ <Footer config={config.footer} />
86
+ </Flex>
87
+ );
88
+ }
89
+
90
+ function SidebarNode({
91
+ item,
92
+ pathname,
93
+ }: {
94
+ item: PageTreeItem;
95
+ pathname: string;
96
+ }) {
97
+ if (item.type === "separator") {
98
+ return null;
99
+ }
100
+
101
+ if (item.type === "folder" && item.children) {
102
+ return (
103
+ <Sidebar.Group
104
+ label={item.name}
105
+ leadingIcon={item.icon ? iconMap[item.icon] : undefined}
106
+ classNames={{ items: styles.groupItems }}
107
+ >
108
+ {item.children.map((child) => (
109
+ <SidebarNode
110
+ key={child.url ?? child.name}
111
+ item={child}
112
+ pathname={pathname}
113
+ />
114
+ ))}
115
+ </Sidebar.Group>
116
+ );
117
+ }
118
+
119
+ const isActive = pathname === item.url;
120
+ const href = item.url ?? "#";
121
+ const link = useMemo(() => <NextLink href={href} scroll={false} />, [href]);
122
+
123
+ return (
124
+ <Sidebar.Item
125
+ href={href}
126
+ active={isActive}
127
+ leadingIcon={item.icon ? iconMap[item.icon] : undefined}
128
+ as={link}
129
+ >
130
+ {item.name}
131
+ </Sidebar.Item>
132
+ );
133
+ }
@@ -0,0 +1,46 @@
1
+ .page {
2
+ gap: var(--rs-space-9);
3
+ }
4
+
5
+ .article {
6
+ flex: 1;
7
+ min-width: 0;
8
+ max-width: 768px;
9
+ }
10
+
11
+ .content {
12
+ line-height: 1.7;
13
+ }
14
+
15
+ .content h1,
16
+ .content h2,
17
+ .content h3,
18
+ .content h4,
19
+ .content h5,
20
+ .content h6 {
21
+ margin-top: var(--rs-space-8);
22
+ margin-bottom: var(--rs-space-5);
23
+ line-height: 1.4;
24
+ }
25
+
26
+ .content ul,
27
+ .content ol {
28
+ padding-left: var(--rs-space-5);
29
+ margin-bottom: var(--rs-space-5);
30
+ }
31
+
32
+ .content li {
33
+ font-size: var(--rs-font-size-regular);
34
+ margin: var(--rs-space-2) 0;
35
+ }
36
+
37
+ .content [role="tablist"] {
38
+ margin-bottom: var(--rs-space-3);
39
+ }
40
+
41
+ .content table {
42
+ display: block;
43
+ max-width: 100%;
44
+ overflow-x: auto;
45
+ margin-bottom: var(--rs-space-5);
46
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import { Flex } from '@raystack/apsara'
4
+ import type { ThemePageProps } from '@/types'
5
+ import { Breadcrumbs } from '@/components/ui/breadcrumbs'
6
+ import { Toc } from './Toc'
7
+ import styles from './Page.module.css'
8
+
9
+ export function Page({ page, tree }: ThemePageProps) {
10
+ return (
11
+ <Flex className={styles.page}>
12
+ <article className={styles.article}>
13
+ <Breadcrumbs slug={page.slug} tree={tree} />
14
+ <div className={styles.content}>
15
+ {page.content}
16
+ </div>
17
+ </article>
18
+ <Toc items={page.toc} />
19
+ </Flex>
20
+ )
21
+ }
@@ -0,0 +1,48 @@
1
+ .toc {
2
+ width: 200px;
3
+ flex-shrink: 0;
4
+ position: sticky;
5
+ top: var(--rs-space-9);
6
+ max-height: calc(100vh - var(--rs-space-17));
7
+ overflow-y: auto;
8
+ }
9
+
10
+ .title {
11
+ display: block;
12
+ color: var(--rs-color-foreground-base-secondary);
13
+ text-transform: uppercase;
14
+ letter-spacing: 0.05em;
15
+ margin-bottom: var(--rs-space-3);
16
+ font-size: var(--rs-font-size-mini);
17
+ }
18
+
19
+ .nav {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 0;
23
+ border-left: 1px solid var(--rs-color-border-base-primary);
24
+ padding-left: var(--rs-space-3);
25
+ margin-bottom: var(--rs-space-6);
26
+ }
27
+
28
+ .link {
29
+ color: var(--rs-color-foreground-base-tertiary);
30
+ text-decoration: none;
31
+ font-size: var(--rs-font-size-small);
32
+ line-height: 1.4;
33
+ padding: var(--rs-space-1) 0;
34
+ transition: color 0.15s ease;
35
+ }
36
+
37
+ .link:hover {
38
+ color: var(--rs-color-foreground-base-primary);
39
+ }
40
+
41
+ .active {
42
+ color: var(--rs-color-foreground-base-primary);
43
+ font-weight: 500;
44
+ }
45
+
46
+ .nested {
47
+ padding-left: var(--rs-space-3);
48
+ }
@@ -0,0 +1,66 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Text } from '@raystack/apsara'
5
+ import type { TocItem } from '@/types'
6
+ import styles from './Toc.module.css'
7
+
8
+ interface TocProps {
9
+ items: TocItem[]
10
+ }
11
+
12
+ export function Toc({ items }: TocProps) {
13
+ const [activeId, setActiveId] = useState<string>('')
14
+
15
+ // Filter to only show h2 and h3 headings
16
+ const filteredItems = items.filter((item) => item.depth >= 2 && item.depth <= 3)
17
+
18
+ useEffect(() => {
19
+ const headingIds = filteredItems.map((item) => item.url.replace('#', ''))
20
+
21
+ const observer = new IntersectionObserver(
22
+ (entries) => {
23
+ entries.forEach((entry) => {
24
+ if (entry.isIntersecting) {
25
+ setActiveId(entry.target.id)
26
+ }
27
+ })
28
+ },
29
+ // -80px top: offset for fixed header, -80% bottom: trigger when heading is in top 20% of viewport
30
+ { rootMargin: '-80px 0px -80% 0px' }
31
+ )
32
+
33
+ headingIds.forEach((id) => {
34
+ const element = document.getElementById(id)
35
+ if (element) observer.observe(element)
36
+ })
37
+
38
+ return () => observer.disconnect()
39
+ }, [filteredItems])
40
+
41
+ if (filteredItems.length === 0) return null
42
+
43
+ return (
44
+ <aside className={styles.toc}>
45
+ <Text size={1} weight="medium" className={styles.title}>
46
+ On this page
47
+ </Text>
48
+ <nav className={styles.nav}>
49
+ {filteredItems.map((item) => {
50
+ const id = item.url.replace('#', '')
51
+ const isActive = activeId === id
52
+ const isNested = item.depth > 2
53
+ return (
54
+ <a
55
+ key={item.url}
56
+ href={item.url}
57
+ className={`${styles.link} ${isActive ? styles.active : ''} ${isNested ? styles.nested : ''}`}
58
+ >
59
+ {item.title}
60
+ </a>
61
+ )
62
+ })}
63
+ </nav>
64
+ </aside>
65
+ )
66
+ }
@@ -0,0 +1,6 @@
1
+ import { Inter } from 'next/font/google'
2
+
3
+ export const inter = Inter({
4
+ subsets: ['latin'],
5
+ display: 'swap',
6
+ })
@@ -0,0 +1,13 @@
1
+ import { Layout } from './Layout'
2
+ import { Page } from './Page'
3
+ import { Toc } from './Toc'
4
+ import { inter } from './font'
5
+ import type { Theme } from '@/types'
6
+
7
+ export const defaultTheme: Theme = {
8
+ Layout,
9
+ Page,
10
+ className: inter.className,
11
+ }
12
+
13
+ export { Layout, Page, Toc }
@@ -0,0 +1,71 @@
1
+ .nav {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--rs-space-5);
5
+ }
6
+
7
+ .chapter {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--rs-space-2);
11
+ }
12
+
13
+ .chapterLabel {
14
+ font-size: var(--rs-font-size-small);
15
+ font-weight: 600;
16
+ text-transform: uppercase;
17
+ letter-spacing: 0.05em;
18
+ color: var(--rs-color-foreground-base-primary);
19
+ white-space: nowrap;
20
+ overflow: hidden;
21
+ text-overflow: ellipsis;
22
+ }
23
+
24
+ .chapterItems {
25
+ list-style: none;
26
+ padding: 0;
27
+ margin: 0;
28
+ display: flex;
29
+ flex-direction: column;
30
+ gap: var(--rs-space-1);
31
+ padding-left: var(--rs-space-4);
32
+ }
33
+
34
+ .link {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: var(--rs-space-2);
38
+ font-size: var(--rs-font-size-small);
39
+ color: var(--rs-color-foreground-base-tertiary);
40
+ text-decoration: none;
41
+ padding: var(--rs-space-1) 0;
42
+ white-space: nowrap;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ }
46
+
47
+ .link:hover {
48
+ color: var(--rs-color-foreground-base-primary);
49
+ }
50
+
51
+ .active {
52
+ color: var(--rs-color-foreground-accent-primary);
53
+ font-weight: 500;
54
+ }
55
+
56
+ .icon {
57
+ display: flex;
58
+ align-items: center;
59
+ flex-shrink: 0;
60
+ }
61
+
62
+ .subLabel {
63
+ font-size: var(--rs-font-size-small);
64
+ font-weight: 500;
65
+ color: var(--rs-color-foreground-base-secondary);
66
+ margin-top: var(--rs-space-3);
67
+ display: block;
68
+ white-space: nowrap;
69
+ overflow: hidden;
70
+ text-overflow: ellipsis;
71
+ }
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+
3
+ import { usePathname } from 'next/navigation'
4
+ import NextLink from 'next/link'
5
+ import { MethodBadge } from '@/components/api/method-badge'
6
+ import type { PageTree, PageTreeItem } from '@/types'
7
+ import styles from './ChapterNav.module.css'
8
+
9
+ const iconMap: Record<string, React.ReactNode> = {
10
+ 'method-get': <MethodBadge method="GET" size="micro" />,
11
+ 'method-post': <MethodBadge method="POST" size="micro" />,
12
+ 'method-put': <MethodBadge method="PUT" size="micro" />,
13
+ 'method-delete': <MethodBadge method="DELETE" size="micro" />,
14
+ 'method-patch': <MethodBadge method="PATCH" size="micro" />,
15
+ }
16
+
17
+ interface ChapterNavProps {
18
+ tree: PageTree
19
+ }
20
+
21
+ function buildChapterIndices(children: PageTreeItem[]): Map<PageTreeItem, number> {
22
+ const indices = new Map<PageTreeItem, number>()
23
+ let index = 0
24
+ for (const item of children) {
25
+ if (item.type === 'folder' && item.children) {
26
+ index++
27
+ indices.set(item, index)
28
+ }
29
+ }
30
+ return indices
31
+ }
32
+
33
+ export function ChapterNav({ tree }: ChapterNavProps) {
34
+ const pathname = usePathname()
35
+ const chapterIndices = buildChapterIndices(tree.children)
36
+
37
+ return (
38
+ <nav className={styles.nav}>
39
+ <ul className={styles.chapterItems}>
40
+ {tree.children.map((item) => {
41
+ if (item.type === 'separator') return null
42
+
43
+ if (item.type === 'folder' && item.children) {
44
+ const chapterIndex = chapterIndices.get(item) ?? 0
45
+ return (
46
+ <li key={item.name} className={styles.chapter}>
47
+ <span className={styles.chapterLabel}>
48
+ {String(chapterIndex).padStart(2, '0')}. {item.name}
49
+ </span>
50
+ <ul className={styles.chapterItems}>
51
+ {item.children.map((child) => (
52
+ <ChapterItem key={child.url ?? child.name} item={child} pathname={pathname} />
53
+ ))}
54
+ </ul>
55
+ </li>
56
+ )
57
+ }
58
+
59
+ return <ChapterItem key={item.url ?? item.name} item={item} pathname={pathname} />
60
+ })}
61
+ </ul>
62
+ </nav>
63
+ )
64
+ }
65
+
66
+ function ChapterItem({ item, pathname }: { item: PageTreeItem; pathname: string }) {
67
+ if (item.type === 'separator') return null
68
+
69
+ if (item.type === 'folder' && item.children) {
70
+ return (
71
+ <li>
72
+ <span className={styles.subLabel}>{item.name}</span>
73
+ <ul className={styles.chapterItems}>
74
+ {item.children.map((child) => (
75
+ <ChapterItem key={child.url ?? child.name} item={child} pathname={pathname} />
76
+ ))}
77
+ </ul>
78
+ </li>
79
+ )
80
+ }
81
+
82
+ const isActive = pathname === item.url
83
+ const icon = item.icon ? iconMap[item.icon] : null
84
+
85
+ return (
86
+ <li>
87
+ <NextLink
88
+ href={item.url ?? '#'}
89
+ className={`${styles.link} ${isActive ? styles.active : ''}`}
90
+ >
91
+ {icon && <span className={styles.icon}>{icon}</span>}
92
+ <span>{item.name}</span>
93
+ </NextLink>
94
+ </li>
95
+ )
96
+ }
@@ -0,0 +1,33 @@
1
+ .layout {
2
+ --paper-sidebar-width: 260px;
3
+
4
+ min-height: 100vh;
5
+ }
6
+
7
+ .body {
8
+ flex: 1;
9
+ }
10
+
11
+ .sidebar {
12
+ width: var(--paper-sidebar-width);
13
+ padding: var(--rs-space-7) var(--rs-space-5);
14
+ background: var(--rs-color-background-neutral-primary);
15
+ overflow-y: auto;
16
+ font-family: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
17
+ }
18
+
19
+ .title {
20
+ text-transform: uppercase;
21
+ letter-spacing: 0.08em;
22
+ color: var(--rs-color-foreground-accent-primary);
23
+ font-family: inherit;
24
+ font-size: var(--rs-font-size-mono-large);
25
+ margin-bottom: var(--rs-space-7);
26
+ }
27
+
28
+ .content {
29
+ flex: 1;
30
+ overflow-y: auto;
31
+ background: var(--rs-color-background-neutral-primary);
32
+ padding-right: var(--paper-sidebar-width);
33
+ }
@@ -0,0 +1,25 @@
1
+ 'use client'
2
+
3
+ import { Flex, Headline } from '@raystack/apsara'
4
+ import { cx } from 'class-variance-authority'
5
+ import { Footer } from '@/components/ui/footer'
6
+ import { ChapterNav } from './ChapterNav'
7
+ import type { ThemeLayoutProps } from '@/types'
8
+ import styles from './Layout.module.css'
9
+
10
+ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
11
+ return (
12
+ <Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
13
+ <Flex className={cx(styles.body, classNames?.body)}>
14
+ <aside className={cx(styles.sidebar, classNames?.sidebar)}>
15
+ <Headline size="small" weight="medium" as="h1" className={styles.title}>
16
+ {config.title}
17
+ </Headline>
18
+ <ChapterNav tree={tree} />
19
+ </aside>
20
+ <div className={cx(styles.content, classNames?.content)}>{children}</div>
21
+ </Flex>
22
+ <Footer config={config.footer} />
23
+ </Flex>
24
+ )
25
+ }