@raystack/chronicle 0.11.2 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raystack/chronicle",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -3,6 +3,7 @@
3
3
  border-radius: var(--rs-radius-2);
4
4
  overflow: hidden;
5
5
  width: 100%;
6
+ max-width: 100%;
6
7
  }
7
8
 
8
9
  .header {
@@ -20,4 +21,5 @@
20
21
 
21
22
  .body {
22
23
  background: var(--rs-color-background-base-primary);
24
+ overflow-x: auto;
23
25
  }
@@ -60,6 +60,16 @@
60
60
 
61
61
  .left,
62
62
  .right {
63
+ min-width: 0;
64
+ max-width: 100%;
63
65
  width: 100%;
64
66
  }
65
67
  }
68
+
69
+ @media (max-width: 768px) {
70
+ .layout {
71
+ gap: var(--rs-space-5);
72
+ padding-left: var(--rs-space-5);
73
+ padding-right: var(--rs-space-5);
74
+ }
75
+ }
@@ -1,10 +1,12 @@
1
1
  .dialogContent {
2
- border-radius: var(--rs-radius-4);
2
+ border-radius: var(--rs-radius-5);
3
3
  padding: 0px;
4
4
  width: 80%;
5
5
  max-width: 600px;
6
6
  position: fixed;
7
7
  top: 20%;
8
+ overflow: clip;
9
+ box-shadow: var(--rs-shadow-floating);
8
10
  }
9
11
 
10
12
  .input {
@@ -13,7 +15,8 @@
13
15
 
14
16
  .list {
15
17
  max-height: 400px;
16
- gap: var(--rs-space-3);
18
+ padding: 0 var(--rs-space-2);
19
+ gap: var(--rs-space-2);
17
20
  }
18
21
 
19
22
  .list :global([cmdk-group-heading]) {
@@ -25,35 +28,50 @@
25
28
  }
26
29
 
27
30
  .item {
28
- min-height: 40px;
29
- padding: var(--rs-space-3);
31
+ padding: var(--rs-space-5) var(--rs-space-4);
30
32
  gap: var(--rs-space-3);
31
- border-radius: var(--rs-radius-2);
33
+ border-radius: var(--rs-radius-3);
32
34
  cursor: pointer;
33
35
  }
34
36
 
35
-
36
37
  .item[data-selected="true"] {
37
38
  background: var(--rs-color-background-base-primary-hover);
38
39
  }
39
40
 
40
41
  .itemContent {
41
42
  display: flex;
42
- align-items: center;
43
- gap: 12px;
43
+ align-items: flex-start;
44
+ gap: var(--rs-space-3);
44
45
  flex: 1;
45
46
  }
46
47
 
47
- .sectionBadge {
48
- margin-left: auto;
49
- flex-shrink: 0;
50
- }
51
-
52
48
  .resultText {
53
49
  display: flex;
54
50
  flex-direction: column;
55
- gap: 2px;
51
+ gap: var(--rs-space-2);
56
52
  min-width: 0;
53
+ flex: 1;
54
+ }
55
+
56
+ .breadcrumb {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: var(--rs-space-2);
60
+ }
61
+
62
+ .breadcrumbText {
63
+ font-family: var(--rs-font-body);
64
+ font-size: var(--rs-font-size-small);
65
+ font-weight: var(--rs-font-weight-medium);
66
+ line-height: var(--rs-line-height-small);
67
+ letter-spacing: var(--rs-letter-spacing-small);
68
+ color: var(--rs-color-foreground-base-primary);
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .breadcrumbSeparator {
73
+ color: var(--rs-color-foreground-base-tertiary);
74
+ font-size: var(--rs-font-size-small);
57
75
  }
58
76
 
59
77
  .headingText {
@@ -64,10 +82,6 @@
64
82
  color: var(--rs-color-foreground-accent-primary-hover);
65
83
  }
66
84
 
67
- .separator {
68
- color: var(--rs-color-foreground-base-secondary);
69
- }
70
-
71
85
  .pageText {
72
86
  color: var(--rs-color-foreground-base-primary);
73
87
  }
@@ -77,14 +91,15 @@
77
91
  }
78
92
 
79
93
  .icon {
80
- width: 48px;
81
- height: 24px;
94
+ width: 16px;
95
+ height: 16px;
82
96
  color: var(--rs-color-foreground-base-secondary);
83
97
  flex-shrink: 0;
98
+ margin-top: 1px;
84
99
  }
85
100
 
86
101
  .itemContent :global([class*="badge-module"]) {
87
- min-width: 48px;
102
+ min-width: auto;
88
103
  justify-content: center;
89
104
  }
90
105
 
@@ -95,14 +110,15 @@
95
110
  .snippetText {
96
111
  font-size: var(--rs-font-size-mini);
97
112
  line-height: var(--rs-line-height-mini);
98
- color: var(--rs-color-foreground-base-tertiary);
113
+ letter-spacing: var(--rs-letter-spacing-mini);
114
+ color: var(--rs-color-foreground-base-secondary);
99
115
  overflow: hidden;
100
116
  text-overflow: ellipsis;
101
117
  white-space: nowrap;
102
118
  }
103
119
 
104
120
  .matchHighlight {
105
- color: var(--rs-color-foreground-accent-primary);
121
+ color: var(--rs-color-foreground-base-primary);
106
122
  font-weight: var(--rs-font-weight-medium);
107
123
  }
108
124
 
@@ -1,15 +1,18 @@
1
1
  import {
2
2
  DocumentIcon,
3
3
  HashtagIcon,
4
- MagnifyingGlassIcon
4
+ MagnifyingGlassIcon,
5
+ CodeBracketIcon,
6
+ ChevronRightIcon
5
7
  } from '@heroicons/react/24/outline';
6
- import { Badge, Command, IconButton, Text } from '@raystack/apsara';
8
+ import { Command, IconButton, Text } from '@raystack/apsara';
7
9
  import { keepPreviousData, useQuery } from '@tanstack/react-query';
8
10
  import { debounce } from 'lodash-es';
9
11
  import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react';
10
12
  import { useNavigate } from 'react-router';
11
13
  import { MethodBadge } from '@/components/api/method-badge';
12
14
  import { usePageContext } from '@/lib/page-context';
15
+ import { SearchMatchType } from '@/types';
13
16
  import styles from './search.module.css';
14
17
 
15
18
  interface SearchResult {
@@ -17,7 +20,7 @@ interface SearchResult {
17
20
  url: string;
18
21
  type: string;
19
22
  content: string;
20
- match?: 'title' | 'heading' | 'body';
23
+ match?: SearchMatchType;
21
24
  snippet?: string;
22
25
  section?: string;
23
26
  }
@@ -135,15 +138,7 @@ export function Search({ classNames }: SearchProps) {
135
138
  onClick={() => onSelect(result.url)}
136
139
  className={styles.item}
137
140
  >
138
- <div className={styles.itemContent}>
139
- {getResultIcon(result)}
140
- <Text className={styles.pageText}>
141
- <HighlightedText
142
- html={stripMethod(result.content)}
143
- />
144
- </Text>
145
- {result.section && <Badge size="small" className={styles.sectionBadge}>{result.section}</Badge>}
146
- </div>
141
+ <SearchResultItem result={result} query="" />
147
142
  </Command.Item>
148
143
  ))}
149
144
  </Command.Group>
@@ -156,25 +151,7 @@ export function Search({ classNames }: SearchProps) {
156
151
  onClick={() => onSelect(result.url)}
157
152
  className={styles.item}
158
153
  >
159
- <div className={styles.itemContent}>
160
- {getResultIcon(result)}
161
- <div className={styles.resultText}>
162
- <Text className={styles.pageText}>
163
- <HighlightQuery text={stripMethod(result.content)} query={search} />
164
- </Text>
165
- {result.snippet && result.match === 'heading' && (
166
- <Text className={styles.snippetText}>
167
- # <HighlightQuery text={result.snippet} query={search} />
168
- </Text>
169
- )}
170
- {result.snippet && result.match === 'body' && (
171
- <Text className={styles.snippetText}>
172
- <HighlightQuery text={result.snippet} query={search} />
173
- </Text>
174
- )}
175
- </div>
176
- {result.section && <Badge size="small" className={styles.sectionBadge}>{result.section}</Badge>}
177
- </div>
154
+ <SearchResultItem result={result} query={search} />
178
155
  </Command.Item>
179
156
  ))}
180
157
  </Command.Content>
@@ -185,6 +162,38 @@ export function Search({ classNames }: SearchProps) {
185
162
  );
186
163
  }
187
164
 
165
+ function SearchResultItem({ result, query }: { result: SearchResult; query: string }) {
166
+ const method = extractMethod(result.content);
167
+ const title = stripMethod(result.content);
168
+
169
+ return (
170
+ <div className={styles.itemContent}>
171
+ {getResultIcon(result)}
172
+ <div className={styles.resultText}>
173
+ <div className={styles.breadcrumb}>
174
+ {result.section && (
175
+ <>
176
+ <span className={styles.breadcrumbText}>{result.section}</span>
177
+ <ChevronRightIcon width={12} height={12} className={styles.breadcrumbSeparator} />
178
+ </>
179
+ )}
180
+ {method && <MethodBadge method={method} size='micro' />}
181
+ <Text className={styles.breadcrumbText}>
182
+ {query ? <HighlightQuery text={title} query={query} /> : <HighlightedText html={title} />}
183
+ </Text>
184
+ </div>
185
+ {result.snippet && (
186
+ <Text className={styles.snippetText}>
187
+ {query
188
+ ? <HighlightQuery text={result.snippet} query={query} />
189
+ : result.snippet}
190
+ </Text>
191
+ )}
192
+ </div>
193
+ </div>
194
+ );
195
+ }
196
+
188
197
  function deduplicateByUrl(results: SearchResult[]): SearchResult[] {
189
198
  const seen = new Set<string>();
190
199
  return results.filter(r => {
@@ -233,15 +242,13 @@ function HighlightQuery({ text, query }: { text: string; query: string }) {
233
242
  }
234
243
 
235
244
  function getResultIcon(result: SearchResult): React.ReactNode {
236
- if (!result.url.startsWith('/apis/')) {
237
- return result.type === 'page' ? (
238
- <DocumentIcon className={styles.icon} />
239
- ) : (
240
- <HashtagIcon className={styles.icon} />
241
- );
245
+ if (result.url.startsWith('/apis/')) {
246
+ return <CodeBracketIcon className={styles.icon} />;
242
247
  }
243
- const method = extractMethod(result.content);
244
- return method ? <MethodBadge method={method} size='micro' /> : null;
248
+ if (result.match === SearchMatchType.Heading) {
249
+ return <HashtagIcon className={styles.icon} />;
250
+ }
251
+ return <DocumentIcon className={styles.icon} />;
245
252
  }
246
253
 
247
254
  function getPageTitle(url: string): string {
@@ -7,6 +7,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
7
7
  import { loadApiSpecs } from '@/lib/openapi';
8
8
  import { extractFrontmatter, getPageSearchContent, getPagesForVersion } from '@/lib/source';
9
9
  import { LATEST_CONTEXT, type VersionContext } from '@/lib/version-source';
10
+ import { SearchResultType, SearchMatchType } from '@/types';
10
11
 
11
12
  interface SearchDocument {
12
13
  id: string;
@@ -14,7 +15,7 @@ interface SearchDocument {
14
15
  title: string;
15
16
  headings: string;
16
17
  body: string;
17
- type: 'page' | 'api';
18
+ type: SearchResultType;
18
19
  section: string;
19
20
  }
20
21
 
@@ -104,7 +105,7 @@ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
104
105
  title: fm.title,
105
106
  headings,
106
107
  body: [fm.description ?? '', body].join(' '),
107
- type: 'page',
108
+ type: SearchResultType.Page,
108
109
  section: entry?.label ?? dir ?? '',
109
110
  });
110
111
  }
@@ -128,7 +129,7 @@ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
128
129
  title: `${method.toUpperCase()} ${op.summary ?? opId}`,
129
130
  headings: op.summary ?? opId,
130
131
  body: [op.description ?? '', pathStr, method.toUpperCase()].join(' '),
131
- type: 'api',
132
+ type: SearchResultType.Api,
132
133
  section: spec.name,
133
134
  });
134
135
  }
@@ -144,9 +145,9 @@ function findMatch(
144
145
  title: string,
145
146
  headings: string,
146
147
  body: string,
147
- ): { match: 'title' | 'heading' | 'body'; snippet: string; slug?: string } {
148
+ ): { match: SearchMatchType; snippet: string; slug?: string } {
148
149
  if (title.toLowerCase().includes(query)) {
149
- return { match: 'title', snippet: title };
150
+ return { match: SearchMatchType.Title, snippet: title };
150
151
  }
151
152
 
152
153
  const slugger = new GithubSlugger();
@@ -154,7 +155,7 @@ function findMatch(
154
155
  for (const h of headingList) {
155
156
  const slug = slugger.slug(h);
156
157
  if (h.toLowerCase().includes(query)) {
157
- return { match: 'heading', snippet: h, slug };
158
+ return { match: SearchMatchType.Heading, snippet: h, slug };
158
159
  }
159
160
  }
160
161
 
@@ -163,10 +164,10 @@ function findMatch(
163
164
  const start = Math.max(0, idx - 40);
164
165
  const end = Math.min(body.length, idx + query.length + 80);
165
166
  const snippet = (start > 0 ? '...' : '') + body.slice(start, end).trim() + (end < body.length ? '...' : '');
166
- return { match: 'body', snippet };
167
+ return { match: SearchMatchType.Body, snippet };
167
168
  }
168
169
 
169
- return { match: 'title', snippet: title };
170
+ return { match: SearchMatchType.Title, snippet: title };
170
171
  }
171
172
 
172
173
  function resolveCtx(tag: string | null): VersionContext {
@@ -218,8 +219,8 @@ export default defineHandler(async event => {
218
219
  const queryLower = query.toLowerCase();
219
220
  return Response.json((result.rows ?? []).map(r => {
220
221
  const { match, snippet, slug } = findMatch(queryLower, r.title as string, r.headings as string, r.body as string);
221
- const id = match === 'heading' && slug ? `${r.id}#${slug}` : r.id as string;
222
- const url = match === 'heading' && slug ? `${r.url}#${slug}` : r.url as string;
222
+ const id = match === SearchMatchType.Heading && slug ? `${r.id}#${slug}` : r.id as string;
223
+ const url = match === SearchMatchType.Heading && slug ? `${r.url}#${slug}` : r.url as string;
223
224
  return {
224
225
  id,
225
226
  url,
@@ -41,6 +41,11 @@ function generateApiMarkdown(
41
41
  lines.push(operation.description)
42
42
  lines.push('')
43
43
  }
44
+ if (operation.externalDocs?.url) {
45
+ const label = operation.externalDocs.description || 'external documentation'
46
+ lines.push(`Read more about this operation in the [${label}](${operation.externalDocs.url}).`)
47
+ lines.push('')
48
+ }
44
49
  lines.push(`\`${method}\` \`${path}\``)
45
50
  lines.push('')
46
51
 
@@ -171,11 +171,15 @@
171
171
  }
172
172
 
173
173
  .groupItems {
174
- padding-left: var(--rs-space-4);
174
+ padding-left: 0;
175
175
  padding-bottom: var(--rs-space-3);
176
176
  gap: 0;
177
177
  }
178
178
 
179
+ .navGroup:not([data-depth='0']) .groupItems {
180
+ padding-left: var(--rs-space-4);
181
+ }
182
+
179
183
  .navGroup {
180
184
  margin-top: 0;
181
185
  }
@@ -279,3 +283,121 @@
279
283
  line-height: var(--rs-line-height-mini);
280
284
  flex-shrink: 0;
281
285
  }
286
+
287
+ .mobileMenuBtn {
288
+ display: none;
289
+ align-items: center;
290
+ justify-content: center;
291
+ background: none;
292
+ border: none;
293
+ cursor: pointer;
294
+ padding: var(--rs-space-1);
295
+ color: var(--rs-color-foreground-base-primary);
296
+ }
297
+
298
+ .mobileHeader {
299
+ display: none;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ height: var(--navbar-height);
303
+ padding: 0 var(--rs-space-5);
304
+ background: var(--rs-color-background-base-primary);
305
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
306
+ backdrop-filter: blur(1px);
307
+ }
308
+
309
+ .mobileMenu {
310
+ display: none;
311
+ position: fixed;
312
+ top: var(--navbar-height);
313
+ left: 0;
314
+ right: 0;
315
+ bottom: 0;
316
+ z-index: 100;
317
+ background: var(--rs-color-background-base-primary);
318
+ overflow-y: auto;
319
+ padding: var(--rs-space-7) var(--rs-space-5);
320
+ }
321
+
322
+ .mobileMenuFooter {
323
+ margin-top: var(--rs-space-7);
324
+ padding-top: var(--rs-space-5);
325
+ border-top: 0.5px solid var(--rs-color-border-base-primary);
326
+ }
327
+
328
+ .mobileNav {
329
+ display: none;
330
+ }
331
+
332
+ @media (max-width: 768px) {
333
+ .sidebar {
334
+ display: none;
335
+ }
336
+
337
+ .mobileHeader {
338
+ display: flex;
339
+ }
340
+
341
+ .mobileMenuBtn {
342
+ display: flex;
343
+ }
344
+
345
+ .mobileMenu[data-open='true'] {
346
+ display: block;
347
+ }
348
+
349
+ .subNav {
350
+ display: none;
351
+ }
352
+
353
+ .content {
354
+ padding: var(--rs-space-10) var(--rs-space-5);
355
+ }
356
+
357
+ .card {
358
+ width: 100%;
359
+ border-left: none;
360
+ box-shadow: none;
361
+ }
362
+
363
+ .cardWrapper {
364
+ padding: 0;
365
+ }
366
+
367
+ .mobileNav {
368
+ display: flex;
369
+ gap: var(--rs-space-10);
370
+ padding: var(--rs-space-3) var(--rs-space-5);
371
+ background: var(--rs-color-background-base-primary);
372
+ }
373
+
374
+ .mobileNavLink {
375
+ flex: 1;
376
+ display: flex;
377
+ align-items: center;
378
+ gap: var(--rs-space-3);
379
+ padding: var(--rs-space-4) var(--rs-space-3);
380
+ border: 0.5px solid var(--rs-color-border-base-primary);
381
+ border-radius: var(--rs-radius-4);
382
+ text-decoration: none;
383
+ font-family: var(--rs-font-body);
384
+ font-size: var(--rs-font-size-regular);
385
+ font-weight: var(--rs-font-weight-medium);
386
+ line-height: var(--rs-line-height-regular);
387
+ letter-spacing: var(--rs-letter-spacing-regular);
388
+ color: var(--rs-color-foreground-base-tertiary);
389
+ min-width: 0;
390
+ }
391
+
392
+ .mobileNavLink[data-direction='next'] {
393
+ justify-content: flex-end;
394
+ background: var(--rs-color-background-base-secondary);
395
+ color: var(--rs-color-foreground-base-primary);
396
+ }
397
+
398
+ .mobileNavLabel {
399
+ overflow: hidden;
400
+ text-overflow: ellipsis;
401
+ white-space: nowrap;
402
+ }
403
+ }
@@ -4,7 +4,9 @@ import {
4
4
  CodeBracketSquareIcon,
5
5
  RectangleStackIcon,
6
6
  DocumentTextIcon,
7
- Squares2X2Icon
7
+ Squares2X2Icon,
8
+ Bars3Icon,
9
+ XMarkIcon
8
10
  } from '@heroicons/react/24/outline';
9
11
  import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara';
10
12
  import { PlayIcon } from '@radix-ui/react-icons';
@@ -28,8 +30,11 @@ import type { ThemeLayoutProps } from '@/types';
28
30
  import styles from './Layout.module.css';
29
31
  import { OpenInAI } from './OpenInAI';
30
32
  import { SidebarLogo } from './SidebarLogo';
33
+
31
34
  import { VersionSwitcher } from './VersionSwitcher';
32
35
 
36
+ const MAX_SIDEBAR_DEPTH = 3;
37
+
33
38
  const iconMap: Record<string, React.ReactNode> = {
34
39
  'rectangle-stack': <RectangleStackIcon width={16} height={16} />,
35
40
  'method-get': <MethodBadge method='GET' size='micro' />,
@@ -70,6 +75,7 @@ export function Layout({
70
75
  const navigate = useNavigate();
71
76
  const { page, version } = usePageContext();
72
77
  const scrollRef = useRef<HTMLDivElement>(null);
78
+ const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
73
79
  const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/');
74
80
  const isApiBase = (basePath: string) =>
75
81
  pathname === basePath || pathname.startsWith(`${basePath}/`);
@@ -106,10 +112,82 @@ export function Layout({
106
112
  requestAnimationFrame(() => {
107
113
  el.scrollTop = savedScrollTop;
108
114
  });
115
+ setMobileSidebarOpen(false);
109
116
  }, [pathname]);
110
117
 
111
118
  return (
112
119
  <Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
120
+ <div className={styles.mobileHeader}>
121
+ <SidebarLogo config={config} />
122
+ <Flex align='center' gap={3}>
123
+ {config.search?.enabled && <Search />}
124
+ <ClientThemeSwitcher size={16} />
125
+ {!hideSidebar && (
126
+ <button
127
+ type='button'
128
+ className={styles.mobileMenuBtn}
129
+ onClick={() => setMobileSidebarOpen(o => !o)}
130
+ aria-label={mobileSidebarOpen ? 'Close menu' : 'Open menu'}
131
+ aria-expanded={mobileSidebarOpen}
132
+ aria-controls='mobile-menu'
133
+ >
134
+ {mobileSidebarOpen
135
+ ? <XMarkIcon width={16} height={16} />
136
+ : <Bars3Icon width={16} height={16} />}
137
+ </button>
138
+ )}
139
+ </Flex>
140
+ </div>
141
+ <div id='mobile-menu' className={styles.mobileMenu} data-open={!hideSidebar && mobileSidebarOpen}>
142
+ {showTopLinks ? (
143
+ <div className={styles.topLinks}>
144
+ {contentEntries.map(entry => (
145
+ <Sidebar.Item
146
+ key={entry.href}
147
+ href={entry.href}
148
+ active={activeContentDir === entry.contentDir}
149
+ leadingIcon={renderConfigIcon(entry.icon, entry.label, <DocumentTextIcon width={16} height={16} />)}
150
+ classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
151
+ render={<RouterLink to={entry.href} />}
152
+ >
153
+ {entry.label}
154
+ </Sidebar.Item>
155
+ ))}
156
+ {apiEntries.map(api => (
157
+ <Sidebar.Item
158
+ key={`${api.basePath}-${api.name}`}
159
+ href={api.basePath}
160
+ active={isApiBase(api.basePath)}
161
+ leadingIcon={renderConfigIcon(api.icon, api.name, <CodeBracketSquareIcon width={16} height={16} />)}
162
+ classNames={{ root: styles.topLinkItem, text: styles.topLinkText }}
163
+ render={<RouterLink to={api.basePath} />}
164
+ >
165
+ {api.name} API
166
+ </Sidebar.Item>
167
+ ))}
168
+ </div>
169
+ ) : null}
170
+ {tree.children.map((item, i) => (
171
+ isApiRoute ? (
172
+ <ApiSidebarNode
173
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
174
+ item={item}
175
+ pathname={pathname}
176
+ />
177
+ ) : (
178
+ <SidebarNode
179
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
180
+ item={item}
181
+ pathname={pathname}
182
+ />
183
+ )
184
+ ))}
185
+ {config.versions?.length ? (
186
+ <div className={styles.mobileMenuFooter}>
187
+ <VersionSwitcher />
188
+ </div>
189
+ ) : null}
190
+ </div>
113
191
  <Flex className={cx(styles.body, classNames?.body)}>
114
192
  {hideSidebar ? null : (
115
193
  <Sidebar
@@ -218,6 +296,20 @@ export function Layout({
218
296
  <main className={cx(styles.content, classNames?.content)}>
219
297
  {children}
220
298
  </main>
299
+ <div className={styles.mobileNav}>
300
+ {prev ? (
301
+ <RouterLink to={prev.url} className={styles.mobileNavLink}>
302
+ <ArrowLeftIcon width={16} height={16} />
303
+ <span className={styles.mobileNavLabel}>{prev.title}</span>
304
+ </RouterLink>
305
+ ) : <div />}
306
+ {next ? (
307
+ <RouterLink to={next.url} className={styles.mobileNavLink} data-direction='next'>
308
+ <span className={styles.mobileNavLabel}>{next.title}</span>
309
+ <ArrowRightIcon width={16} height={16} />
310
+ </RouterLink>
311
+ ) : <div />}
312
+ </div>
221
313
  </div>
222
314
  </div>
223
315
  </Flex>
@@ -249,7 +341,7 @@ function SidebarNode({
249
341
  }
250
342
 
251
343
  if (item.type === 'folder') {
252
- if (depth > 1) return null;
344
+ if (depth > MAX_SIDEBAR_DEPTH) return null;
253
345
  const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
254
346
  const hasActiveChild = hasActiveDescendant(item, pathname);
255
347
  return (
@@ -258,7 +350,7 @@ function SidebarNode({
258
350
  data-depth={depth}
259
351
  label={item.name?.toString() ?? ''}
260
352
  leadingIcon={icon ?? undefined}
261
- collapsible={depth === 1}
353
+ collapsible={depth >= 1}
262
354
  defaultOpen={hasActiveChild}
263
355
  classNames={{
264
356
  items: styles.groupItems,
@@ -39,15 +39,22 @@
39
39
  .content h1 {
40
40
  font-size: var(--rs-font-size-t4);
41
41
  line-height: var(--rs-line-height-t4);
42
- margin-top: 0;
43
- margin-bottom: var(--rs-space-10);
42
+ margin: var(--rs-space-10) 0;
44
43
  }
45
44
 
46
45
  .content h2 {
47
46
  font-size: var(--rs-font-size-t3);
48
47
  line-height: var(--rs-line-height-t3);
49
- margin-top: var(--rs-space-8);
50
- margin-bottom: var(--rs-space-8);
48
+ margin-top: var(--rs-space-10);
49
+ margin-bottom: var(--rs-space-7);
50
+ }
51
+
52
+ .content p + h2,
53
+ .content ul + h2,
54
+ .content ol + h2,
55
+ .content div + h2,
56
+ .content table + h2 {
57
+ margin-top: var(--rs-space-13);
51
58
  }
52
59
 
53
60
  .content h3 {
@@ -77,6 +84,7 @@
77
84
  font-style: normal;
78
85
  font-weight: var(--rs-font-weight-regular);
79
86
  line-height: 171.429%;
87
+ margin-bottom: var(--rs-space-7);
80
88
  }
81
89
 
82
90
  .content ul,
@@ -90,6 +98,15 @@
90
98
  margin: var(--rs-space-2) 0;
91
99
  }
92
100
 
101
+ .content table td {
102
+ font-size: var(--rs-font-size-regular);
103
+ }
104
+
105
+ .content table th {
106
+ font-size: var(--rs-font-size-regular);
107
+ font-weight: var(--rs-font-weight-medium);
108
+ }
109
+
93
110
  .content a {
94
111
  font-size: inherit;
95
112
  }
@@ -101,6 +118,7 @@
101
118
  .content img {
102
119
  max-width: 100%;
103
120
  height: auto;
121
+ margin: var(--rs-space-7) 0;
104
122
  }
105
123
 
106
124
  .content table {
@@ -166,3 +184,17 @@
166
184
  .headerLoader {
167
185
  margin-bottom: var(--rs-space-5);
168
186
  }
187
+
188
+ @media (max-width: 768px) {
189
+ .page {
190
+ gap: var(--rs-space-5);
191
+ }
192
+
193
+ .article {
194
+ max-width: 100%;
195
+ }
196
+
197
+ .title {
198
+ margin-bottom: var(--rs-space-5);
199
+ }
200
+ }
@@ -213,6 +213,11 @@
213
213
  text-overflow: unset;
214
214
  word-wrap: break-word;
215
215
  vertical-align: top;
216
+ font-size: var(--rs-font-size-regular);
217
+ }
218
+
219
+ .content table th {
220
+ font-weight: var(--rs-font-weight-medium);
216
221
  }
217
222
 
218
223
  .content a {
@@ -24,6 +24,21 @@ export interface PageNav {
24
24
  next: PageNavLink | null
25
25
  }
26
26
 
27
+ export const SearchResultType = {
28
+ Page: 'page',
29
+ Api: 'api',
30
+ } as const;
31
+
32
+ export type SearchResultType = (typeof SearchResultType)[keyof typeof SearchResultType];
33
+
34
+ export const SearchMatchType = {
35
+ Title: 'title',
36
+ Heading: 'heading',
37
+ Body: 'body',
38
+ } as const;
39
+
40
+ export type SearchMatchType = (typeof SearchMatchType)[keyof typeof SearchMatchType];
41
+
27
42
  export interface Page extends PageNav {
28
43
  slug: string[]
29
44
  frontmatter: Frontmatter