@raystack/chronicle 0.6.1 → 0.7.1

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 (37) hide show
  1. package/dist/cli/index.js +277 -27
  2. package/package.json +5 -1
  3. package/src/components/ui/search.tsx +3 -3
  4. package/src/lib/config.ts +5 -0
  5. package/src/lib/remark-resolve-images.ts +59 -0
  6. package/src/lib/remark-resolve-links.ts +32 -0
  7. package/src/lib/source.ts +20 -4
  8. package/src/pages/ApiLayout.tsx +0 -2
  9. package/src/pages/LandingPage.module.css +137 -24
  10. package/src/pages/LandingPage.tsx +23 -7
  11. package/src/server/api/apis-proxy.ts +2 -2
  12. package/src/server/api/health.ts +1 -1
  13. package/src/server/api/page.ts +2 -2
  14. package/src/server/api/search.ts +4 -4
  15. package/src/server/api/specs.ts +2 -2
  16. package/src/server/entry-server.tsx +4 -1
  17. package/src/server/routes/[...slug].md.ts +1 -2
  18. package/src/server/routes/[version]/llms.txt.ts +1 -2
  19. package/src/server/routes/_content/[...path].ts +40 -0
  20. package/src/server/routes/llms.txt.ts +2 -3
  21. package/src/server/routes/og.tsx +1 -3
  22. package/src/server/routes/robots.txt.ts +2 -3
  23. package/src/server/routes/sitemap.xml.ts +3 -5
  24. package/src/server/vite-config.ts +8 -2
  25. package/src/themes/paper/ChapterNav.module.css +23 -12
  26. package/src/themes/paper/ChapterNav.tsx +1 -17
  27. package/src/themes/paper/Layout.module.css +61 -16
  28. package/src/themes/paper/Layout.tsx +73 -17
  29. package/src/themes/paper/Page.module.css +89 -37
  30. package/src/themes/paper/Page.tsx +89 -53
  31. package/src/themes/paper/ReaderModeContext.tsx +28 -0
  32. package/src/themes/paper/ReadingProgress.tsx +1 -0
  33. package/src/themes/paper/fonts/DepartureMono-Regular.woff2 +0 -0
  34. package/src/themes/registry.ts +1 -1
  35. package/src/types/config.ts +1 -0
  36. package/src/types/content.ts +1 -0
  37. package/src/lib/remark-strip-md-extensions.ts +0 -14
@@ -1,6 +1,5 @@
1
1
  import { cx } from 'class-variance-authority';
2
2
  import type { ReactNode } from 'react';
3
- import { Search } from '@/components/ui/search';
4
3
  import { buildApiPageTree } from '@/lib/api-routes';
5
4
  import { usePageContext } from '@/lib/page-context';
6
5
  import { getTheme } from '@/themes/registry';
@@ -26,7 +25,6 @@ export function ApiLayout({ children }: ApiLayoutProps) {
26
25
  content: styles.content
27
26
  }}
28
27
  >
29
- <Search className={styles.hiddenSearch} />
30
28
  {children}
31
29
  </Layout>
32
30
  );
@@ -1,56 +1,169 @@
1
1
  .root {
2
2
  display: flex;
3
3
  flex-direction: column;
4
- gap: var(--rs-space-8);
5
- padding: var(--rs-space-9) var(--rs-space-7);
6
- max-width: 960px;
7
- margin: 0 auto;
4
+ padding: var(--rs-space-12) var(--rs-space-9);
5
+ width: 100%;
6
+ }
7
+
8
+ .header {
9
+ display: flex;
10
+ align-items: center;
11
+ padding-bottom: var(--rs-space-10);
12
+ border-bottom: 0.5px solid var(--rs-color-border-base-primary);
8
13
  }
9
14
 
10
15
  .title {
11
- font-size: var(--rs-font-size-h3);
12
- font-weight: 600;
13
- color: var(--rs-color-foreground-base-primary);
16
+ flex: 1;
17
+ font-family: var(--paper-font-mono, inherit);
18
+ font-size: 64px;
19
+ line-height: 1.1;
20
+ color: var(--rs-color-foreground-accent-primary);
21
+ text-transform: uppercase;
14
22
  margin: 0;
23
+ font-weight: var(--rs-font-weight-regular);
15
24
  }
16
25
 
17
26
  .description {
27
+ width: 385px;
28
+ flex-shrink: 0;
29
+ font-family: var(--paper-font-body, inherit);
18
30
  font-size: var(--rs-font-size-regular);
19
- color: var(--rs-color-foreground-base-secondary);
31
+ line-height: 1.4;
32
+ color: var(--rs-color-foreground-base-primary);
20
33
  margin: 0;
21
34
  }
22
35
 
23
36
  .grid {
24
- display: grid;
25
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
26
- gap: var(--rs-space-6);
37
+ display: flex;
38
+ flex-wrap: wrap;
39
+ gap: var(--rs-space-7);
40
+ margin-top: var(--rs-space-9);
27
41
  }
28
42
 
29
43
  .card {
30
44
  display: flex;
31
45
  flex-direction: column;
32
- gap: var(--rs-space-3);
33
- padding: var(--rs-space-6);
34
- border: 1px solid var(--rs-color-border-base-primary);
35
- border-radius: var(--rs-radius-3);
46
+ gap: var(--rs-space-7);
47
+ padding: var(--rs-space-2);
36
48
  text-decoration: none;
37
49
  color: inherit;
38
- background: var(--rs-color-background-base-primary);
39
- transition: border-color 0.15s ease, background 0.15s ease;
50
+ width: 360px;
51
+ }
52
+
53
+ .card:hover .cardImage {
54
+ border-color: var(--rs-color-foreground-accent-primary);
55
+ }
56
+
57
+ .cardImage {
58
+ position: relative;
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ width: 100%;
63
+ aspect-ratio: 1.05;
64
+ min-height: 280px;
65
+ max-width: 360px;
66
+ background: var(--rs-color-background-accent-secondary);
67
+ border: 1px solid var(--rs-color-foreground-accent-primary);
68
+ overflow: hidden;
69
+ transition: border-color 0.15s ease;
70
+ }
71
+
72
+ .cardImage::before {
73
+ content: "";
74
+ position: absolute;
75
+ inset: 0;
76
+ background-image:
77
+ radial-gradient(circle, var(--rs-color-foreground-accent-primary) 1px, transparent 1px);
78
+ background-size: 10px 10px;
79
+ opacity: 0.3;
80
+ pointer-events: none;
81
+ }
82
+
83
+ .cardImageLabel {
84
+ position: absolute;
85
+ font-family: var(--paper-font-mono, monospace);
86
+ font-size: var(--rs-font-size-micro);
87
+ text-transform: uppercase;
88
+ color: var(--rs-color-foreground-base-secondary);
89
+ background: var(--rs-color-background-accent-secondary);
90
+ padding: var(--rs-space-1);
91
+ white-space: nowrap;
92
+ }
93
+
94
+ .cardImageLabelTop {
95
+ top: 19px;
96
+ left: -1px;
97
+ writing-mode: vertical-rl;
98
+ transform: rotate(180deg);
40
99
  }
41
100
 
42
- .card:hover {
43
- border-color: var(--rs-color-border-accent-primary);
44
- background: var(--rs-color-background-neutral-secondary);
101
+ .cardImageLabelRight {
102
+ bottom: 19px;
103
+ right: -1px;
104
+ writing-mode: vertical-rl;
105
+ }
106
+
107
+ .cardIcon {
108
+ width: 90%;
109
+ height: 90%;
110
+ min-width: 64px;
111
+ min-height: 64px;
112
+ color: var(--rs-color-foreground-accent-primary);
113
+ opacity: 0.3;
114
+ transition: opacity 0.2s ease, transform 0.2s ease;
115
+ }
116
+
117
+ .card:hover .cardIcon {
118
+ opacity: 0.5;
119
+ transform: scale(1.05);
120
+ }
121
+
122
+ .card:hover .cardImage {
123
+ background: var(--rs-color-background-accent-primary);
124
+ }
125
+
126
+ .cardBody {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: var(--rs-space-3);
130
+ padding-bottom: var(--rs-space-4);
45
131
  }
46
132
 
47
133
  .cardLabel {
134
+ font-family: var(--paper-font-mono, inherit);
48
135
  font-size: var(--rs-font-size-large);
49
- font-weight: 500;
136
+ line-height: 1.4;
137
+ text-transform: uppercase;
138
+ color: var(--rs-color-foreground-base-primary);
50
139
  }
51
140
 
52
- .cardHref {
141
+ .cardDescription {
142
+ font-family: var(--paper-font-body, inherit);
53
143
  font-size: var(--rs-font-size-small);
54
- color: var(--rs-color-foreground-base-secondary);
55
- font-family: var(--rs-font-family-mono);
144
+ line-height: 1.4;
145
+ color: var(--rs-color-foreground-base-primary);
146
+ }
147
+
148
+ @media (max-width: 768px) {
149
+ .header {
150
+ flex-direction: column;
151
+ gap: var(--rs-space-5);
152
+ }
153
+
154
+ .title {
155
+ font-size: var(--rs-font-size-t4);
156
+ }
157
+
158
+ .description {
159
+ width: 100%;
160
+ }
161
+
162
+ .card {
163
+ width: 100%;
164
+ }
165
+
166
+ .cardImage {
167
+ max-width: 100%;
168
+ }
56
169
  }
@@ -1,3 +1,4 @@
1
+ import { FolderIcon } from '@heroicons/react/24/outline';
1
2
  import { Link as RouterLink } from 'react-router';
2
3
  import { getLandingEntries } from '@/lib/config';
3
4
  import { usePageContext } from '@/lib/page-context';
@@ -13,15 +14,30 @@ export function LandingPage() {
13
14
 
14
15
  return (
15
16
  <div className={styles.root}>
16
- <h1 className={styles.title}>{heading}</h1>
17
- {config.site.description ? (
18
- <p className={styles.description}>{config.site.description}</p>
19
- ) : null}
17
+ <div className={styles.header}>
18
+ <h1 className={styles.title}>{heading}</h1>
19
+ {config.site.description ? (
20
+ <p className={styles.description}>{config.site.description}</p>
21
+ ) : null}
22
+ </div>
20
23
  <div className={styles.grid}>
21
- {entries.map((entry) => (
24
+ {entries.map((entry, i) => (
22
25
  <RouterLink key={entry.href} to={entry.href} className={styles.card}>
23
- <span className={styles.cardLabel}>{entry.label}</span>
24
- <span className={styles.cardHref}>{entry.href}</span>
26
+ <div className={styles.cardImage} aria-hidden='true'>
27
+ <span className={`${styles.cardImageLabel} ${styles.cardImageLabelTop}`}>
28
+ Fig_{String(i + 1).padStart(3, '0')}
29
+ </span>
30
+ <span className={`${styles.cardImageLabel} ${styles.cardImageLabelRight}`}>
31
+ [ {entry.label} ]
32
+ </span>
33
+ <FolderIcon className={styles.cardIcon} />
34
+ </div>
35
+ <div className={styles.cardBody}>
36
+ <span className={styles.cardLabel}>{entry.label}</span>
37
+ {entry.description ? (
38
+ <span className={styles.cardDescription}>{entry.description}</span>
39
+ ) : null}
40
+ </div>
25
41
  </RouterLink>
26
42
  ))}
27
43
  </div>
@@ -51,11 +51,11 @@ export default defineHandler(async event => {
51
51
  ? await response.json()
52
52
  : await response.text();
53
53
 
54
- return {
54
+ return Response.json({
55
55
  status: response.status,
56
56
  statusText: response.statusText,
57
57
  body: responseBody
58
- };
58
+ });
59
59
  } catch (error) {
60
60
  const message =
61
61
  error instanceof Error
@@ -1,5 +1,5 @@
1
1
  import { defineHandler } from 'nitro';
2
2
 
3
3
  export default defineHandler(() => {
4
- return { status: 'ok' };
4
+ return Response.json({ status: 'ok' });
5
5
  });
@@ -12,11 +12,11 @@ export default defineHandler(async event => {
12
12
 
13
13
  const nav = await getPageNav(slug);
14
14
 
15
- return {
15
+ return Response.json({
16
16
  frontmatter: extractFrontmatter(page, slug[slug.length - 1]),
17
17
  relativePath: getRelativePath(page),
18
18
  originalPath: getOriginalPath(page),
19
19
  prev: nav.prev,
20
20
  next: nav.next,
21
- };
21
+ });
22
22
  });
@@ -125,7 +125,7 @@ export default defineHandler(async event => {
125
125
 
126
126
  if (!query) {
127
127
  const docs = await getDocs(ctx);
128
- return docs
128
+ return Response.json(docs
129
129
  .filter(d => d.type === 'page')
130
130
  .slice(0, 8)
131
131
  .map(d => ({
@@ -133,13 +133,13 @@ export default defineHandler(async event => {
133
133
  url: d.url,
134
134
  type: d.type,
135
135
  content: d.title
136
- }));
136
+ })));
137
137
  }
138
138
 
139
- return index.search(query).map(r => ({
139
+ return Response.json(index.search(query).map(r => ({
140
140
  id: r.id,
141
141
  url: r.url,
142
142
  type: r.type,
143
143
  content: r.title
144
- }));
144
+ })));
145
145
  });
@@ -15,7 +15,7 @@ export default defineHandler(async event => {
15
15
  }
16
16
 
17
17
  const apiConfigs = getApiConfigsForVersion(config, versionDir);
18
- if (!apiConfigs.length) return [];
18
+ if (!apiConfigs.length) return Response.json([]);
19
19
 
20
- return loadApiSpecs(apiConfigs);
20
+ return Response.json(await loadApiSpecs(apiConfigs));
21
21
  });
@@ -54,7 +54,10 @@ export default {
54
54
  const pageData = page
55
55
  ? {
56
56
  slug: pageSlug,
57
- frontmatter: extractFrontmatter(page, pageSlug[pageSlug.length - 1]),
57
+ frontmatter: {
58
+ ...extractFrontmatter(page, pageSlug[pageSlug.length - 1]),
59
+ _readingTime: mdxModule?._readingTime,
60
+ },
58
61
  content: mdxModule?.default
59
62
  ? React.createElement(mdxModule.default, { components: mdxComponents })
60
63
  : null,
@@ -34,6 +34,5 @@ export default defineHandler(async event => {
34
34
  throw new HTTPError({ status: 404, message: 'Not Found' });
35
35
  }
36
36
 
37
- event.res.headers.set('Content-Type', 'text/markdown; charset=utf-8');
38
- return matter(raw).content;
37
+ return new Response(matter(raw).content, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } });
39
38
  });
@@ -21,6 +21,5 @@ export default defineHandler(async event => {
21
21
  ctx,
22
22
  );
23
23
 
24
- event.res.headers.set('Content-Type', 'text/plain');
25
- return body;
24
+ return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
26
25
  });
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { defineHandler, HTTPError } from 'nitro';
4
+ import { safePath } from '@/server/utils/safe-path';
5
+
6
+ const MIME: Record<string, string> = {
7
+ '.png': 'image/png',
8
+ '.jpg': 'image/jpeg',
9
+ '.jpeg': 'image/jpeg',
10
+ '.gif': 'image/gif',
11
+ '.svg': 'image/svg+xml',
12
+ '.webp': 'image/webp',
13
+ '.ico': 'image/x-icon',
14
+ '.pdf': 'application/pdf',
15
+ };
16
+
17
+ export default defineHandler(async event => {
18
+ const pathname = event.path?.replace(/^\/_content/, '') || '';
19
+ if (!pathname || pathname.endsWith('.md') || pathname.endsWith('.mdx')) {
20
+ throw new HTTPError({ status: 404, message: 'Not Found' });
21
+ }
22
+
23
+ const contentDir = __CHRONICLE_CONTENT_DIR__;
24
+ let filePath: string | null = null;
25
+ try { filePath = safePath(contentDir, pathname); } catch { /* malformed URL encoding */ }
26
+ if (!filePath) throw new HTTPError({ status: 404, message: 'Not Found' });
27
+
28
+ const data = await fs.readFile(filePath).catch(() => null);
29
+ if (!data) throw new HTTPError({ status: 404, message: 'Not Found' });
30
+
31
+ const ext = path.extname(filePath).toLowerCase();
32
+ const contentType = MIME[ext] ?? 'application/octet-stream';
33
+
34
+ return new Response(data, {
35
+ headers: {
36
+ 'Content-Type': contentType,
37
+ 'Cache-Control': 'public, max-age=86400',
38
+ },
39
+ });
40
+ });
@@ -4,7 +4,7 @@ import { buildLlmsTxt } from '@/lib/llms';
4
4
  import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
5
5
  import { LATEST_CONTEXT } from '@/lib/version-source';
6
6
 
7
- export default defineHandler(async event => {
7
+ export default defineHandler(async () => {
8
8
  const config = loadConfig();
9
9
 
10
10
  const pages = await getPagesForVersion(LATEST_CONTEXT);
@@ -14,6 +14,5 @@ export default defineHandler(async event => {
14
14
  LATEST_CONTEXT,
15
15
  );
16
16
 
17
- event.res.headers.set('Content-Type', 'text/plain');
18
- return body;
17
+ return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
19
18
  });
@@ -69,7 +69,5 @@ export default defineHandler(async event => {
69
69
  },
70
70
  );
71
71
 
72
- event.res.headers.set('Content-Type', 'image/svg+xml');
73
- event.res.headers.set('Cache-Control', 'public, max-age=86400');
74
- return svg;
72
+ return new Response(svg, { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' } });
75
73
  });
@@ -1,11 +1,10 @@
1
1
  import { defineHandler } from 'nitro';
2
2
  import { loadConfig } from '@/lib/config';
3
3
 
4
- export default defineHandler(event => {
4
+ export default defineHandler(() => {
5
5
  const config = loadConfig();
6
6
  const sitemap = config.url ? `\nSitemap: ${config.url}/sitemap.xml` : '';
7
7
  const body = `User-agent: *\nAllow: /${sitemap}`;
8
8
 
9
- event.res.headers.set('Content-Type', 'text/plain');
10
- return body;
9
+ return new Response(body, { headers: { 'Content-Type': 'text/plain' } });
11
10
  });
@@ -4,12 +4,11 @@ import { getAllVersions, getApiConfigsForVersion, loadConfig } from '@/lib/confi
4
4
  import { loadApiSpecs } from '@/lib/openapi';
5
5
  import { getPages } from '@/lib/source';
6
6
 
7
- export default defineHandler(async event => {
7
+ export default defineHandler(async () => {
8
8
  const config = loadConfig();
9
9
 
10
10
  if (!config.url) {
11
- event.res.headers.set('Content-Type', 'application/xml');
12
- return '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>';
11
+ return new Response('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"/>', { headers: { 'Content-Type': 'application/xml' } });
13
12
  }
14
13
 
15
14
  const baseUrl = config.url.replace(/\/$/, '');
@@ -43,6 +42,5 @@ export default defineHandler(async event => {
43
42
  ${[...docPages, ...apiPages].join('\n')}
44
43
  </urlset>`;
45
44
 
46
- event.res.headers.set('Content-Type', 'application/xml');
47
- return xml;
45
+ return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
48
46
  });
@@ -7,7 +7,9 @@ import fs from 'node:fs/promises';
7
7
  import path from 'node:path';
8
8
  import remarkDirective from 'remark-directive';
9
9
  import { type InlineConfig } from 'vite';
10
- import remarkStripMdExtensions from '../lib/remark-strip-md-extensions';
10
+ import remarkResolveImages from '../lib/remark-resolve-images';
11
+ import remarkResolveLinks from '../lib/remark-resolve-links';
12
+ import remarkReadingTime from 'remark-reading-time';
11
13
  import remarkUnusedDirectives from '../lib/remark-unused-directives';
12
14
 
13
15
  function resolveOutputDir(projectRoot: string, preset?: string): string {
@@ -55,6 +57,8 @@ export async function createViteConfig(
55
57
  mdx({
56
58
  default: defineFumadocsConfig({
57
59
  mdxOptions: {
60
+ remarkImageOptions: false,
61
+ valueToExport: ['readingTime'],
58
62
  remarkPlugins: [
59
63
  remarkDirective,
60
64
  [remarkDirectiveAdmonition, {
@@ -75,8 +79,10 @@ export async function createViteConfig(
75
79
  },
76
80
  }],
77
81
  remarkUnusedDirectives,
78
- remarkStripMdExtensions,
82
+ remarkResolveLinks,
83
+ remarkResolveImages,
79
84
  remarkMdxMermaid,
85
+ remarkReadingTime,
80
86
  ],
81
87
  },
82
88
  }),
@@ -1,21 +1,25 @@
1
1
  .nav {
2
2
  display: flex;
3
3
  flex-direction: column;
4
- gap: var(--rs-space-5);
4
+ gap: var(--rs-space-8);
5
5
  }
6
6
 
7
7
  .chapter {
8
8
  display: flex;
9
9
  flex-direction: column;
10
10
  gap: var(--rs-space-2);
11
+ margin-top: var(--rs-space-8);
11
12
  }
12
13
 
13
14
  .chapterLabel {
15
+ font-family: var(--paper-font-mono);
14
16
  font-size: var(--rs-font-size-small);
15
- font-weight: 600;
17
+ font-weight: var(--rs-font-weight-medium);
18
+ line-height: var(--rs-line-height-small);
19
+ letter-spacing: var(--rs-letter-spacing-small);
16
20
  text-transform: uppercase;
17
- letter-spacing: 0.05em;
18
- color: var(--rs-color-foreground-base-primary);
21
+ color: var(--rs-color-foreground-base-secondary);
22
+ padding: 0 var(--rs-space-3);
19
23
  white-space: nowrap;
20
24
  overflow: hidden;
21
25
  text-overflow: ellipsis;
@@ -27,30 +31,32 @@
27
31
  margin: 0;
28
32
  display: flex;
29
33
  flex-direction: column;
30
- gap: var(--rs-space-1);
31
- padding-left: var(--rs-space-4);
32
34
  }
33
35
 
34
36
  .link {
35
37
  display: flex;
36
38
  align-items: center;
37
- gap: var(--rs-space-2);
39
+ gap: var(--rs-space-3);
40
+ font-family: var(--paper-font-body);
38
41
  font-size: var(--rs-font-size-small);
39
- color: var(--rs-color-foreground-base-tertiary);
42
+ font-weight: var(--rs-font-weight-regular);
43
+ line-height: var(--rs-line-height-small);
44
+ letter-spacing: var(--rs-letter-spacing-small);
45
+ color: var(--rs-color-foreground-base-primary);
40
46
  text-decoration: none;
41
- padding: var(--rs-space-1) 0;
47
+ padding: var(--rs-space-3);
48
+ border-radius: var(--rs-radius-2);
42
49
  white-space: nowrap;
43
50
  overflow: hidden;
44
51
  text-overflow: ellipsis;
45
52
  }
46
53
 
47
54
  .link:hover {
48
- color: var(--rs-color-foreground-base-primary);
55
+ color: var(--rs-color-foreground-accent-primary);
49
56
  }
50
57
 
51
58
  .active {
52
59
  color: var(--rs-color-foreground-accent-primary);
53
- font-weight: 500;
54
60
  }
55
61
 
56
62
  .icon {
@@ -60,9 +66,14 @@
60
66
  }
61
67
 
62
68
  .subLabel {
69
+ font-family: var(--paper-font-mono);
63
70
  font-size: var(--rs-font-size-small);
64
- font-weight: 500;
71
+ font-weight: var(--rs-font-weight-medium);
72
+ line-height: var(--rs-line-height-small);
73
+ letter-spacing: var(--rs-letter-spacing-small);
74
+ text-transform: uppercase;
65
75
  color: var(--rs-color-foreground-base-secondary);
76
+ padding: 0 var(--rs-space-3);
66
77
  margin-top: var(--rs-space-3);
67
78
  display: block;
68
79
  white-space: nowrap;
@@ -15,23 +15,8 @@ interface ChapterNavProps {
15
15
  tree: Root;
16
16
  }
17
17
 
18
- function buildChapterIndices(
19
- children: Node[]
20
- ): Map<Node, number> {
21
- const indices = new Map<Node, number>();
22
- let index = 0;
23
- for (const item of children) {
24
- if (item.type === 'folder') {
25
- index++;
26
- indices.set(item, index);
27
- }
28
- }
29
- return indices;
30
- }
31
-
32
18
  export function ChapterNav({ tree }: ChapterNavProps) {
33
19
  const { pathname } = useLocation();
34
- const chapterIndices = buildChapterIndices(tree.children);
35
20
 
36
21
  return (
37
22
  <nav className={styles.nav}>
@@ -40,11 +25,10 @@ export function ChapterNav({ tree }: ChapterNavProps) {
40
25
  if (item.type === 'separator') return null;
41
26
 
42
27
  if (item.type === 'folder') {
43
- const chapterIndex = chapterIndices.get(item) ?? 0;
44
28
  return (
45
29
  <li key={item.name?.toString()} className={styles.chapter}>
46
30
  <span className={styles.chapterLabel}>
47
- {String(chapterIndex).padStart(2, '0')}. {item.name}
31
+ {item.name}
48
32
  </span>
49
33
  <ul className={styles.chapterItems}>
50
34
  {item.children.map(child => (