@raystack/chronicle 0.5.3 → 0.6.0

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 (74) hide show
  1. package/dist/cli/index.js +260 -81
  2. package/package.json +8 -6
  3. package/src/cli/commands/build.ts +5 -8
  4. package/src/cli/commands/dev.ts +5 -6
  5. package/src/cli/commands/init.test.ts +77 -0
  6. package/src/cli/commands/init.ts +73 -40
  7. package/src/cli/commands/serve.ts +6 -9
  8. package/src/cli/commands/start.ts +5 -5
  9. package/src/cli/utils/config.ts +6 -12
  10. package/src/cli/utils/scaffold.test.ts +179 -0
  11. package/src/cli/utils/scaffold.ts +70 -9
  12. package/src/components/api/field-row.tsx +1 -1
  13. package/src/components/api/field-section.tsx +2 -2
  14. package/src/components/mdx/index.tsx +1 -1
  15. package/src/components/mdx/mermaid.tsx +24 -21
  16. package/src/components/ui/breadcrumbs.tsx +4 -2
  17. package/src/components/ui/client-theme-switcher.tsx +21 -4
  18. package/src/components/ui/search.module.css +16 -41
  19. package/src/components/ui/search.tsx +30 -41
  20. package/src/lib/config.test.ts +493 -0
  21. package/src/lib/config.ts +123 -22
  22. package/src/lib/head.tsx +23 -5
  23. package/src/lib/llms.test.ts +94 -0
  24. package/src/lib/llms.ts +41 -0
  25. package/src/lib/navigation.test.ts +94 -0
  26. package/src/lib/navigation.ts +51 -0
  27. package/src/lib/page-context.tsx +79 -32
  28. package/src/lib/route-resolver.test.ts +173 -0
  29. package/src/lib/route-resolver.ts +73 -0
  30. package/src/lib/source.ts +94 -1
  31. package/src/lib/version-source.test.ts +163 -0
  32. package/src/lib/version-source.ts +101 -0
  33. package/src/pages/ApiPage.tsx +1 -1
  34. package/src/pages/DocsLayout.tsx +24 -3
  35. package/src/pages/DocsPage.tsx +7 -7
  36. package/src/pages/LandingPage.module.css +56 -0
  37. package/src/pages/LandingPage.tsx +39 -0
  38. package/src/pages/NotFound.module.css +3 -0
  39. package/src/pages/NotFound.tsx +9 -12
  40. package/src/server/App.tsx +21 -23
  41. package/src/server/api/{page/[...slug].ts → page.ts} +7 -3
  42. package/src/server/api/search.ts +51 -24
  43. package/src/server/api/specs.ts +17 -5
  44. package/src/server/entry-client.tsx +42 -14
  45. package/src/server/entry-server.tsx +35 -13
  46. package/src/server/plugins/telemetry.ts +47 -7
  47. package/src/server/routes/[...slug].md.ts +0 -6
  48. package/src/server/routes/[version]/llms.txt.ts +26 -0
  49. package/src/server/routes/llms.txt.ts +10 -13
  50. package/src/server/routes/og.tsx +2 -2
  51. package/src/server/routes/sitemap.xml.ts +14 -6
  52. package/src/server/vite-config.ts +5 -5
  53. package/src/themes/default/ContentDirButtons.tsx +66 -0
  54. package/src/themes/default/Layout.module.css +187 -40
  55. package/src/themes/default/Layout.tsx +166 -65
  56. package/src/themes/default/OpenInAI.tsx +112 -0
  57. package/src/themes/default/Page.module.css +30 -0
  58. package/src/themes/default/Page.tsx +1 -3
  59. package/src/themes/default/SidebarLogo.tsx +26 -0
  60. package/src/themes/default/Toc.module.css +102 -25
  61. package/src/themes/default/Toc.tsx +56 -10
  62. package/src/themes/default/VersionSwitcher.tsx +59 -0
  63. package/src/themes/paper/ContentDirDropdown.tsx +47 -0
  64. package/src/themes/paper/Layout.module.css +7 -0
  65. package/src/themes/paper/Layout.tsx +20 -13
  66. package/src/themes/paper/VersionSwitcher.tsx +60 -0
  67. package/src/types/config.ts +146 -23
  68. package/src/types/content.ts +11 -1
  69. package/src/types/theme.ts +1 -0
  70. package/src/components/ui/footer.module.css +0 -27
  71. package/src/components/ui/footer.tsx +0 -30
  72. package/src/server/api/metrics.ts +0 -23
  73. package/src/server/api/page/index.ts +0 -1
  74. package/src/server/telemetry.ts +0 -49
@@ -18,7 +18,6 @@ function resolveOutputDir(projectRoot: string, preset?: string): string {
18
18
  export interface ViteConfigOptions {
19
19
  packageRoot: string;
20
20
  projectRoot: string;
21
- contentDir: string;
22
21
  configPath?: string;
23
22
  preset?: string;
24
23
  }
@@ -41,8 +40,9 @@ async function readChronicleConfig(projectRoot: string, configPath?: string): Pr
41
40
  export async function createViteConfig(
42
41
  options: ViteConfigOptions
43
42
  ): Promise<InlineConfig> {
44
- const { packageRoot, projectRoot, contentDir, configPath, preset } = options;
43
+ const { packageRoot, projectRoot, configPath, preset } = options;
45
44
  const rawConfig = await readChronicleConfig(projectRoot, configPath);
45
+ const contentMirror = path.resolve(packageRoot, '.content');
46
46
 
47
47
  return {
48
48
  root: packageRoot,
@@ -86,8 +86,8 @@ export async function createViteConfig(
86
86
  resolve: {
87
87
  alias: {
88
88
  '@': path.resolve(packageRoot, 'src'),
89
+ 'tslib': 'tslib/tslib.es6.js',
89
90
  },
90
- conditions: ['module-sync', 'import', 'node'],
91
91
  dedupe: [
92
92
  'react',
93
93
  'react-dom',
@@ -98,11 +98,11 @@ export async function createViteConfig(
98
98
  },
99
99
  server: {
100
100
  fs: {
101
- allow: [packageRoot, projectRoot, contentDir]
101
+ allow: [packageRoot, projectRoot, contentMirror]
102
102
  }
103
103
  },
104
104
  define: {
105
- __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentDir),
105
+ __CHRONICLE_CONTENT_DIR__: JSON.stringify(contentMirror),
106
106
  __CHRONICLE_PROJECT_ROOT__: JSON.stringify(projectRoot),
107
107
  __CHRONICLE_CONFIG_RAW__: JSON.stringify(rawConfig),
108
108
  },
@@ -0,0 +1,66 @@
1
+ import { ChevronDownIcon } from '@heroicons/react/24/outline';
2
+ import { Button, Menu, Flex } from '@raystack/apsara';
3
+ import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
4
+ import { getLandingEntries } from '@/lib/config';
5
+ import { getActiveContentDir, splitContentButtons } from '@/lib/navigation';
6
+ import { usePageContext } from '@/lib/page-context';
7
+
8
+ const MAX_VISIBLE = 3;
9
+
10
+ export function ContentDirButtons() {
11
+ const { config, version } = usePageContext();
12
+ const { pathname } = useLocation();
13
+ const navigate = useNavigate();
14
+
15
+ const entries = getLandingEntries(config, version.dir);
16
+ if (entries.length <= 1) return null;
17
+
18
+ const active = getActiveContentDir(pathname, config);
19
+ const { visible, overflow } = splitContentButtons(entries, MAX_VISIBLE);
20
+
21
+ return (
22
+ <Flex gap='small' align='center'>
23
+ {visible.map(entry => (
24
+ <RouterLink
25
+ key={entry.href}
26
+ to={entry.href}
27
+ style={{ textDecoration: 'none' }}
28
+ >
29
+ <Button
30
+ size='small'
31
+ variant={active === entry.contentDir ? 'solid' : 'outline'}
32
+ color='neutral'
33
+ >
34
+ {entry.label}
35
+ </Button>
36
+ </RouterLink>
37
+ ))}
38
+ {overflow.length > 0 ? (
39
+ <Menu>
40
+ <Menu.Trigger
41
+ render={
42
+ <Button
43
+ size='small'
44
+ variant='outline'
45
+ color='neutral'
46
+ trailingIcon={<ChevronDownIcon width={14} height={14} />}
47
+ />
48
+ }
49
+ >
50
+ More
51
+ </Menu.Trigger>
52
+ <Menu.Content>
53
+ {overflow.map(entry => (
54
+ <Menu.Item
55
+ key={entry.href}
56
+ onClick={() => navigate(entry.href)}
57
+ >
58
+ {entry.label}
59
+ </Menu.Item>
60
+ ))}
61
+ </Menu.Content>
62
+ </Menu>
63
+ ) : null}
64
+ </Flex>
65
+ );
66
+ }
@@ -1,79 +1,226 @@
1
1
  .layout {
2
+ --navbar-height: 48px;
2
3
  min-height: 100vh;
3
4
  }
4
5
 
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
6
  .body {
14
7
  flex: 1;
15
8
  }
16
9
 
17
10
  .sidebar {
18
- width: 260px;
11
+ width: 262px;
12
+ flex: 0 0 262px;
13
+ display: flex;
14
+ flex-direction: column;
19
15
  position: sticky;
20
16
  top: 0;
21
17
  height: 100vh;
18
+ background: var(--rs-color-background-base-secondary);
22
19
  }
23
20
 
24
- .content {
25
- flex: 1;
26
- padding: var(--rs-space-9);
21
+ .sidebarLogo {
22
+ width: 28px;
23
+ height: 28px;
24
+ object-fit: contain;
27
25
  }
28
26
 
29
- .sidebarList {
30
- list-style: none;
27
+ .configIcon {
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ width: 16px;
32
+ height: 16px;
33
+ color: currentColor;
34
+ }
35
+
36
+ .configIcon svg {
37
+ width: 100%;
38
+ height: 100%;
39
+ display: block;
40
+ }
41
+
42
+ .sidebar[data-position='left'] {
43
+ border-right: none;
31
44
  padding: 0;
32
- margin: 0;
33
45
  }
34
46
 
35
- .separator {
36
- height: 1px;
37
- background: var(--rs-color-border-base-primary);
38
- margin: var(--rs-space-3) 0;
47
+ .sidebarNavbar {
48
+ display: flex;
49
+ align-items: center;
50
+ justify-content: space-between;
51
+ gap: var(--rs-space-3);
52
+ width: 100%;
53
+ height: var(--navbar-height);
54
+ padding: 0 var(--rs-space-5);
55
+ flex-shrink: 0;
56
+ backdrop-filter: blur(1px);
57
+ }
58
+
59
+ .sidebarNavLogo {
60
+ color: var(--rs-color-foreground-base-primary);
61
+ }
62
+
63
+ .sidebarNavActions {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--rs-space-3);
67
+ }
68
+
69
+ .sidebarMain {
70
+ padding: var(--rs-space-7) var(--rs-space-5);
71
+ gap: 0;
72
+ min-height: 0;
73
+ overflow-y: auto;
74
+ scrollbar-width: none;
75
+ }
76
+
77
+ .topLinks {
78
+ width: 100%;
79
+ margin-bottom: var(--rs-space-7);
80
+ }
81
+
82
+ .topLinkItem {
83
+ color: var(--rs-color-foreground-base-tertiary);
84
+ }
85
+
86
+ .topLinkText {
87
+ color: var(--rs-color-foreground-base-tertiary);
88
+ }
89
+
90
+ .topLinkItem[data-active='true'] {
91
+ background: transparent;
92
+ color: var(--rs-color-foreground-base-primary);
93
+ }
94
+
95
+ .topLinkItem[data-active='true'] .topLinkText {
96
+ color: var(--rs-color-foreground-base-primary);
97
+ }
98
+
99
+ .sidebarFooter {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: var(--rs-space-3);
103
+ height: var(--navbar-height);
104
+ box-sizing: border-box;
105
+ padding: 0 var(--rs-space-5);
106
+ border-top: 0.5px solid var(--rs-color-border-base-primary);
107
+ background: var(--rs-color-background-base-secondary);
108
+ backdrop-filter: blur(7.5px);
109
+ }
110
+
111
+ .sidebarMain::-webkit-scrollbar {
112
+ display: none;
39
113
  }
40
114
 
41
- .folder {
42
- margin-bottom: var(--rs-space-3);
115
+ .sidebarHeader {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: space-between;
119
+ gap: var(--rs-space-3);
120
+ height: var(--navbar-height);
121
+ box-sizing: border-box;
122
+ margin-bottom: 0;
123
+ padding: 0 var(--rs-space-5);
124
+ background: var(--rs-color-background-base-secondary);
125
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
126
+ border-radius: 0;
127
+ backdrop-filter: blur(1px);
128
+ }
129
+
130
+ .mainArea {
131
+ flex: 1;
132
+ min-width: 0;
43
133
  }
44
134
 
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;
135
+ .cardWrapper {
136
+ flex: 1;
137
+ display: flex;
138
+ padding: 0 0 var(--rs-space-2) 0;
139
+ min-height: 0;
140
+ background: var(--rs-color-background-base-secondary);
141
+ }
142
+
143
+ .card {
144
+ flex: 1;
145
+ display: flex;
146
+ flex-direction: column;
147
+ border-left: 0.5px solid var(--rs-color-border-base-primary);
148
+ box-shadow: var(--rs-shadow-soft);
149
+ overflow: visible;
150
+ }
151
+
152
+ .subNav {
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: space-between;
156
+ height: var(--navbar-height);
157
+ padding: var(--rs-space-4) var(--rs-space-7);
158
+ background: var(--rs-color-background-base-primary);
159
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
160
+ backdrop-filter: blur(1px);
161
+ }
162
+
163
+ .subNavLeft {
164
+ min-width: 0;
165
+ }
166
+
167
+ .content {
168
+ flex: 1;
169
+ padding: var(--rs-space-9) var(--rs-space-7);
170
+ background: var(--rs-color-background-base-primary);
51
171
  }
52
172
 
53
- .folder > .sidebarList {
54
- margin-top: var(--rs-space-2);
173
+ .groupItems {
55
174
  padding-left: var(--rs-space-4);
175
+ padding-bottom: var(--rs-space-3);
176
+ gap: 0;
177
+ }
178
+
179
+ .navGroup {
180
+ margin-top: 0;
56
181
  }
57
182
 
58
- .navButton {
183
+ .navGroup[data-depth='0'] {
184
+ margin-top: var(--rs-space-7);
185
+ }
186
+
187
+ .navGroup .navGroupHeader {
188
+ margin: 0;
189
+ }
190
+
191
+ .navGroup[data-depth='1'] .navGroupHeader {
192
+ height: auto;
193
+ }
194
+
195
+ .navGroup[data-depth='1'] .navGroupLabel {
196
+ color: var(--rs-color-foreground-base-primary);
197
+ }
198
+
199
+ .navGroupTrigger {
59
200
  display: flex;
201
+ padding: var(--rs-space-3);
60
202
  align-items: center;
203
+ gap: var(--rs-space-3);
204
+ align-self: stretch;
205
+ width: 100%;
61
206
  height: 32px;
62
- padding: 0 var(--rs-space-4);
63
- border: 1px solid var(--rs-color-border-base-primary);
64
207
  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;
208
+ box-sizing: border-box;
69
209
  }
70
210
 
71
- .navButton:hover {
72
- background: var(--rs-color-background-base-primary-hover);
211
+ .navGroupLabel {
212
+ flex: 1;
213
+ text-align: left;
214
+ color: var(--rs-color-foreground-base-secondary);
215
+ font-family: var(--rs-font-body);
216
+ font-size: var(--rs-font-size-small);
217
+ font-weight: var(--rs-font-weight-medium);
218
+ line-height: var(--rs-line-height-small);
219
+ letter-spacing: var(--rs-letter-spacing-small);
73
220
  }
74
221
 
75
- .groupItems {
76
- padding-left: var(--rs-space-4);
222
+ .navGroupChevron {
223
+ margin-left: auto;
77
224
  }
78
225
 
79
226
  .page {
@@ -1,22 +1,28 @@
1
- import { RectangleStackIcon } from '@heroicons/react/24/outline';
2
1
  import {
3
- Button,
4
- Flex,
5
- Headline,
6
- Link,
7
- Navbar,
8
- Sidebar
9
- } from '@raystack/apsara';
2
+ ArrowLeftIcon,
3
+ ArrowRightIcon,
4
+ CodeBracketSquareIcon,
5
+ RectangleStackIcon,
6
+ DocumentTextIcon,
7
+ Squares2X2Icon
8
+ } from '@heroicons/react/24/outline';
9
+ import { Flex, IconButton, Sidebar } from '@raystack/apsara';
10
10
  import { cx } from 'class-variance-authority';
11
- import { useEffect, useRef } from 'react';
12
- import { Link as RouterLink, useLocation } from 'react-router';
11
+ import { useEffect, useMemo, useRef } from 'react';
12
+ import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
13
13
  import { MethodBadge } from '@/components/api/method-badge';
14
14
  import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
15
- import { Footer } from '@/components/ui/footer';
16
15
  import { Search } from '@/components/ui/search';
16
+ import { Breadcrumbs } from '@/components/ui/breadcrumbs';
17
+ import { getLandingEntries } from '@/lib/config';
18
+ import { getActiveContentDir } from '@/lib/navigation';
19
+ import { usePageContext } from '@/lib/page-context';
17
20
  import type { Node } from 'fumadocs-core/page-tree';
18
21
  import type { ThemeLayoutProps } from '@/types';
19
22
  import styles from './Layout.module.css';
23
+ import { OpenInAI } from './OpenInAI';
24
+ import { SidebarLogo } from './SidebarLogo';
25
+ import { VersionSwitcher } from './VersionSwitcher';
20
26
 
21
27
  const iconMap: Record<string, React.ReactNode> = {
22
28
  'rectangle-stack': <RectangleStackIcon width={16} height={16} />,
@@ -27,16 +33,51 @@ const iconMap: Record<string, React.ReactNode> = {
27
33
  'method-patch': <MethodBadge method='PATCH' size='micro' />
28
34
  };
29
35
 
36
+ function renderConfigIcon(
37
+ icon: string | undefined,
38
+ alt: string,
39
+ fallback: React.ReactNode
40
+ ): React.ReactNode {
41
+ if (!icon) return fallback;
42
+ if (icon.trim().startsWith('<svg')) {
43
+ return (
44
+ <span
45
+ aria-label={alt}
46
+ className={styles.configIcon}
47
+ dangerouslySetInnerHTML={{ __html: icon }}
48
+ />
49
+ );
50
+ }
51
+ return <img src={icon} alt={alt} className={styles.configIcon} />;
52
+ }
53
+
30
54
  let savedScrollTop = 0;
31
55
 
32
56
  export function Layout({
33
57
  children,
34
58
  config,
35
59
  tree,
60
+ hideSidebar,
36
61
  classNames
37
62
  }: ThemeLayoutProps) {
38
63
  const { pathname } = useLocation();
64
+ const navigate = useNavigate();
65
+ const { page, version } = usePageContext();
39
66
  const scrollRef = useRef<HTMLDivElement>(null);
67
+ const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/');
68
+ const isApiBase = (basePath: string) =>
69
+ pathname === basePath || pathname.startsWith(`${basePath}/`);
70
+ const { prev, next } = page ?? { prev: null, next: null };
71
+
72
+ const contentEntries = getLandingEntries(config, version.dir);
73
+ const activeContentDir = getActiveContentDir(pathname, config);
74
+ const apiEntries = config.api ?? [];
75
+ const showTopLinks = contentEntries.length + apiEntries.length > 1;
76
+
77
+ const slug = useMemo(
78
+ () => (pathname === '/' ? [] : pathname.split('/').filter(Boolean)),
79
+ [pathname]
80
+ );
40
81
 
41
82
  useEffect(() => {
42
83
  const el = scrollRef.current;
@@ -58,87 +99,147 @@ export function Layout({
58
99
 
59
100
  return (
60
101
  <Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
61
- <Navbar className={styles.header}>
62
- <Navbar.Start>
63
- <RouterLink
64
- to='/'
65
- style={{ textDecoration: 'none', color: 'inherit' }}
66
- >
67
- <Headline size='small' weight='medium' as='h1'>
68
- {config.title}
69
- </Headline>
70
- </RouterLink>
71
- </Navbar.Start>
72
- <Navbar.End>
73
- <Flex gap='medium' align='center' className={styles.navActions}>
74
- {config.api?.map(api => (
75
- <RouterLink
76
- key={api.basePath}
77
- to={api.basePath}
78
- className={styles.navButton}
79
- >
80
- {api.name} API
81
- </RouterLink>
82
- ))}
83
- {config.navigation?.links?.map(link => (
84
- <Link key={link.href} href={link.href}>
85
- {link.label}
86
- </Link>
87
- ))}
88
- {config.search?.enabled && <Search />}
89
- </Flex>
90
- <ClientThemeSwitcher size={16} />
91
- </Navbar.End>
92
- </Navbar>
93
102
  <Flex className={cx(styles.body, classNames?.body)}>
94
- <Sidebar
95
- defaultOpen
96
- collapsible={false}
97
- className={cx(styles.sidebar, classNames?.sidebar)}
98
- >
99
- <Sidebar.Main ref={scrollRef}>
100
- {tree.children.map((item, i) => (
101
- <SidebarNode
102
- key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
103
- item={item}
104
- pathname={pathname}
105
- />
106
- ))}
107
- </Sidebar.Main>
108
- </Sidebar>
109
- <main className={cx(styles.content, classNames?.content)}>
110
- {children}
111
- </main>
103
+ {hideSidebar ? null : (
104
+ <Sidebar
105
+ defaultOpen
106
+ collapsible={false}
107
+ className={cx(styles.sidebar, classNames?.sidebar)}
108
+ >
109
+ <Sidebar.Header className={styles.sidebarHeader}>
110
+ <SidebarLogo config={config} />
111
+ <Flex gap='small' align='center'>
112
+ {config.search?.enabled && <Search />}
113
+ <ClientThemeSwitcher size={16} />
114
+ </Flex>
115
+ </Sidebar.Header>
116
+ <Sidebar.Main ref={scrollRef} className={styles.sidebarMain}>
117
+ {showTopLinks ? (
118
+ <div className={styles.topLinks}>
119
+ {contentEntries.map(entry => (
120
+ <Sidebar.Item
121
+ key={entry.href}
122
+ href={entry.href}
123
+ active={activeContentDir === entry.contentDir}
124
+ leadingIcon={renderConfigIcon(
125
+ entry.icon,
126
+ entry.label,
127
+ <DocumentTextIcon width={16} height={16} />
128
+ )}
129
+ classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
130
+ render={<RouterLink to={entry.href} />}
131
+ >
132
+ {entry.label}
133
+ </Sidebar.Item>
134
+ ))}
135
+ {apiEntries.map(api => (
136
+ <Sidebar.Item
137
+ key={api.basePath}
138
+ href={api.basePath}
139
+ active={isApiBase(api.basePath)}
140
+ leadingIcon={renderConfigIcon(
141
+ api.icon,
142
+ api.name,
143
+ <CodeBracketSquareIcon width={16} height={16} />
144
+ )}
145
+ classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
146
+ render={<RouterLink to={api.basePath} />}
147
+ >
148
+ {api.name} API
149
+ </Sidebar.Item>
150
+ ))}
151
+ </div>
152
+ ) : null}
153
+ {tree.children.map((item, i) => (
154
+ <SidebarNode
155
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
156
+ item={item}
157
+ pathname={pathname}
158
+ />
159
+ ))}
160
+ </Sidebar.Main>
161
+ {config.versions?.length ? (
162
+ <Sidebar.Footer className={styles.sidebarFooter}>
163
+ <VersionSwitcher />
164
+ </Sidebar.Footer>
165
+ ) : null}
166
+ </Sidebar>
167
+ )}
168
+ <Flex direction='column' className={styles.mainArea}>
169
+ <div className={styles.cardWrapper}>
170
+ <div className={styles.card}>
171
+ <nav className={styles.subNav}>
172
+ <Flex align='center' gap='small' className={styles.subNavLeft}>
173
+ <Flex align='center' gap='extra-small'>
174
+ <IconButton
175
+ size={2}
176
+ disabled={!prev}
177
+ onClick={() => prev && navigate(prev.url)}
178
+ aria-label='Previous page'
179
+ >
180
+ <ArrowLeftIcon width={14} height={14} />
181
+ </IconButton>
182
+ <IconButton
183
+ size={2}
184
+ disabled={!next}
185
+ onClick={() => next && navigate(next.url)}
186
+ aria-label='Next page'
187
+ >
188
+ <ArrowRightIcon width={14} height={14} />
189
+ </IconButton>
190
+ </Flex>
191
+ {!isApiRoute && <Breadcrumbs slug={slug} tree={tree} />}
192
+ </Flex>
193
+ <OpenInAI />
194
+ </nav>
195
+ <main className={cx(styles.content, classNames?.content)}>
196
+ {children}
197
+ </main>
198
+ </div>
199
+ </div>
200
+ </Flex>
112
201
  </Flex>
113
- <Footer config={config.footer} />
114
202
  </Flex>
115
203
  );
116
204
  }
117
205
 
118
206
  function SidebarNode({
119
207
  item,
120
- pathname
208
+ pathname,
209
+ depth = 0
121
210
  }: {
122
211
  item: Node;
123
212
  pathname: string;
213
+ depth?: number;
124
214
  }) {
125
215
  if (item.type === 'separator') {
126
216
  return null;
127
217
  }
128
218
 
129
219
  if (item.type === 'folder') {
220
+ if (depth > 1) return null;
130
221
  const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
131
222
  return (
132
223
  <Sidebar.Group
224
+ className={styles.navGroup}
225
+ data-depth={depth}
133
226
  label={item.name?.toString() ?? ''}
134
227
  leadingIcon={icon ?? undefined}
135
- classNames={{ items: styles.groupItems }}
228
+ collapsible={depth === 1}
229
+ classNames={{
230
+ items: styles.groupItems,
231
+ header: styles.navGroupHeader,
232
+ trigger: styles.navGroupTrigger,
233
+ label: styles.navGroupLabel,
234
+ chevron: styles.navGroupChevron,
235
+ }}
136
236
  >
137
237
  {item.children.map((child, i) => (
138
238
  <SidebarNode
139
239
  key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
140
240
  item={child}
141
241
  pathname={pathname}
242
+ depth={depth + 1}
142
243
  />
143
244
  ))}
144
245
  </Sidebar.Group>
@@ -155,7 +256,7 @@ function SidebarNode({
155
256
  href={href}
156
257
  active={isActive}
157
258
  leadingIcon={icon ?? undefined}
158
- as={link}
259
+ render={link}
159
260
  >
160
261
  {item.name}
161
262
  </Sidebar.Item>