@raystack/chronicle 0.7.3 → 0.8.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 (46) hide show
  1. package/dist/cli/index.js +4 -1
  2. package/package.json +2 -1
  3. package/src/components/api/api-code-snippet.module.css +23 -0
  4. package/src/components/api/api-code-snippet.tsx +64 -0
  5. package/src/components/api/api-field-list.module.css +76 -0
  6. package/src/components/api/api-field-list.tsx +91 -0
  7. package/src/components/api/api-overview.module.css +65 -0
  8. package/src/components/api/api-overview.tsx +216 -0
  9. package/src/components/api/api-response-panel.module.css +62 -0
  10. package/src/components/api/api-response-panel.tsx +54 -0
  11. package/src/components/api/index.ts +5 -6
  12. package/src/components/api/json-editor.tsx +8 -8
  13. package/src/components/api/method-badge.tsx +2 -2
  14. package/src/components/api/playground-dialog.module.css +342 -0
  15. package/src/components/api/playground-dialog.tsx +583 -0
  16. package/src/lib/api-routes.ts +37 -8
  17. package/src/lib/openapi.ts +26 -0
  18. package/src/lib/schema.ts +45 -3
  19. package/src/lib/source.ts +57 -23
  20. package/src/lib/use-api-operation.ts +15 -0
  21. package/src/pages/ApiLayout.module.css +1 -0
  22. package/src/pages/ApiPage.tsx +7 -38
  23. package/src/pages/DocsPage.tsx +40 -1
  24. package/src/server/api/apis-proxy.ts +8 -1
  25. package/src/server/entry-server.tsx +2 -2
  26. package/src/server/routes/[...slug].md.ts +1 -0
  27. package/src/server/routes/apis/[...slug].md.ts +181 -0
  28. package/src/server/vite-config.ts +2 -0
  29. package/src/themes/default/Layout.module.css +53 -0
  30. package/src/themes/default/Layout.tsx +162 -11
  31. package/src/themes/paper/Page.module.css +7 -2
  32. package/src/themes/paper/Page.tsx +8 -6
  33. package/src/themes/paper/Skeleton.tsx +9 -0
  34. package/src/types/config.ts +1 -0
  35. package/src/components/api/code-snippets.module.css +0 -7
  36. package/src/components/api/code-snippets.tsx +0 -76
  37. package/src/components/api/endpoint-page.module.css +0 -58
  38. package/src/components/api/endpoint-page.tsx +0 -283
  39. package/src/components/api/field-row.module.css +0 -126
  40. package/src/components/api/field-row.tsx +0 -204
  41. package/src/components/api/field-section.module.css +0 -24
  42. package/src/components/api/field-section.tsx +0 -100
  43. package/src/components/api/key-value-editor.module.css +0 -13
  44. package/src/components/api/key-value-editor.tsx +0 -62
  45. package/src/components/api/response-panel.module.css +0 -8
  46. package/src/components/api/response-panel.tsx +0 -44
@@ -226,3 +226,56 @@
226
226
  .page {
227
227
  padding: var(--rs-space-2) 0;
228
228
  }
229
+
230
+ .apiGroup {
231
+ margin-top: var(--rs-space-8);
232
+ width: 100%;
233
+ }
234
+
235
+ .apiGroup:first-child {
236
+ margin-top: 0;
237
+ }
238
+
239
+ .apiGroupLabel {
240
+ font-size: var(--rs-font-size-small);
241
+ font-weight: var(--rs-font-weight-medium);
242
+ line-height: var(--rs-line-height-small);
243
+ letter-spacing: var(--rs-letter-spacing-small);
244
+ color: var(--rs-color-foreground-base-secondary);
245
+ padding: 0 var(--rs-space-3);
246
+ }
247
+
248
+ .apiItem {
249
+ padding: var(--rs-space-3);
250
+ border-radius: var(--rs-radius-2);
251
+ text-decoration: none;
252
+ cursor: pointer;
253
+ white-space: nowrap;
254
+ }
255
+
256
+ .apiItem:hover {
257
+ background: var(--rs-color-background-neutral-secondary);
258
+ }
259
+
260
+ .apiItemActive {
261
+ background: var(--rs-color-background-neutral-secondary);
262
+ }
263
+
264
+ .apiItemName {
265
+ flex: 1;
266
+ min-width: 0;
267
+ overflow: hidden;
268
+ text-overflow: ellipsis;
269
+ font-size: var(--rs-font-size-small);
270
+ font-weight: var(--rs-font-weight-medium);
271
+ line-height: var(--rs-line-height-small);
272
+ letter-spacing: var(--rs-letter-spacing-small);
273
+ color: var(--rs-color-foreground-base-primary);
274
+ }
275
+
276
+ .apiMethodText {
277
+ font-family: var(--rs-font-mono);
278
+ font-size: var(--rs-font-size-mono-mini);
279
+ line-height: var(--rs-line-height-mini);
280
+ flex-shrink: 0;
281
+ }
@@ -6,18 +6,22 @@ import {
6
6
  DocumentTextIcon,
7
7
  Squares2X2Icon
8
8
  } from '@heroicons/react/24/outline';
9
- import { Flex, IconButton, Sidebar } from '@raystack/apsara';
9
+ import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara';
10
+ import { PlayIcon } from '@radix-ui/react-icons';
10
11
  import { cx } from 'class-variance-authority';
11
- import { useEffect, useMemo, useRef } from 'react';
12
+ import { useState, useEffect, useMemo, useRef } from 'react';
12
13
  import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
14
+ import type { OpenAPIV3 } from 'openapi-types';
13
15
  import { MethodBadge } from '@/components/api/method-badge';
16
+ import { useApiOperation } from '@/lib/use-api-operation';
17
+ import { PlaygroundDialog } from '@/components/api/playground-dialog';
14
18
  import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
15
19
  import { Search } from '@/components/ui/search';
16
20
  import { Breadcrumbs } from '@/components/ui/breadcrumbs';
17
21
  import { getLandingEntries } from '@/lib/config';
18
22
  import { getActiveContentDir } from '@/lib/navigation';
19
23
  import { usePageContext } from '@/lib/page-context';
20
- import type { Node } from 'fumadocs-core/page-tree';
24
+ import type { Node, Root } from 'fumadocs-core/page-tree';
21
25
  import type { ThemeLayoutProps } from '@/types';
22
26
  import styles from './Layout.module.css';
23
27
  import { OpenInAI } from './OpenInAI';
@@ -67,7 +71,12 @@ export function Layout({
67
71
  const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/');
68
72
  const isApiBase = (basePath: string) =>
69
73
  pathname === basePath || pathname.startsWith(`${basePath}/`);
70
- const { prev, next } = page ?? { prev: null, next: null };
74
+ const docNav = page ?? { prev: null, next: null };
75
+ const apiNav = useMemo(() => {
76
+ if (!isApiRoute) return { prev: null, next: null };
77
+ return getApiPrevNext(pathname, tree);
78
+ }, [isApiRoute, pathname, tree]);
79
+ const { prev, next } = isApiRoute ? apiNav : docNav;
71
80
 
72
81
  const contentEntries = getLandingEntries(config, version.dir);
73
82
  const activeContentDir = getActiveContentDir(pathname, config);
@@ -151,11 +160,19 @@ export function Layout({
151
160
  </div>
152
161
  ) : null}
153
162
  {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
- />
163
+ isApiRoute ? (
164
+ <ApiSidebarNode
165
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
166
+ item={item}
167
+ pathname={pathname}
168
+ />
169
+ ) : (
170
+ <SidebarNode
171
+ key={item.type === 'page' ? item.url : (item.name?.toString() ?? i)}
172
+ item={item}
173
+ pathname={pathname}
174
+ />
175
+ )
159
176
  ))}
160
177
  </Sidebar.Main>
161
178
  {config.versions?.length ? (
@@ -188,9 +205,13 @@ export function Layout({
188
205
  <ArrowRightIcon width={14} height={14} />
189
206
  </IconButton>
190
207
  </Flex>
191
- {!isApiRoute && <Breadcrumbs slug={slug} tree={tree} />}
208
+ <Breadcrumbs slug={slug} tree={tree} />
209
+ </Flex>
210
+ <Flex align='center' gap='small'>
211
+ {isApiRoute && <TestRequestButton />}
212
+ {isApiRoute && <ViewDocsButton />}
213
+ <OpenInAI />
192
214
  </Flex>
193
- <OpenInAI />
194
215
  </nav>
195
216
  <main className={cx(styles.content, classNames?.content)}>
196
217
  {children}
@@ -262,3 +283,133 @@ function SidebarNode({
262
283
  </Sidebar.Item>
263
284
  );
264
285
  }
286
+
287
+ const methodColorMap: Record<string, string> = {
288
+ 'method-get': 'var(--rs-color-foreground-success-primary)',
289
+ 'method-post': 'var(--rs-color-foreground-accent-primary)',
290
+ 'method-put': 'var(--rs-color-foreground-attention-primary)',
291
+ 'method-delete': 'var(--rs-color-foreground-danger-primary)',
292
+ 'method-patch': 'var(--rs-color-foreground-base-secondary)',
293
+ };
294
+
295
+ const methodLabelMap: Record<string, string> = {
296
+ 'method-get': 'GET',
297
+ 'method-post': 'POST',
298
+ 'method-put': 'PUT',
299
+ 'method-delete': 'DEL',
300
+ 'method-patch': 'PATCH',
301
+ };
302
+
303
+ function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) {
304
+ if (item.type === 'separator') return null;
305
+
306
+ if (item.type === 'folder') {
307
+ return (
308
+ <Flex direction='column' gap='small' className={styles.apiGroup}>
309
+ <span className={styles.apiGroupLabel}>{item.name?.toString()}</span>
310
+ <Flex direction='column'>
311
+ {item.children.map((child, i) => (
312
+ <ApiSidebarNode
313
+ key={child.type === 'page' ? child.url : (child.name?.toString() ?? i)}
314
+ item={child}
315
+ pathname={pathname}
316
+ />
317
+ ))}
318
+ </Flex>
319
+ </Flex>
320
+ );
321
+ }
322
+
323
+ const isActive = pathname === item.url;
324
+ const href = item.url ?? '#';
325
+ const iconKey = typeof item.icon === 'string' ? item.icon : '';
326
+ const methodLabel = methodLabelMap[iconKey];
327
+ const methodColor = methodColorMap[iconKey];
328
+
329
+ return (
330
+ <Flex
331
+ align='center'
332
+ gap='small'
333
+ className={`${styles.apiItem} ${isActive ? styles.apiItemActive : ''}`}
334
+ render={<RouterLink to={href} />}
335
+ >
336
+ <span className={styles.apiItemName}>{item.name}</span>
337
+ {methodLabel && (
338
+ <span className={styles.apiMethodText} style={{ color: methodColor }}>
339
+ {methodLabel}
340
+ </span>
341
+ )}
342
+ </Flex>
343
+ );
344
+ }
345
+
346
+ function TestRequestButton() {
347
+ const match = useApiOperation();
348
+ const [open, setOpen] = useState(false);
349
+ if (!match) return null;
350
+
351
+ return (
352
+ <>
353
+ <Button
354
+ variant='outline'
355
+ color='neutral'
356
+ size='small'
357
+ leadingIcon={<PlayIcon width={12} height={12} />}
358
+ onClick={() => setOpen(true)}
359
+ >
360
+ Test request
361
+ </Button>
362
+ <PlaygroundDialog
363
+ key={`${match.spec.name}-${match.path}-${match.method}`}
364
+ open={open}
365
+ onOpenChange={setOpen}
366
+ method={match.method}
367
+ path={match.path}
368
+ operation={match.operation}
369
+ serverUrl={match.spec.server.url}
370
+ specName={match.spec.name}
371
+ auth={match.spec.auth}
372
+ document={match.spec.document}
373
+ />
374
+ </>
375
+ );
376
+ }
377
+
378
+ function ViewDocsButton() {
379
+ const match = useApiOperation();
380
+ if (!match) return null;
381
+
382
+ const operation = match.operation as OpenAPIV3.OperationObject;
383
+ const docsUrl = operation.externalDocs?.url ?? match.spec.document.externalDocs?.url;
384
+ if (!docsUrl) return null;
385
+
386
+ return (
387
+ <Button
388
+ variant='outline'
389
+ color='neutral'
390
+ size='small'
391
+ leadingIcon={<DocumentTextIcon width={12} height={12} />}
392
+ onClick={() => window.open(docsUrl, '_blank', 'noopener,noreferrer')}
393
+ >
394
+ View documentation
395
+ </Button>
396
+ );
397
+ }
398
+
399
+ function getApiPrevNext(pathname: string, tree: Root): { prev: { url: string; title: string } | null; next: { url: string; title: string } | null } {
400
+ const pages: { url: string; title: string }[] = [];
401
+ function collect(node: Node) {
402
+ if (node.type === 'page') {
403
+ pages.push({ url: node.url, title: node.name?.toString() ?? '' });
404
+ } else if (node.type === 'folder') {
405
+ for (const child of node.children) collect(child);
406
+ }
407
+ }
408
+ for (const child of tree.children) collect(child);
409
+
410
+ const idx = pages.findIndex(p => p.url === pathname);
411
+ return {
412
+ prev: idx > 0 ? pages[idx - 1] : null,
413
+ next: idx >= 0 && idx < pages.length - 1 ? pages[idx + 1] : null,
414
+ };
415
+ }
@@ -1,6 +1,6 @@
1
1
  .main {
2
2
  flex: 1;
3
- width: 90%;
3
+ width: 100%;
4
4
  max-width: calc(1024px + var(--rs-space-17));
5
5
  margin: 0 auto;
6
6
  padding-top: var(--rs-space-12);
@@ -137,8 +137,8 @@
137
137
  box-shadow:
138
138
  0 1px 3px rgba(0, 0, 0, 0.08),
139
139
  0 4px 12px rgba(0, 0, 0, 0.04);
140
- margin-bottom: var(--rs-space-9);
141
140
  min-height: calc(100vh - var(--rs-space-12));
141
+ margin: 0 var(--rs-space-7) var(--rs-space-9) var(--rs-space-7);
142
142
  }
143
143
 
144
144
  .content h1,
@@ -225,5 +225,10 @@
225
225
  }
226
226
 
227
227
  .loader {
228
+ flex: 1;
228
229
  margin-bottom: var(--rs-space-3)
229
230
  }
231
+
232
+ .navbarLoaderWrapper {
233
+ width: 30%;
234
+ }
@@ -1,12 +1,14 @@
1
1
  import {
2
- ArrowLeftIcon,
3
- ArrowRightIcon,
4
- AdjustmentsHorizontalIcon,
5
2
  EyeIcon,
6
3
  SunIcon,
7
4
  MoonIcon,
8
- XMarkIcon,
9
5
  } from '@heroicons/react/24/outline';
6
+ import {
7
+ ArrowLeftIcon,
8
+ ArrowRightIcon,
9
+ MixerHorizontalIcon,
10
+ Cross2Icon
11
+ } from '@radix-ui/react-icons'
10
12
  import { IconButton, useTheme } from '@raystack/apsara';
11
13
  import { useEffect, useMemo, useState } from 'react';
12
14
  import { Link as RouterLink, useLocation } from 'react-router';
@@ -83,12 +85,12 @@ export function Page({ page, tree }: ThemePageProps) {
83
85
  </IconButton>
84
86
  )}
85
87
  <IconButton size={2} onClick={() => setSettingsOpen(false)} aria-label='Close settings'>
86
- <XMarkIcon width={14} height={14} />
88
+ <Cross2Icon width={14} height={14} />
87
89
  </IconButton>
88
90
  </>
89
91
  ) : (
90
92
  <IconButton size={2} onClick={() => setSettingsOpen(true)} aria-label='Open settings'>
91
- <AdjustmentsHorizontalIcon width={14} height={14} />
93
+ <MixerHorizontalIcon width={14} height={14} />
92
94
  </IconButton>
93
95
  )}
94
96
  </div>
@@ -1,9 +1,18 @@
1
1
  import { Skeleton } from '@raystack/apsara';
2
2
  import styles from './Page.module.css';
3
+ import { cx } from 'class-variance-authority';
3
4
 
4
5
  export function PageSkeleton() {
5
6
  return (
6
7
  <main className={styles.main}>
8
+ <div className={styles.navbar}>
9
+ <div className={cx(styles.navLeft, styles.navbarLoaderWrapper)}>
10
+ <Skeleton highlightColor="var(--rs-color-foreground-base-emphasis)" containerClassName={styles.loader}/>
11
+ </div>
12
+ <div className={cx(styles.navRight, styles.navbarLoaderWrapper)}>
13
+ <Skeleton highlightColor="var(--rs-color-foreground-base-emphasis)" containerClassName={styles.loader}/>
14
+ </div>
15
+ </div>
7
16
  <div className={styles.content}>
8
17
  <header className={styles.articleHeader}>
9
18
  <Skeleton width="50%" height="16px" containerClassName={styles.headerLoader}/>
@@ -86,6 +86,7 @@ const contentEntrySchema = z.object({
86
86
  label: z.string().min(1),
87
87
  description: z.string().optional(),
88
88
  icon: z.string().optional(),
89
+ index_page: z.string().optional(),
89
90
  })
90
91
 
91
92
  // Variants map to Apsara Badge color prop.
@@ -1,7 +0,0 @@
1
- .snippets {
2
- width: 100%;
3
- }
4
-
5
- .snippets :global([class*="code-block-module_header"]) {
6
- justify-content: space-between;
7
- }
@@ -1,76 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo, useState } from "react";
4
- import { CodeBlock } from "@raystack/apsara";
5
- import {
6
- generateCurl,
7
- generatePython,
8
- generateGo,
9
- generateTypeScript,
10
- } from "@/lib/snippet-generators";
11
- import styles from "./code-snippets.module.css";
12
-
13
- interface CodeSnippetsProps {
14
- method: string;
15
- url: string;
16
- headers: Record<string, string>;
17
- body?: string;
18
- }
19
-
20
- const languages = [
21
- { value: "curl", label: "cURL", lang: "curl", generate: generateCurl },
22
- {
23
- value: "python",
24
- label: "Python",
25
- lang: "python",
26
- generate: generatePython,
27
- },
28
- { value: "go", label: "Go", lang: "go", generate: generateGo },
29
- {
30
- value: "typescript",
31
- label: "TypeScript",
32
- lang: "typescript",
33
- generate: generateTypeScript,
34
- },
35
- ];
36
-
37
- export function CodeSnippets({
38
- method,
39
- url,
40
- headers,
41
- body,
42
- }: CodeSnippetsProps) {
43
- const opts = { method, url, headers, body };
44
- const [selected, setSelected] = useState("curl");
45
- const current = languages.find((l) => l.value === selected) ?? languages[0];
46
-
47
- const code = useMemo(
48
- () => current.generate(opts),
49
- [selected, method, url, headers, body],
50
- );
51
-
52
- return (
53
- <CodeBlock
54
- value={selected}
55
- onValueChange={setSelected}
56
- className={styles.snippets}
57
- >
58
- <CodeBlock.Header>
59
- <CodeBlock.LanguageSelect>
60
- <CodeBlock.LanguageSelectTrigger />
61
- <CodeBlock.LanguageSelectContent>
62
- {languages.map((l) => (
63
- <CodeBlock.LanguageSelectItem key={l.value} value={l.value}>
64
- {l.label}
65
- </CodeBlock.LanguageSelectItem>
66
- ))}
67
- </CodeBlock.LanguageSelectContent>
68
- </CodeBlock.LanguageSelect>
69
- <CodeBlock.CopyButton />
70
- </CodeBlock.Header>
71
- <CodeBlock.Content>
72
- <CodeBlock.Code language={current.lang}>{code}</CodeBlock.Code>
73
- </CodeBlock.Content>
74
- </CodeBlock>
75
- );
76
- }
@@ -1,58 +0,0 @@
1
- .layout {
2
- display: grid;
3
- grid-template-columns: 1fr 1fr;
4
- gap: var(--rs-space-9);
5
- padding: var(--rs-space-7);
6
- max-width: 1400px;
7
- }
8
-
9
- .left {
10
- gap: var(--rs-space-7);
11
- min-width: 0;
12
- }
13
-
14
- .right {
15
- gap: var(--rs-space-5);
16
- min-width: 0;
17
- }
18
-
19
- .title {
20
- margin: 0;
21
- }
22
-
23
- .description {
24
- color: var(--rs-color-foreground-base-secondary);
25
- }
26
-
27
- .methodPath {
28
- gap: var(--rs-space-3);
29
- padding: var(--rs-space-4) var(--rs-space-5);
30
- border: 1px solid var(--rs-color-border-base-primary);
31
- border-radius: 8px;
32
- background: var(--rs-color-background-base-secondary);
33
- overflow: hidden;
34
- }
35
-
36
- .path {
37
- font-family: monospace;
38
- flex: 1;
39
- min-width: 0;
40
- overflow: hidden;
41
- text-overflow: ellipsis;
42
- white-space: nowrap;
43
- }
44
-
45
- .tryButton {
46
- margin-left: auto;
47
- flex-shrink: 0;
48
- }
49
-
50
- @media (max-width: 900px) {
51
- .layout {
52
- grid-template-columns: 1fr;
53
- }
54
-
55
- .right {
56
- position: static;
57
- }
58
- }