@raystack/chronicle 0.1.0-canary.111b55a

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 (128) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +963 -0
  3. package/package.json +67 -0
  4. package/src/cli/__tests__/config.test.ts +25 -0
  5. package/src/cli/__tests__/scaffold.test.ts +10 -0
  6. package/src/cli/commands/build.ts +74 -0
  7. package/src/cli/commands/dev.ts +21 -0
  8. package/src/cli/commands/init.ts +154 -0
  9. package/src/cli/commands/serve.ts +55 -0
  10. package/src/cli/commands/start.ts +24 -0
  11. package/src/cli/index.ts +21 -0
  12. package/src/cli/utils/config.ts +43 -0
  13. package/src/cli/utils/index.ts +2 -0
  14. package/src/cli/utils/resolve.ts +6 -0
  15. package/src/cli/utils/scaffold.ts +20 -0
  16. package/src/components/api/code-snippets.module.css +7 -0
  17. package/src/components/api/code-snippets.tsx +76 -0
  18. package/src/components/api/endpoint-page.module.css +58 -0
  19. package/src/components/api/endpoint-page.tsx +283 -0
  20. package/src/components/api/field-row.module.css +126 -0
  21. package/src/components/api/field-row.tsx +204 -0
  22. package/src/components/api/field-section.module.css +24 -0
  23. package/src/components/api/field-section.tsx +100 -0
  24. package/src/components/api/index.ts +8 -0
  25. package/src/components/api/json-editor.module.css +9 -0
  26. package/src/components/api/json-editor.tsx +61 -0
  27. package/src/components/api/key-value-editor.module.css +13 -0
  28. package/src/components/api/key-value-editor.tsx +62 -0
  29. package/src/components/api/method-badge.module.css +4 -0
  30. package/src/components/api/method-badge.tsx +29 -0
  31. package/src/components/api/response-panel.module.css +8 -0
  32. package/src/components/api/response-panel.tsx +44 -0
  33. package/src/components/common/breadcrumb.tsx +3 -0
  34. package/src/components/common/button.tsx +3 -0
  35. package/src/components/common/callout.module.css +7 -0
  36. package/src/components/common/callout.tsx +27 -0
  37. package/src/components/common/code-block.tsx +3 -0
  38. package/src/components/common/dialog.tsx +3 -0
  39. package/src/components/common/index.ts +10 -0
  40. package/src/components/common/input-field.tsx +3 -0
  41. package/src/components/common/sidebar.tsx +3 -0
  42. package/src/components/common/switch.tsx +3 -0
  43. package/src/components/common/table.tsx +3 -0
  44. package/src/components/common/tabs.tsx +3 -0
  45. package/src/components/mdx/code.module.css +42 -0
  46. package/src/components/mdx/code.tsx +36 -0
  47. package/src/components/mdx/details.module.css +14 -0
  48. package/src/components/mdx/details.tsx +17 -0
  49. package/src/components/mdx/image.tsx +24 -0
  50. package/src/components/mdx/index.tsx +35 -0
  51. package/src/components/mdx/link.tsx +37 -0
  52. package/src/components/mdx/mermaid.module.css +9 -0
  53. package/src/components/mdx/mermaid.tsx +37 -0
  54. package/src/components/mdx/paragraph.module.css +8 -0
  55. package/src/components/mdx/paragraph.tsx +19 -0
  56. package/src/components/mdx/table.tsx +40 -0
  57. package/src/components/ui/breadcrumbs.tsx +72 -0
  58. package/src/components/ui/client-theme-switcher.tsx +18 -0
  59. package/src/components/ui/footer.module.css +27 -0
  60. package/src/components/ui/footer.tsx +31 -0
  61. package/src/components/ui/search.module.css +111 -0
  62. package/src/components/ui/search.tsx +173 -0
  63. package/src/lib/api-routes.ts +120 -0
  64. package/src/lib/config.ts +56 -0
  65. package/src/lib/head.tsx +45 -0
  66. package/src/lib/index.ts +2 -0
  67. package/src/lib/openapi.ts +188 -0
  68. package/src/lib/page-context.tsx +95 -0
  69. package/src/lib/remark-unused-directives.ts +30 -0
  70. package/src/lib/schema.ts +99 -0
  71. package/src/lib/snippet-generators.ts +87 -0
  72. package/src/lib/source.ts +138 -0
  73. package/src/pages/ApiLayout.module.css +22 -0
  74. package/src/pages/ApiLayout.tsx +29 -0
  75. package/src/pages/ApiPage.tsx +68 -0
  76. package/src/pages/DocsLayout.tsx +18 -0
  77. package/src/pages/DocsPage.tsx +43 -0
  78. package/src/pages/NotFound.tsx +10 -0
  79. package/src/pages/__tests__/head.test.tsx +57 -0
  80. package/src/server/App.tsx +59 -0
  81. package/src/server/__tests__/entry-server.test.tsx +35 -0
  82. package/src/server/__tests__/handlers.test.ts +77 -0
  83. package/src/server/__tests__/og.test.ts +23 -0
  84. package/src/server/__tests__/router.test.ts +72 -0
  85. package/src/server/__tests__/vite-config.test.ts +25 -0
  86. package/src/server/adapters/vercel.ts +133 -0
  87. package/src/server/build-search-index.ts +107 -0
  88. package/src/server/dev.ts +156 -0
  89. package/src/server/entry-client.tsx +74 -0
  90. package/src/server/entry-prod.ts +97 -0
  91. package/src/server/entry-server.tsx +35 -0
  92. package/src/server/entry-vercel.ts +28 -0
  93. package/src/server/handlers/apis-proxy.ts +52 -0
  94. package/src/server/handlers/health.ts +3 -0
  95. package/src/server/handlers/llms.ts +58 -0
  96. package/src/server/handlers/og.ts +87 -0
  97. package/src/server/handlers/robots.ts +11 -0
  98. package/src/server/handlers/search.ts +172 -0
  99. package/src/server/handlers/sitemap.ts +39 -0
  100. package/src/server/handlers/specs.ts +9 -0
  101. package/src/server/index.html +12 -0
  102. package/src/server/prod.ts +18 -0
  103. package/src/server/request-handler.ts +63 -0
  104. package/src/server/router.ts +42 -0
  105. package/src/server/vite-config.ts +71 -0
  106. package/src/themes/default/Layout.module.css +81 -0
  107. package/src/themes/default/Layout.tsx +132 -0
  108. package/src/themes/default/Page.module.css +106 -0
  109. package/src/themes/default/Page.tsx +21 -0
  110. package/src/themes/default/Toc.module.css +48 -0
  111. package/src/themes/default/Toc.tsx +66 -0
  112. package/src/themes/default/font.ts +4 -0
  113. package/src/themes/default/index.ts +13 -0
  114. package/src/themes/paper/ChapterNav.module.css +71 -0
  115. package/src/themes/paper/ChapterNav.tsx +95 -0
  116. package/src/themes/paper/Layout.module.css +33 -0
  117. package/src/themes/paper/Layout.tsx +25 -0
  118. package/src/themes/paper/Page.module.css +174 -0
  119. package/src/themes/paper/Page.tsx +106 -0
  120. package/src/themes/paper/ReadingProgress.module.css +132 -0
  121. package/src/themes/paper/ReadingProgress.tsx +294 -0
  122. package/src/themes/paper/index.ts +8 -0
  123. package/src/themes/registry.ts +14 -0
  124. package/src/types/config.ts +80 -0
  125. package/src/types/content.ts +36 -0
  126. package/src/types/index.ts +3 -0
  127. package/src/types/theme.ts +22 -0
  128. package/tsconfig.json +29 -0
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { useMemo, useEffect, useRef } from "react";
4
+ import { useLocation, Link } from "react-router-dom";
5
+ import { cx } from "class-variance-authority";
6
+ import { Flex, Navbar, Headline, Sidebar, Button } from "@raystack/apsara";
7
+ import { RectangleStackIcon } from "@heroicons/react/24/outline";
8
+ import { ClientThemeSwitcher } from "@/components/ui/client-theme-switcher";
9
+ import { Search } from "@/components/ui/search";
10
+ import { Footer } from "@/components/ui/footer";
11
+ import { MethodBadge } from "@/components/api/method-badge";
12
+ import type { ThemeLayoutProps, PageTreeItem } from "@/types";
13
+ import styles from "./Layout.module.css";
14
+
15
+ const iconMap: Record<string, React.ReactNode> = {
16
+ "rectangle-stack": <RectangleStackIcon width={16} height={16} />,
17
+ "method-get": <MethodBadge method="GET" size="micro" />,
18
+ "method-post": <MethodBadge method="POST" size="micro" />,
19
+ "method-put": <MethodBadge method="PUT" size="micro" />,
20
+ "method-delete": <MethodBadge method="DELETE" size="micro" />,
21
+ "method-patch": <MethodBadge method="PATCH" size="micro" />,
22
+ };
23
+
24
+ let savedScrollTop = 0;
25
+
26
+ export function Layout({ children, config, tree, classNames }: ThemeLayoutProps) {
27
+ const { pathname } = useLocation();
28
+ const scrollRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ const el = scrollRef.current;
32
+ if (!el) return;
33
+ const onScroll = () => { savedScrollTop = el.scrollTop; };
34
+ el.addEventListener('scroll', onScroll);
35
+ return () => el.removeEventListener('scroll', onScroll);
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ const el = scrollRef.current;
40
+ if (el) requestAnimationFrame(() => { el.scrollTop = savedScrollTop; });
41
+ }, [pathname]);
42
+
43
+ return (
44
+ <Flex direction="column" className={cx(styles.layout, classNames?.layout)}>
45
+ <Navbar className={styles.header}>
46
+ <Navbar.Start>
47
+ <Link to="/" style={{ textDecoration: 'none', color: 'inherit' }}>
48
+ <Headline size="small" weight="medium" as="h1">
49
+ {config.title}
50
+ </Headline>
51
+ </Link>
52
+ </Navbar.Start>
53
+ <Navbar.End>
54
+ <Flex gap="medium" align="center" className={styles.navActions}>
55
+ {config.api?.map((api) => (
56
+ <Link key={api.name} to={api.basePath} className={styles.navButton}>
57
+ {api.name} API
58
+ </Link>
59
+ ))}
60
+ {config.navigation?.links?.map((link) => (
61
+ <Link key={link.href} to={link.href}>
62
+ {link.label}
63
+ </Link>
64
+ ))}
65
+ {config.search?.enabled && <Search />}
66
+ </Flex>
67
+ <ClientThemeSwitcher size={16} />
68
+ </Navbar.End>
69
+ </Navbar>
70
+ <Flex className={cx(styles.body, classNames?.body)}>
71
+ <Sidebar defaultOpen collapsible={false} className={cx(styles.sidebar, classNames?.sidebar)}>
72
+ <Sidebar.Main ref={scrollRef}>
73
+ {tree.children.map((item) => (
74
+ <SidebarNode
75
+ key={item.url ?? item.name}
76
+ item={item}
77
+ pathname={pathname}
78
+ />
79
+ ))}
80
+ </Sidebar.Main>
81
+ </Sidebar>
82
+ <main className={cx(styles.content, classNames?.content)}>{children}</main>
83
+ </Flex>
84
+ <Footer config={config.footer} />
85
+ </Flex>
86
+ );
87
+ }
88
+
89
+ function SidebarNode({
90
+ item,
91
+ pathname,
92
+ }: {
93
+ item: PageTreeItem;
94
+ pathname: string;
95
+ }) {
96
+ if (item.type === "separator") {
97
+ return null;
98
+ }
99
+
100
+ if (item.type === "folder" && item.children) {
101
+ return (
102
+ <Sidebar.Group
103
+ label={item.name}
104
+ leadingIcon={item.icon ? iconMap[item.icon] : undefined}
105
+ classNames={{ items: styles.groupItems }}
106
+ >
107
+ {item.children.map((child) => (
108
+ <SidebarNode
109
+ key={child.url ?? child.name}
110
+ item={child}
111
+ pathname={pathname}
112
+ />
113
+ ))}
114
+ </Sidebar.Group>
115
+ );
116
+ }
117
+
118
+ const isActive = pathname === item.url;
119
+ const href = item.url ?? "#";
120
+ const link = useMemo(() => <Link to={href} />, [href]);
121
+
122
+ return (
123
+ <Sidebar.Item
124
+ href={href}
125
+ active={isActive}
126
+ leadingIcon={item.icon ? iconMap[item.icon] : undefined}
127
+ as={link}
128
+ >
129
+ {item.name}
130
+ </Sidebar.Item>
131
+ );
132
+ }
@@ -0,0 +1,106 @@
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 :global(pre) {
42
+ background-color: var(--shiki-light-bg, #fff);
43
+ }
44
+
45
+ .content :global(pre code span) {
46
+ color: var(--shiki-light);
47
+ }
48
+
49
+ :global([data-theme="dark"]) .content :global(pre) {
50
+ background-color: var(--shiki-dark-bg, #24292e);
51
+ }
52
+
53
+ :global([data-theme="dark"]) .content :global(pre code span) {
54
+ color: var(--shiki-dark);
55
+ }
56
+
57
+ .content img {
58
+ max-width: 100%;
59
+ height: auto;
60
+ }
61
+
62
+ .content table {
63
+ display: block;
64
+ max-width: 100%;
65
+ overflow-x: auto;
66
+ margin-bottom: var(--rs-space-5);
67
+ }
68
+
69
+ .content details {
70
+ border: 1px solid var(--rs-color-border-base-primary);
71
+ border-radius: var(--rs-radius-2);
72
+ margin: var(--rs-space-5) 0;
73
+ overflow: hidden;
74
+ }
75
+
76
+ .content details summary {
77
+ padding: var(--rs-space-4) var(--rs-space-5);
78
+ cursor: pointer;
79
+ font-weight: 500;
80
+ font-size: var(--rs-font-size-small);
81
+ color: var(--rs-color-text-base-primary);
82
+ background: var(--rs-color-background-base-secondary);
83
+ list-style: none;
84
+ display: flex;
85
+ align-items: center;
86
+ gap: var(--rs-space-3);
87
+ }
88
+
89
+ .content details summary::-webkit-details-marker {
90
+ display: none;
91
+ }
92
+
93
+ .content details summary::before {
94
+ content: '▶';
95
+ font-size: 10px;
96
+ transition: transform 0.2s ease;
97
+ color: var(--rs-color-text-base-secondary);
98
+ }
99
+
100
+ .content details[open] > summary::before {
101
+ transform: rotate(90deg);
102
+ }
103
+
104
+ .content details > :not(summary) {
105
+ padding: var(--rs-space-4) var(--rs-space-5);
106
+ }
@@ -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,4 @@
1
+ export const inter = {
2
+ className: 'chronicle-inter',
3
+ style: { fontFamily: "'Inter', system-ui, -apple-system, sans-serif" },
4
+ }
@@ -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,95 @@
1
+ 'use client'
2
+
3
+ import { useLocation, Link } from 'react-router-dom'
4
+ import { MethodBadge } from '@/components/api/method-badge'
5
+ import type { PageTree, PageTreeItem } from '@/types'
6
+ import styles from './ChapterNav.module.css'
7
+
8
+ const iconMap: Record<string, React.ReactNode> = {
9
+ 'method-get': <MethodBadge method="GET" size="micro" />,
10
+ 'method-post': <MethodBadge method="POST" size="micro" />,
11
+ 'method-put': <MethodBadge method="PUT" size="micro" />,
12
+ 'method-delete': <MethodBadge method="DELETE" size="micro" />,
13
+ 'method-patch': <MethodBadge method="PATCH" size="micro" />,
14
+ }
15
+
16
+ interface ChapterNavProps {
17
+ tree: PageTree
18
+ }
19
+
20
+ function buildChapterIndices(children: PageTreeItem[]): Map<PageTreeItem, number> {
21
+ const indices = new Map<PageTreeItem, number>()
22
+ let index = 0
23
+ for (const item of children) {
24
+ if (item.type === 'folder' && item.children) {
25
+ index++
26
+ indices.set(item, index)
27
+ }
28
+ }
29
+ return indices
30
+ }
31
+
32
+ export function ChapterNav({ tree }: ChapterNavProps) {
33
+ const { pathname } = useLocation()
34
+ const chapterIndices = buildChapterIndices(tree.children)
35
+
36
+ return (
37
+ <nav className={styles.nav}>
38
+ <ul className={styles.chapterItems}>
39
+ {tree.children.map((item) => {
40
+ if (item.type === 'separator') return null
41
+
42
+ if (item.type === 'folder' && item.children) {
43
+ const chapterIndex = chapterIndices.get(item) ?? 0
44
+ return (
45
+ <li key={item.name} className={styles.chapter}>
46
+ <span className={styles.chapterLabel}>
47
+ {String(chapterIndex).padStart(2, '0')}. {item.name}
48
+ </span>
49
+ <ul className={styles.chapterItems}>
50
+ {item.children.map((child) => (
51
+ <ChapterItem key={child.url ?? child.name} item={child} pathname={pathname} />
52
+ ))}
53
+ </ul>
54
+ </li>
55
+ )
56
+ }
57
+
58
+ return <ChapterItem key={item.url ?? item.name} item={item} pathname={pathname} />
59
+ })}
60
+ </ul>
61
+ </nav>
62
+ )
63
+ }
64
+
65
+ function ChapterItem({ item, pathname }: { item: PageTreeItem; pathname: string }) {
66
+ if (item.type === 'separator') return null
67
+
68
+ if (item.type === 'folder' && item.children) {
69
+ return (
70
+ <li>
71
+ <span className={styles.subLabel}>{item.name}</span>
72
+ <ul className={styles.chapterItems}>
73
+ {item.children.map((child) => (
74
+ <ChapterItem key={child.url ?? child.name} item={child} pathname={pathname} />
75
+ ))}
76
+ </ul>
77
+ </li>
78
+ )
79
+ }
80
+
81
+ const isActive = pathname === item.url
82
+ const icon = item.icon ? iconMap[item.icon] : null
83
+
84
+ return (
85
+ <li>
86
+ <Link
87
+ to={item.url ?? '#'}
88
+ className={`${styles.link} ${isActive ? styles.active : ''}`}
89
+ >
90
+ {icon && <span className={styles.icon}>{icon}</span>}
91
+ <span>{item.name}</span>
92
+ </Link>
93
+ </li>
94
+ )
95
+ }
@@ -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
+ }