@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 +1 -1
- package/src/components/api/api-code-snippet.module.css +2 -0
- package/src/components/api/api-overview.module.css +10 -0
- package/src/components/ui/search.module.css +39 -23
- package/src/components/ui/search.tsx +46 -39
- package/src/server/api/search.ts +11 -10
- package/src/server/utils/api-markdown.ts +5 -0
- package/src/themes/default/Layout.module.css +123 -1
- package/src/themes/default/Layout.tsx +95 -3
- package/src/themes/default/Page.module.css +36 -4
- package/src/themes/paper/Page.module.css +5 -0
- package/src/types/content.ts +15 -0
package/package.json
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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:
|
|
43
|
-
gap:
|
|
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:
|
|
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:
|
|
81
|
-
height:
|
|
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:
|
|
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
|
-
|
|
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-
|
|
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 {
|
|
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?:
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 (
|
|
237
|
-
return
|
|
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
|
-
|
|
244
|
-
|
|
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 {
|
package/src/server/api/search.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
148
|
+
): { match: SearchMatchType; snippet: string; slug?: string } {
|
|
148
149
|
if (title.toLowerCase().includes(query)) {
|
|
149
|
-
return { match:
|
|
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:
|
|
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:
|
|
167
|
+
return { match: SearchMatchType.Body, snippet };
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
return { match:
|
|
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 ===
|
|
222
|
-
const url = match ===
|
|
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:
|
|
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 >
|
|
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
|
|
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
|
|
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-
|
|
50
|
-
margin-bottom: var(--rs-space-
|
|
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
|
+
}
|
package/src/types/content.ts
CHANGED
|
@@ -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
|