@raystack/chronicle 0.10.0 → 0.10.2

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 (42) hide show
  1. package/dist/cli/index.js +24 -0
  2. package/package.json +3 -2
  3. package/src/cli/commands/dev.ts +12 -0
  4. package/src/cli/commands/start.ts +12 -0
  5. package/src/components/api/api-overview.tsx +2 -2
  6. package/src/components/api/playground-dialog.tsx +42 -20
  7. package/src/components/mdx/link.tsx +5 -31
  8. package/src/components/ui/PrefetchProvider.tsx +70 -0
  9. package/src/components/ui/search.module.css +6 -0
  10. package/src/components/ui/search.tsx +4 -1
  11. package/src/lib/env.ts +9 -0
  12. package/src/lib/openapi.ts +2 -1
  13. package/src/lib/page-context.tsx +11 -6
  14. package/src/lib/preload.ts +37 -0
  15. package/src/lib/source.ts +21 -2
  16. package/src/pages/DocsLayout.tsx +11 -8
  17. package/src/server/App.module.css +4 -0
  18. package/src/server/App.tsx +32 -15
  19. package/src/server/api/page.ts +2 -2
  20. package/src/server/api/search.ts +16 -6
  21. package/src/server/entry-client.tsx +18 -14
  22. package/src/server/entry-server.tsx +6 -2
  23. package/src/server/routes/[...slug].md.ts +5 -1
  24. package/src/server/{routes/apis/[...slug].md.ts → utils/api-markdown.ts} +3 -6
  25. package/src/themes/default/ContentDirButtons.tsx +1 -1
  26. package/src/themes/default/Layout.tsx +38 -21
  27. package/src/themes/default/Page.module.css +9 -0
  28. package/src/themes/default/Page.tsx +6 -2
  29. package/src/themes/default/Skeleton.tsx +5 -15
  30. package/src/themes/default/VersionSwitcher.tsx +2 -2
  31. package/src/themes/paper/VersionSwitcher.tsx +2 -2
  32. package/src/types/content.ts +1 -0
  33. package/src/components/common/breadcrumb.tsx +0 -3
  34. package/src/components/common/button.tsx +0 -3
  35. package/src/components/common/code-block.tsx +0 -3
  36. package/src/components/common/dialog.tsx +0 -3
  37. package/src/components/common/index.ts +0 -10
  38. package/src/components/common/input-field.tsx +0 -3
  39. package/src/components/common/sidebar.tsx +0 -3
  40. package/src/components/common/switch.tsx +0 -3
  41. package/src/components/common/table.tsx +0 -3
  42. package/src/components/common/tabs.tsx +0 -3
@@ -1,17 +1,20 @@
1
1
  import '@raystack/apsara/normalize.css';
2
2
  import '@raystack/apsara/style.css';
3
- import { ThemeProvider } from '@raystack/apsara';
3
+ import { ThemeProvider, Skeleton, Flex } from '@raystack/apsara';
4
+ import { lazy, Suspense } from 'react';
4
5
  import { Navigate, useLocation } from 'react-router';
5
6
  import { Head } from '@/lib/head';
6
7
  import { usePageContext } from '@/lib/page-context';
7
8
  import { resolveRoute, RouteType } from '@/lib/route-resolver';
8
- import { ApiLayout } from '@/pages/ApiLayout';
9
- import { ApiPage } from '@/pages/ApiPage';
10
- import { DocsLayout } from '@/pages/DocsLayout';
11
- import { DocsPage } from '@/pages/DocsPage';
12
- import { LandingPage } from '@/pages/LandingPage';
13
9
  import type { ChronicleConfig } from '@/types';
14
10
  import { getThemeConfig } from '@/themes/registry';
11
+ import styles from './App.module.css';
12
+
13
+ const ApiLayout = lazy(() => import('@/pages/ApiLayout').then(m => ({ default: m.ApiLayout })));
14
+ const ApiPage = lazy(() => import('@/pages/ApiPage').then(m => ({ default: m.ApiPage })));
15
+ const DocsLayout = lazy(() => import('@/pages/DocsLayout').then(m => ({ default: m.DocsLayout })));
16
+ const DocsPage = lazy(() => import('@/pages/DocsPage').then(m => ({ default: m.DocsPage })));
17
+ const LandingPage = lazy(() => import('@/pages/LandingPage').then(m => ({ default: m.LandingPage })));
15
18
 
16
19
  export function App() {
17
20
  const { pathname } = useLocation();
@@ -35,19 +38,33 @@ export function App() {
35
38
  forcedTheme={themeConfig.forcedTheme}
36
39
  >
37
40
  <RootHead config={config} />
38
- {isApi ? (
39
- <ApiLayout>
40
- <ApiPage slug={apiSlug} />
41
- </ApiLayout>
42
- ) : (
43
- <DocsLayout hideSidebar={isLanding}>
44
- {isLanding ? <LandingPage /> : <DocsPage slug={docsSlug} />}
45
- </DocsLayout>
46
- )}
41
+ <Suspense fallback={<PageFallback />}>
42
+ {isApi ? (
43
+ <ApiLayout>
44
+ <ApiPage slug={apiSlug} />
45
+ </ApiLayout>
46
+ ) : (
47
+ <DocsLayout hideSidebar={isLanding}>
48
+ {isLanding ? <LandingPage /> : <DocsPage slug={docsSlug} />}
49
+ </DocsLayout>
50
+ )}
51
+ </Suspense>
47
52
  </ThemeProvider>
48
53
  );
49
54
  }
50
55
 
56
+ function PageFallback() {
57
+ return (
58
+ <Flex direction="column" gap={4} className={styles.fallback}>
59
+ <Skeleton width="40%" height="var(--rs-line-height-t2)" />
60
+ <Skeleton width="60%" height="var(--rs-line-height-regular)" />
61
+ {[...new Array(12)].map((_, i) => (
62
+ <Skeleton key={i} width="100%" height="var(--rs-line-height-regular)" />
63
+ ))}
64
+ </Flex>
65
+ );
66
+ }
67
+
51
68
  function RootHead({ config }: { config: ChronicleConfig }) {
52
69
  return (
53
70
  <Head
@@ -1,12 +1,12 @@
1
1
  import { defineHandler, HTTPError } from 'nitro';
2
- import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
2
+ import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath, isDraft } from '@/lib/source';
3
3
 
4
4
  export default defineHandler(async event => {
5
5
  const slugParam = event.url.searchParams.get('slug') ?? '';
6
6
  const slug = slugParam ? slugParam.split(',').filter(Boolean) : [];
7
7
  const page = await getPage(slug);
8
8
 
9
- if (!page) {
9
+ if (!page || isDraft(page)) {
10
10
  throw new HTTPError({ status: 404, message: 'Page not found' });
11
11
  }
12
12
 
@@ -14,6 +14,7 @@ interface SearchDocument {
14
14
  headings: string;
15
15
  body: string;
16
16
  type: 'page' | 'api';
17
+ section: string;
17
18
  }
18
19
 
19
20
  import fs from 'node:fs/promises';
@@ -61,7 +62,8 @@ async function buildIndex(ctx: VersionContext, key: string) {
61
62
  headings TEXT NOT NULL,
62
63
  body TEXT NOT NULL,
63
64
  type TEXT NOT NULL,
64
- version TEXT NOT NULL
65
+ version TEXT NOT NULL,
66
+ section TEXT NOT NULL DEFAULT ''
65
67
  )`);
66
68
 
67
69
  await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
@@ -74,8 +76,8 @@ async function buildIndex(ctx: VersionContext, key: string) {
74
76
 
75
77
  const docs = await buildDocs(ctx);
76
78
  for (const doc of docs) {
77
- await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version)
78
- VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key})`;
79
+ await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version, section)
80
+ VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key}, ${doc.section})`;
79
81
  }
80
82
 
81
83
  await db.sql`INSERT INTO search_fts (rowid, title, headings, body)
@@ -86,11 +88,15 @@ async function buildIndex(ctx: VersionContext, key: string) {
86
88
 
87
89
  async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
88
90
  const docs: SearchDocument[] = [];
91
+ const config = loadConfig();
92
+ const contentEntries = config.content ?? [];
89
93
 
90
94
  const pages = await getPagesForVersion(ctx);
91
95
  for (const p of pages) {
92
96
  const fm = extractFrontmatter(p);
93
97
  const { headings, body } = await getPageSearchContent(p);
98
+ const dir = p.url.replace(/^\//, '').split('/')[0];
99
+ const entry = contentEntries.find(c => c.dir === dir);
94
100
  docs.push({
95
101
  id: p.url,
96
102
  url: p.url,
@@ -98,10 +104,10 @@ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
98
104
  headings,
99
105
  body: [fm.description ?? '', body].join(' '),
100
106
  type: 'page',
107
+ section: entry?.label ?? dir ?? '',
101
108
  });
102
109
  }
103
110
 
104
- const config = loadConfig();
105
111
  const apiConfigs = getApiConfigsForVersion(config, ctx.dir);
106
112
  if (apiConfigs.length) {
107
113
  const specs = await loadApiSpecs(apiConfigs);
@@ -122,6 +128,7 @@ async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
122
128
  headings: op.summary ?? opId,
123
129
  body: [op.description ?? '', pathStr, method.toUpperCase()].join(' '),
124
130
  type: 'api',
131
+ section: spec.name,
125
132
  });
126
133
  }
127
134
  }
@@ -183,7 +190,7 @@ export default defineHandler(async event => {
183
190
  const key = versionKey(ctx);
184
191
 
185
192
  if (!query) {
186
- const result = await db.sql`SELECT id, url, title, type FROM search_docs
193
+ const result = await db.sql`SELECT id, url, title, type, section FROM search_docs
187
194
  WHERE version = ${key} AND type = 'page'
188
195
  LIMIT 8`;
189
196
  return Response.json((result.rows ?? []).map(r => ({
@@ -191,11 +198,12 @@ export default defineHandler(async event => {
191
198
  url: r.url,
192
199
  type: r.type,
193
200
  content: r.title,
201
+ section: r.section || null,
194
202
  })));
195
203
  }
196
204
 
197
205
  const searchTerm = query.split(/\s+/).map(t => `"${t}"*`).join(' ');
198
- const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type,
206
+ const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type, s.section,
199
207
  bm25(search_fts, 10.0, 5.0, 1.0) AS score
200
208
  FROM search_fts f
201
209
  JOIN search_docs s ON s.rowid = f.rowid
@@ -214,6 +222,8 @@ export default defineHandler(async event => {
214
222
  content: r.title,
215
223
  match,
216
224
  snippet,
225
+ section: r.section || null,
217
226
  };
218
227
  }));
219
228
  });
229
+
@@ -3,9 +3,11 @@ import React from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
4
  import { BrowserRouter } from 'react-router';
5
5
  import { ReactRouterProvider } from 'fumadocs-core/framework/react-router';
6
+ import { QueryClientProvider } from '@tanstack/react-query';
6
7
  import { mdxComponents } from '@/components/mdx';
7
8
  import { getApiConfigsForVersion } from '@/lib/config';
8
9
  import { PageProvider } from '@/lib/page-context';
10
+ import { queryClient } from '@/lib/preload';
9
11
  import { resolveRoute, RouteType } from '@/lib/route-resolver';
10
12
  import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source';
11
13
  import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types';
@@ -93,20 +95,22 @@ async function hydrate() {
93
95
 
94
96
  hydrateRoot(
95
97
  document.getElementById('root') as HTMLElement,
96
- <BrowserRouter>
97
- <ReactRouterProvider>
98
- <PageProvider
99
- initialConfig={config}
100
- initialTree={tree}
101
- initialPage={page}
102
- initialApiSpecs={apiSpecs}
103
- initialVersion={version}
104
- loadMdx={loadMdxModule}
105
- >
106
- <App />
107
- </PageProvider>
108
- </ReactRouterProvider>
109
- </BrowserRouter>
98
+ <QueryClientProvider client={queryClient}>
99
+ <BrowserRouter>
100
+ <ReactRouterProvider>
101
+ <PageProvider
102
+ initialConfig={config}
103
+ initialTree={tree}
104
+ initialPage={page}
105
+ initialApiSpecs={apiSpecs}
106
+ initialVersion={version}
107
+ loadMdx={loadMdxModule}
108
+ >
109
+ <App />
110
+ </PageProvider>
111
+ </ReactRouterProvider>
112
+ </BrowserRouter>
113
+ </QueryClientProvider>
110
114
  );
111
115
  } catch (err) {
112
116
  console.error('Hydration failed:', err);
@@ -9,7 +9,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
9
9
  import { loadApiSpecs } from '@/lib/openapi';
10
10
  import { PageProvider } from '@/lib/page-context';
11
11
  import { resolveRoute, RouteType } from '@/lib/route-resolver';
12
- import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source';
12
+ import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath, isDraft } from '@/lib/source';
13
13
  import { getFirstApiUrl } from '@/lib/api-routes';
14
14
  import { StatusCodes } from 'http-status-codes';
15
15
  import { resolveDocsRedirect } from '@/lib/tree-utils';
@@ -44,10 +44,12 @@ export default {
44
44
  : [];
45
45
  const apiSpecs = apiConfigs.length ? await loadApiSpecs(apiConfigs) : [];
46
46
 
47
- const [tree, page] = await Promise.all([
47
+ const [tree, rawPage] = await Promise.all([
48
48
  getPageTree(),
49
49
  route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null),
50
50
  ]);
51
+ const page = rawPage && isDraft(rawPage) ? null : rawPage;
52
+
51
53
  // SSR redirects for index pages
52
54
  if (route.type === RouteType.ApiIndex) {
53
55
  const firstUrl = getFirstApiUrl(apiSpecs);
@@ -115,6 +117,8 @@ export default {
115
117
  <head>
116
118
  <meta charSet="UTF-8" />
117
119
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
120
+ <link rel="icon" href="/favicon.ico" />
121
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
118
122
  {assets.css.map((attr: { href: string }) => (
119
123
  <link key={attr.href} rel="stylesheet" {...attr} />
120
124
  ))}
@@ -3,11 +3,15 @@ import matter from 'gray-matter';
3
3
  import { defineHandler, HTTPError } from 'nitro';
4
4
  import { getPage, getOriginalPath } from '@/lib/source';
5
5
  import { safePath } from '@/server/utils/safe-path';
6
+ import { handleApiMarkdown } from '@/server/utils/api-markdown';
6
7
 
7
8
  export default defineHandler(async event => {
8
9
  const pathname = event.path || event.req.url?.split('?')[0] || '';
9
10
  if (!pathname.endsWith('.md')) return;
10
- if (pathname.startsWith('/apis/')) return;
11
+
12
+ if (pathname.startsWith('/apis/')) {
13
+ return handleApiMarkdown(pathname);
14
+ }
11
15
 
12
16
  const stripped = pathname.replace(/\.md$/, '');
13
17
  const parts = stripped === '/index' || stripped === '/'
@@ -1,15 +1,12 @@
1
1
  import type { OpenAPIV3 } from 'openapi-types'
2
- import { defineHandler, HTTPError } from 'nitro'
2
+ import { HTTPError } from 'nitro'
3
3
  import { loadConfig } from '@/lib/config'
4
4
  import { loadApiSpecs } from '@/lib/openapi'
5
5
  import { findApiOperation } from '@/lib/api-routes'
6
6
  import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
7
7
  import { generateCurl } from '@/lib/snippet-generators'
8
8
 
9
- export default defineHandler(async event => {
10
- const pathname = event.path || event.req.url?.split('?')[0] || ''
11
- if (!pathname.endsWith('.md')) return
12
-
9
+ export async function handleApiMarkdown(pathname: string) {
13
10
  const stripped = pathname.replace(/\.md$/, '').replace(/^\/apis\//, '')
14
11
  const slug = stripped.split('/').filter(Boolean)
15
12
  if (slug.length < 2) {
@@ -26,7 +23,7 @@ export default defineHandler(async event => {
26
23
 
27
24
  const md = generateApiMarkdown(match.method, match.path, match.operation, match.spec.server.url, match.spec.auth)
28
25
  return new Response(md, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } })
29
- })
26
+ }
30
27
 
31
28
  function generateApiMarkdown(
32
29
  method: string,
@@ -19,7 +19,7 @@ export function ContentDirButtons() {
19
19
  const { visible, overflow } = splitContentButtons(entries, MAX_VISIBLE);
20
20
 
21
21
  return (
22
- <Flex gap='small' align='center'>
22
+ <Flex gap={3} align='center'>
23
23
  {visible.map(entry => (
24
24
  <RouterLink
25
25
  key={entry.href}
@@ -9,12 +9,13 @@ import {
9
9
  import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara';
10
10
  import { PlayIcon } from '@radix-ui/react-icons';
11
11
  import { cx } from 'class-variance-authority';
12
- import { useState, useEffect, useMemo, useRef } from 'react';
12
+ import { useState, useEffect, useMemo, useRef, lazy, Suspense } from 'react';
13
13
  import { Link as RouterLink, useLocation, useNavigate } from 'react-router';
14
14
  import type { OpenAPIV3 } from 'openapi-types';
15
15
  import { MethodBadge } from '@/components/api/method-badge';
16
16
  import { useApiOperation } from '@/lib/use-api-operation';
17
- import { PlaygroundDialog } from '@/components/api/playground-dialog';
17
+
18
+ const PlaygroundDialog = lazy(() => import('@/components/api/playground-dialog').then(m => ({ default: m.PlaygroundDialog })));
18
19
  import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher';
19
20
  import { Search } from '@/components/ui/search';
20
21
  import { Breadcrumbs } from '@/components/ui/breadcrumbs';
@@ -22,6 +23,7 @@ import { getLandingEntries } from '@/lib/config';
22
23
  import { getActiveContentDir } from '@/lib/navigation';
23
24
  import { usePageContext } from '@/lib/page-context';
24
25
  import type { Node, Root } from 'fumadocs-core/page-tree';
26
+ import { NodeType } from '@/lib/tree-utils';
25
27
  import type { ThemeLayoutProps } from '@/types';
26
28
  import styles from './Layout.module.css';
27
29
  import { OpenInAI } from './OpenInAI';
@@ -117,7 +119,7 @@ export function Layout({
117
119
  >
118
120
  <Sidebar.Header className={styles.sidebarHeader}>
119
121
  <SidebarLogo config={config} />
120
- <Flex gap='small' align='center'>
122
+ <Flex gap={3} align='center'>
121
123
  {config.search?.enabled && <Search />}
122
124
  <ClientThemeSwitcher size={16} />
123
125
  </Flex>
@@ -143,7 +145,7 @@ export function Layout({
143
145
  ))}
144
146
  {apiEntries.map(api => (
145
147
  <Sidebar.Item
146
- key={api.basePath}
148
+ key={`${api.basePath}-${api.name}`}
147
149
  href={api.basePath}
148
150
  active={isApiBase(api.basePath)}
149
151
  leadingIcon={renderConfigIcon(
@@ -186,8 +188,8 @@ export function Layout({
186
188
  <div className={styles.cardWrapper}>
187
189
  <div className={styles.card}>
188
190
  <nav className={styles.subNav}>
189
- <Flex align='center' gap='small' className={styles.subNavLeft}>
190
- <Flex align='center' gap='extra-small'>
191
+ <Flex align='center' gap={3} className={styles.subNavLeft}>
192
+ <Flex align='center' gap={2}>
191
193
  <IconButton
192
194
  size={2}
193
195
  disabled={!prev}
@@ -207,7 +209,7 @@ export function Layout({
207
209
  </Flex>
208
210
  <Breadcrumbs slug={slug} tree={tree} />
209
211
  </Flex>
210
- <Flex align='center' gap='small'>
212
+ <Flex align='center' gap={3}>
211
213
  {isApiRoute && <TestRequestButton />}
212
214
  {isApiRoute && <ViewDocsButton />}
213
215
  <OpenInAI />
@@ -224,6 +226,15 @@ export function Layout({
224
226
  );
225
227
  }
226
228
 
229
+ function hasActiveDescendant(node: Node, pathname: string): boolean {
230
+ if (node.type === NodeType.Page) return pathname === node.url;
231
+ if (node.type === NodeType.Folder) {
232
+ if (node.index && pathname === node.index.url) return true;
233
+ return node.children.some(child => hasActiveDescendant(child, pathname));
234
+ }
235
+ return false;
236
+ }
237
+
227
238
  function SidebarNode({
228
239
  item,
229
240
  pathname,
@@ -240,6 +251,7 @@ function SidebarNode({
240
251
  if (item.type === 'folder') {
241
252
  if (depth > 1) return null;
242
253
  const icon = typeof item.icon === 'string' ? iconMap[item.icon] : item.icon;
254
+ const hasActiveChild = hasActiveDescendant(item, pathname);
243
255
  return (
244
256
  <Sidebar.Group
245
257
  className={styles.navGroup}
@@ -247,6 +259,7 @@ function SidebarNode({
247
259
  label={item.name?.toString() ?? ''}
248
260
  leadingIcon={icon ?? undefined}
249
261
  collapsible={depth === 1}
262
+ defaultOpen={hasActiveChild}
250
263
  classNames={{
251
264
  items: styles.groupItems,
252
265
  header: styles.navGroupHeader,
@@ -305,7 +318,7 @@ function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) {
305
318
 
306
319
  if (item.type === 'folder') {
307
320
  return (
308
- <Flex direction='column' gap='small' className={styles.apiGroup}>
321
+ <Flex direction='column' gap={3} className={styles.apiGroup}>
309
322
  <span className={styles.apiGroupLabel}>{item.name?.toString()}</span>
310
323
  <Flex direction='column'>
311
324
  {item.children.map((child, i) => (
@@ -329,7 +342,7 @@ function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) {
329
342
  return (
330
343
  <Flex
331
344
  align='center'
332
- gap='small'
345
+ gap={3}
333
346
  className={`${styles.apiItem} ${isActive ? styles.apiItemActive : ''}`}
334
347
  render={<RouterLink to={href} />}
335
348
  >
@@ -359,18 +372,22 @@ function TestRequestButton() {
359
372
  >
360
373
  Test request
361
374
  </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
- />
375
+ {open && (
376
+ <Suspense fallback={null}>
377
+ <PlaygroundDialog
378
+ key={`${match.spec.name}-${match.path}-${match.method}`}
379
+ open={open}
380
+ onOpenChange={setOpen}
381
+ method={match.method}
382
+ path={match.path}
383
+ operation={match.operation}
384
+ serverUrl={match.spec.server.url}
385
+ specName={match.spec.name}
386
+ auth={match.spec.auth}
387
+ document={match.spec.document}
388
+ />
389
+ </Suspense>
390
+ )}
374
391
  </>
375
392
  );
376
393
  }
@@ -125,3 +125,12 @@
125
125
  .content details > :not(summary) {
126
126
  padding: var(--rs-space-4) var(--rs-space-5);
127
127
  }
128
+
129
+ .loader {
130
+ flex: 1;
131
+ margin-bottom: var(--rs-space-3);
132
+ }
133
+
134
+ .headerLoader {
135
+ margin-bottom: var(--rs-space-5);
136
+ }
@@ -1,9 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { Flex, Headline } from '@raystack/apsara';
4
+ import { lazy, Suspense } from 'react';
4
5
  import type { ThemePageProps } from '@/types';
5
6
  import styles from './Page.module.css';
6
- import { Toc } from './Toc';
7
+
8
+ const Toc = lazy(() => import('./Toc').then(m => ({ default: m.Toc })));
7
9
 
8
10
  export function Page({ page }: ThemePageProps) {
9
11
  return (
@@ -16,7 +18,9 @@ export function Page({ page }: ThemePageProps) {
16
18
  )}
17
19
  <div className={styles.content}>{page.content}</div>
18
20
  </article>
19
- <Toc items={page.toc} />
21
+ <Suspense fallback={null}>
22
+ <Toc items={page.toc} />
23
+ </Suspense>
20
24
  </Flex>
21
25
  );
22
26
  }
@@ -6,21 +6,11 @@ export function PageSkeleton() {
6
6
  return (
7
7
  <Flex className={styles.page}>
8
8
  <article className={styles.article}>
9
- <Skeleton width="40%" height="32px" />
10
- <Skeleton.Provider duration={2}>
11
- <Skeleton width="100%" height="16px" />
12
- <Skeleton width="95%" height="16px" />
13
- <Skeleton width="80%" height="16px" />
14
- <Skeleton width="100%" height="16px" />
15
- <Skeleton width="60%" height="16px" />
16
- </Skeleton.Provider>
17
- <Skeleton width="30%" height="24px" />
18
- <Skeleton.Provider duration={2}>
19
- <Skeleton width="100%" height="16px" />
20
- <Skeleton width="90%" height="16px" />
21
- <Skeleton width="100%" height="16px" />
22
- <Skeleton width="70%" height="16px" />
23
- </Skeleton.Provider>
9
+ <Skeleton width="40%" height="var(--rs-line-height-t2)" containerClassName={styles.headerLoader} />
10
+ <Skeleton width="60%" height="var(--rs-line-height-regular)" containerClassName={styles.headerLoader} />
11
+ {[...new Array(20)].map((_, i) => (
12
+ <Skeleton key={i} width="100%" height="var(--rs-line-height-regular)" containerClassName={styles.loader} />
13
+ ))}
24
14
  </article>
25
15
  </Flex>
26
16
  );
@@ -28,7 +28,7 @@ export function VersionSwitcher() {
28
28
  />
29
29
  }
30
30
  >
31
- <Flex gap='small' align='center'>
31
+ <Flex gap={3} align='center'>
32
32
  {active?.label ?? 'Version'}
33
33
  {active?.badge ? (
34
34
  <Badge variant={active.badge.variant} size='micro'>
@@ -43,7 +43,7 @@ export function VersionSwitcher() {
43
43
  key={v.dir ?? '_latest'}
44
44
  onClick={() => navigate(getVersionHomeHref(config, v.dir))}
45
45
  >
46
- <Flex gap='small' align='center'>
46
+ <Flex gap={3} align='center'>
47
47
  {v.label}
48
48
  {v.badge ? (
49
49
  <Badge variant={v.badge.variant} size='micro'>
@@ -29,7 +29,7 @@ export function VersionSwitcher() {
29
29
  />
30
30
  }
31
31
  >
32
- <Flex gap='small' align='center' justify='start'>
32
+ <Flex gap={3} align='center' justify='start'>
33
33
  {active?.label ?? 'Version'}
34
34
  {active?.badge ? (
35
35
  <Badge variant={active.badge.variant} size='micro'>
@@ -44,7 +44,7 @@ export function VersionSwitcher() {
44
44
  key={v.dir ?? '_latest'}
45
45
  onClick={() => navigate(getVersionHomeHref(config, v.dir))}
46
46
  >
47
- <Flex gap='small' align='center'>
47
+ <Flex gap={3} align='center'>
48
48
  {v.label}
49
49
  {v.badge ? (
50
50
  <Badge variant={v.badge.variant} size='micro'>
@@ -10,6 +10,7 @@ export interface Frontmatter {
10
10
  order?: number
11
11
  icon?: string
12
12
  lastModified?: string
13
+ draft?: boolean
13
14
  _readingTime?: number
14
15
  }
15
16
 
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Breadcrumb } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Button } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { CodeBlock } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Dialog } from '@raystack/apsara'
@@ -1,10 +0,0 @@
1
- export { Sidebar } from './sidebar'
2
- export { Table } from './table'
3
- export { Dialog } from './dialog'
4
- export { InputField } from './input-field'
5
- export { Tabs } from './tabs'
6
- export { Breadcrumb } from './breadcrumb'
7
- export { Button } from './button'
8
- export { CodeBlock } from './code-block'
9
- export { Callout } from './callout'
10
- export { Switch } from './switch'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { InputField } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Sidebar } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Switch } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Table } from '@raystack/apsara'
@@ -1,3 +0,0 @@
1
- 'use client'
2
-
3
- export { Tabs } from '@raystack/apsara'