@raystack/chronicle 0.1.0-canary.1f5227c

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 (124) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +543 -0
  3. package/package.json +68 -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 +52 -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 +174 -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/dev.ts +156 -0
  87. package/src/server/entry-client.tsx +74 -0
  88. package/src/server/entry-prod.ts +127 -0
  89. package/src/server/entry-server.tsx +35 -0
  90. package/src/server/handlers/apis-proxy.ts +52 -0
  91. package/src/server/handlers/health.ts +3 -0
  92. package/src/server/handlers/llms.ts +58 -0
  93. package/src/server/handlers/og.ts +87 -0
  94. package/src/server/handlers/robots.ts +11 -0
  95. package/src/server/handlers/search.ts +140 -0
  96. package/src/server/handlers/sitemap.ts +39 -0
  97. package/src/server/handlers/specs.ts +9 -0
  98. package/src/server/index.html +12 -0
  99. package/src/server/prod.ts +18 -0
  100. package/src/server/router.ts +42 -0
  101. package/src/server/vite-config.ts +71 -0
  102. package/src/themes/default/Layout.module.css +81 -0
  103. package/src/themes/default/Layout.tsx +132 -0
  104. package/src/themes/default/Page.module.css +102 -0
  105. package/src/themes/default/Page.tsx +21 -0
  106. package/src/themes/default/Toc.module.css +48 -0
  107. package/src/themes/default/Toc.tsx +66 -0
  108. package/src/themes/default/font.ts +4 -0
  109. package/src/themes/default/index.ts +13 -0
  110. package/src/themes/paper/ChapterNav.module.css +71 -0
  111. package/src/themes/paper/ChapterNav.tsx +95 -0
  112. package/src/themes/paper/Layout.module.css +33 -0
  113. package/src/themes/paper/Layout.tsx +25 -0
  114. package/src/themes/paper/Page.module.css +174 -0
  115. package/src/themes/paper/Page.tsx +106 -0
  116. package/src/themes/paper/ReadingProgress.module.css +132 -0
  117. package/src/themes/paper/ReadingProgress.tsx +294 -0
  118. package/src/themes/paper/index.ts +8 -0
  119. package/src/themes/registry.ts +14 -0
  120. package/src/types/config.ts +80 -0
  121. package/src/types/content.ts +36 -0
  122. package/src/types/index.ts +3 -0
  123. package/src/types/theme.ts +22 -0
  124. package/tsconfig.json +29 -0
@@ -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
+ }
@@ -0,0 +1,174 @@
1
+ .main {
2
+ --paper-navbar-height: 40px;
3
+ --paper-navbar-padding: var(--rs-space-3);
4
+ --paper-navbar-total: calc(var(--paper-navbar-height) + var(--paper-navbar-padding) * 2 + 1px);
5
+
6
+ flex: 1;
7
+ max-width: 1024px;
8
+ margin: 0 auto;
9
+ }
10
+
11
+ .navbar {
12
+ height: var(--paper-navbar-height);
13
+ padding: var(--paper-navbar-padding) 0;
14
+ border-bottom: 1px solid var(--rs-color-border-base-primary);
15
+ justify-content: space-between;
16
+ width: 100%;
17
+ position: fixed;
18
+ top: 0;
19
+ background: var(--rs-color-background-neutral-primary);
20
+ z-index: 10;
21
+ max-width: 1024px;
22
+ }
23
+
24
+ .navLeft {
25
+ align-items: center;
26
+ }
27
+
28
+ .navRight {
29
+ align-items: center;
30
+ }
31
+
32
+ .arrow {
33
+ display: flex;
34
+ align-items: center;
35
+ color: var(--rs-color-foreground-base-primary);
36
+ text-decoration: none;
37
+ }
38
+
39
+ .arrow:hover {
40
+ color: var(--rs-color-foreground-accent-primary);
41
+ }
42
+
43
+ .arrowDisabled {
44
+ display: flex;
45
+ align-items: center;
46
+ color: var(--rs-color-foreground-base-tertiary);
47
+ opacity: 0.4;
48
+ cursor: default;
49
+ border: none;
50
+ background: none;
51
+ padding: 0;
52
+ }
53
+
54
+ .breadcrumb {
55
+ font-family: 'SF Mono', 'Fira Code', monospace;
56
+ font-size: var(--rs-font-size-small);
57
+ text-transform: uppercase;
58
+ letter-spacing: 0.05em;
59
+ margin-left: var(--rs-space-3);
60
+ }
61
+
62
+ .separator {
63
+ margin: 0 var(--rs-space-2);
64
+ color: var(--rs-color-foreground-base-tertiary);
65
+ }
66
+
67
+ .crumbLink {
68
+ color: var(--rs-color-foreground-base-tertiary);
69
+ text-decoration: none;
70
+ }
71
+
72
+ .crumbLink:hover {
73
+ color: var(--rs-color-foreground-base-primary);
74
+ }
75
+
76
+ .crumbActive {
77
+ color: var(--rs-color-foreground-base-primary);
78
+ font-weight: 600;
79
+ }
80
+
81
+ .article {
82
+ flex: 1;
83
+ min-width: 0;
84
+ margin-top: var(--paper-navbar-total);
85
+ padding: 0 var(--rs-space-7);
86
+ }
87
+
88
+ .searchButton {
89
+ height: 28px;
90
+ padding: 0 var(--rs-space-3);
91
+ font-size: var(--rs-font-size-small);
92
+ border: none;
93
+ box-shadow: none;
94
+ }
95
+
96
+ .content {
97
+ font-family: Georgia, 'Times New Roman', serif;
98
+ line-height: 1.8;
99
+ background: var(--rs-color-background-base-primary);
100
+ padding: var(--rs-space-9);
101
+ border-left: 1px solid var(--rs-color-border-base-primary);
102
+ border-right: 1px solid var(--rs-color-border-base-primary);
103
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04);
104
+ margin-bottom: var(--rs-space-9);
105
+ }
106
+
107
+ .content h1,
108
+ .content h2,
109
+ .content h3,
110
+ .content h4,
111
+ .content h5,
112
+ .content h6 {
113
+ line-height: 1.4;
114
+ }
115
+
116
+ .content h1 {
117
+ margin: 2rem 0 1rem;
118
+ font-size: 2rem;
119
+ }
120
+
121
+ .content h2 {
122
+ margin: 1.75rem 0 0.75rem;
123
+ font-size: 1.5rem;
124
+ }
125
+
126
+ .content h3 {
127
+ margin: 1.5rem 0 0.5rem;
128
+ font-size: 1.25rem;
129
+ }
130
+
131
+ .content h4 {
132
+ margin: 1.25rem 0 0.5rem;
133
+ font-size: 1.1rem;
134
+ }
135
+
136
+ .content h5 {
137
+ margin: 1rem 0 0.5rem;
138
+ font-size: 1rem;
139
+ }
140
+
141
+ .content h6 {
142
+ margin: 1rem 0 0.5rem;
143
+ font-size: 0.875rem;
144
+ }
145
+
146
+ .content p {
147
+ margin: 0.75rem 0;
148
+ }
149
+
150
+ .content ul,
151
+ .content ol {
152
+ margin: 0.75rem 0;
153
+ padding-left: 1.5rem;
154
+ margin-bottom: var(--rs-space-5);
155
+ }
156
+
157
+ .content li {
158
+ font-size: var(--rs-font-size-regular);
159
+ margin: var(--rs-space-2) 0;
160
+ }
161
+
162
+ .content table {
163
+ margin-bottom: var(--rs-space-5);
164
+ }
165
+
166
+ .content [role="tablist"] {
167
+ margin-bottom: var(--rs-space-3);
168
+ }
169
+
170
+ .content blockquote {
171
+ margin: 1rem 0;
172
+ padding-left: 1rem;
173
+ border-left: 3px solid var(--rs-color-border-base-primary);
174
+ }
@@ -0,0 +1,106 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import { useLocation, Link } from 'react-router-dom'
5
+ import { Flex } from '@raystack/apsara'
6
+ import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'
7
+ import type { ThemePageProps, PageTreeItem } from '@/types'
8
+ import { Search } from '@/components/ui/search'
9
+ import { ReadingProgress } from './ReadingProgress'
10
+ import styles from './Page.module.css'
11
+
12
+ function flattenTree(items: PageTreeItem[]): PageTreeItem[] {
13
+ const result: PageTreeItem[] = []
14
+ for (const item of items) {
15
+ if (item.type === 'page' && item.url) result.push(item)
16
+ if (item.children) result.push(...flattenTree(item.children))
17
+ }
18
+ return result
19
+ }
20
+
21
+ function findBreadcrumb(items: PageTreeItem[], slug: string[]): { label: string; href: string }[] {
22
+ const result: { label: string; href: string }[] = []
23
+ for (let i = 0; i < slug.length; i++) {
24
+ const path = '/' + slug.slice(0, i + 1).join('/')
25
+ const found = findInTree(items, path)
26
+ result.push({ label: found?.name ?? slug[i], href: path })
27
+ }
28
+ return result
29
+ }
30
+
31
+ function findInTree(items: PageTreeItem[], path: string): PageTreeItem | undefined {
32
+ for (const item of items) {
33
+ if (item.url === path) return item
34
+ if (item.children) {
35
+ const found = findInTree(item.children, path)
36
+ if (found) return found
37
+ }
38
+ }
39
+ return undefined
40
+ }
41
+
42
+ export function Page({ page, config, tree }: ThemePageProps) {
43
+ const { pathname } = useLocation()
44
+
45
+ const { prev, next, crumbs } = useMemo(() => {
46
+ const pages = flattenTree(tree.children)
47
+ const currentIndex = pages.findIndex((p) => p.url === pathname)
48
+ return {
49
+ prev: currentIndex > 0 ? pages[currentIndex - 1] : null,
50
+ next: currentIndex < pages.length - 1 ? pages[currentIndex + 1] : null,
51
+ crumbs: findBreadcrumb(tree.children, page.slug),
52
+ }
53
+ }, [tree, pathname, page.slug])
54
+
55
+ return (
56
+ <>
57
+ <main className={styles.main}>
58
+ <Flex align="center" className={styles.navbar}>
59
+ <Flex align="center" gap="small" className={styles.navLeft}>
60
+ {prev ? (
61
+ <Link to={prev.url!} className={styles.arrow} aria-label="Previous page">
62
+ <ChevronLeftIcon width={14} height={14} />
63
+ </Link>
64
+ ) : (
65
+ <button disabled className={styles.arrowDisabled} aria-label="Previous page">
66
+ <ChevronLeftIcon width={14} height={14} />
67
+ </button>
68
+ )}
69
+ {next ? (
70
+ <Link to={next.url!} className={styles.arrow} aria-label="Next page">
71
+ <ChevronRightIcon width={14} height={14} />
72
+ </Link>
73
+ ) : (
74
+ <button disabled className={styles.arrowDisabled} aria-label="Next page">
75
+ <ChevronRightIcon width={14} height={14} />
76
+ </button>
77
+ )}
78
+ <nav className={styles.breadcrumb}>
79
+ {crumbs.map((crumb, i) => (
80
+ <span key={crumb.href}>
81
+ {i > 0 && <span className={styles.separator}>/</span>}
82
+ {i === crumbs.length - 1 ? (
83
+ <span className={styles.crumbActive}>{crumb.label}</span>
84
+ ) : (
85
+ <Link to={crumb.href} className={styles.crumbLink}>
86
+ {crumb.label}
87
+ </Link>
88
+ )}
89
+ </span>
90
+ ))}
91
+ </nav>
92
+ </Flex>
93
+ <Flex align="center" className={styles.navRight}>
94
+ {config.search?.enabled && <Search className={styles.searchButton} />}
95
+ </Flex>
96
+ </Flex>
97
+ <article className={styles.article} data-article-content>
98
+ <div className={styles.content}>
99
+ {page.content}
100
+ </div>
101
+ </article>
102
+ </main>
103
+ <ReadingProgress items={page.toc} />
104
+ </>
105
+ )
106
+ }