@raystack/chronicle 0.3.0 → 0.4.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 (83) hide show
  1. package/dist/cli/index.js +289 -9931
  2. package/package.json +20 -12
  3. package/src/cli/commands/build.ts +28 -31
  4. package/src/cli/commands/dev.ts +24 -31
  5. package/src/cli/commands/init.ts +38 -132
  6. package/src/cli/commands/serve.ts +36 -55
  7. package/src/cli/commands/start.ts +20 -31
  8. package/src/cli/index.ts +14 -14
  9. package/src/cli/utils/config.ts +25 -26
  10. package/src/cli/utils/index.ts +3 -3
  11. package/src/cli/utils/resolve.ts +7 -3
  12. package/src/cli/utils/scaffold.ts +11 -130
  13. package/src/components/mdx/code.tsx +10 -1
  14. package/src/components/mdx/details.module.css +1 -26
  15. package/src/components/mdx/details.tsx +2 -3
  16. package/src/components/mdx/image.tsx +5 -34
  17. package/src/components/mdx/index.tsx +15 -1
  18. package/src/components/mdx/link.tsx +18 -15
  19. package/src/components/ui/breadcrumbs.tsx +8 -42
  20. package/src/components/ui/search.tsx +63 -51
  21. package/src/lib/api-routes.ts +6 -8
  22. package/src/lib/config.ts +12 -36
  23. package/src/lib/head.tsx +49 -0
  24. package/src/lib/openapi.ts +8 -8
  25. package/src/lib/page-context.tsx +111 -0
  26. package/src/lib/remark-strip-md-extensions.ts +14 -0
  27. package/src/lib/source.ts +139 -63
  28. package/src/pages/ApiLayout.tsx +33 -0
  29. package/src/pages/ApiPage.tsx +73 -0
  30. package/src/pages/DocsLayout.tsx +18 -0
  31. package/src/pages/DocsPage.tsx +43 -0
  32. package/src/pages/NotFound.tsx +17 -0
  33. package/src/server/App.tsx +72 -0
  34. package/src/server/api/apis-proxy.ts +69 -0
  35. package/src/server/api/health.ts +5 -0
  36. package/src/server/api/page/[...slug].ts +18 -0
  37. package/src/server/api/search.ts +118 -0
  38. package/src/server/api/specs.ts +9 -0
  39. package/src/server/build-search-index.ts +117 -0
  40. package/src/server/entry-client.tsx +88 -0
  41. package/src/server/entry-server.tsx +102 -0
  42. package/src/server/routes/llms.txt.ts +21 -0
  43. package/src/server/routes/og.tsx +75 -0
  44. package/src/server/routes/robots.txt.ts +11 -0
  45. package/src/server/routes/sitemap.xml.ts +40 -0
  46. package/src/server/utils/safe-path.ts +17 -0
  47. package/src/server/vite-config.ts +129 -0
  48. package/src/themes/default/Layout.tsx +78 -48
  49. package/src/themes/default/Page.module.css +44 -0
  50. package/src/themes/default/Page.tsx +9 -11
  51. package/src/themes/default/Toc.tsx +25 -39
  52. package/src/themes/default/index.ts +7 -9
  53. package/src/themes/paper/ChapterNav.tsx +64 -45
  54. package/src/themes/paper/Layout.module.css +1 -1
  55. package/src/themes/paper/Layout.tsx +24 -12
  56. package/src/themes/paper/Page.module.css +16 -4
  57. package/src/themes/paper/Page.tsx +56 -63
  58. package/src/themes/paper/ReadingProgress.tsx +160 -139
  59. package/src/themes/paper/index.ts +5 -5
  60. package/src/themes/registry.ts +14 -7
  61. package/src/types/content.ts +5 -21
  62. package/src/types/globals.d.ts +4 -0
  63. package/src/types/theme.ts +4 -3
  64. package/tsconfig.json +2 -3
  65. package/next.config.mjs +0 -10
  66. package/source.config.ts +0 -51
  67. package/src/app/[[...slug]]/layout.tsx +0 -15
  68. package/src/app/[[...slug]]/page.tsx +0 -106
  69. package/src/app/api/apis-proxy/route.ts +0 -59
  70. package/src/app/api/health/route.ts +0 -3
  71. package/src/app/api/search/route.ts +0 -90
  72. package/src/app/apis/[[...slug]]/layout.tsx +0 -26
  73. package/src/app/apis/[[...slug]]/page.tsx +0 -117
  74. package/src/app/layout.tsx +0 -57
  75. package/src/app/llms-full.txt/route.ts +0 -18
  76. package/src/app/llms.txt/route.ts +0 -15
  77. package/src/app/og/route.tsx +0 -62
  78. package/src/app/providers.tsx +0 -8
  79. package/src/app/robots.ts +0 -10
  80. package/src/app/sitemap.ts +0 -29
  81. package/src/cli/utils/process.ts +0 -7
  82. package/src/themes/default/font.ts +0 -6
  83. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
@@ -1,7 +1,12 @@
1
1
  .main {
2
2
  --paper-navbar-height: 40px;
3
3
  --paper-navbar-padding: var(--rs-space-3);
4
- --paper-navbar-total: calc(var(--paper-navbar-height) + var(--paper-navbar-padding) * 2 + 1px);
4
+ --paper-navbar-total: calc(
5
+ var(--paper-navbar-height) +
6
+ var(--paper-navbar-padding) *
7
+ 2 +
8
+ 1px
9
+ );
5
10
 
6
11
  flex: 1;
7
12
  max-width: 1024px;
@@ -52,7 +57,7 @@
52
57
  }
53
58
 
54
59
  .breadcrumb {
55
- font-family: 'SF Mono', 'Fira Code', monospace;
60
+ font-family: "SF Mono", "Fira Code", monospace;
56
61
  font-size: var(--rs-font-size-small);
57
62
  text-transform: uppercase;
58
63
  letter-spacing: 0.05em;
@@ -94,13 +99,15 @@
94
99
  }
95
100
 
96
101
  .content {
97
- font-family: Georgia, 'Times New Roman', serif;
102
+ font-family: Georgia, "Times New Roman", serif;
98
103
  line-height: 1.8;
99
104
  background: var(--rs-color-background-base-primary);
100
105
  padding: var(--rs-space-9);
101
106
  border-left: 1px solid var(--rs-color-border-base-primary);
102
107
  border-right: 1px solid var(--rs-color-border-base-primary);
103
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04);
108
+ box-shadow:
109
+ 0 1px 3px rgba(0, 0, 0, 0.08),
110
+ 0 4px 12px rgba(0, 0, 0, 0.04);
104
111
  margin-bottom: var(--rs-space-9);
105
112
  }
106
113
 
@@ -167,6 +174,11 @@
167
174
  margin-bottom: var(--rs-space-3);
168
175
  }
169
176
 
177
+ .content img {
178
+ max-width: 100%;
179
+ height: auto;
180
+ }
181
+
170
182
  .content blockquote {
171
183
  margin: 1rem 0;
172
184
  padding-left: 1rem;
@@ -1,78 +1,71 @@
1
- 'use client'
2
-
3
- import { useMemo } from 'react'
4
- import { usePathname } from 'next/navigation'
5
- import NextLink from 'next/link'
6
- import { Flex } from '@raystack/apsara'
7
- import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'
8
- import type { ThemePageProps, PageTreeItem } from '@/types'
9
- import { Search } from '@/components/ui/search'
10
- import { ReadingProgress } from './ReadingProgress'
11
- import styles from './Page.module.css'
12
-
13
- function flattenTree(items: PageTreeItem[]): PageTreeItem[] {
14
- const result: PageTreeItem[] = []
15
- for (const item of items) {
16
- if (item.type === 'page' && item.url) result.push(item)
17
- if (item.children) result.push(...flattenTree(item.children))
18
- }
19
- return result
20
- }
21
-
22
- function findBreadcrumb(items: PageTreeItem[], slug: string[]): { label: string; href: string }[] {
23
- const result: { label: string; href: string }[] = []
24
- for (let i = 0; i < slug.length; i++) {
25
- const path = '/' + slug.slice(0, i + 1).join('/')
26
- const found = findInTree(items, path)
27
- result.push({ label: found?.name ?? slug[i], href: path })
28
- }
29
- return result
30
- }
31
-
32
- function findInTree(items: PageTreeItem[], path: string): PageTreeItem | undefined {
33
- for (const item of items) {
34
- if (item.url === path) return item
35
- if (item.children) {
36
- const found = findInTree(item.children, path)
37
- if (found) return found
38
- }
39
- }
40
- return undefined
41
- }
1
+ import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
2
+ import { Flex } from '@raystack/apsara';
3
+ import { useMemo } from 'react';
4
+ import { Link as RouterLink, useLocation } from 'react-router';
5
+ import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
6
+ import { flattenTree } from 'fumadocs-core/page-tree';
7
+ import { Search } from '@/components/ui/search';
8
+ import type { ThemePageProps } from '@/types';
9
+ import styles from './Page.module.css';
10
+ import { ReadingProgress } from './ReadingProgress';
42
11
 
43
12
  export function Page({ page, config, tree }: ThemePageProps) {
44
- const pathname = usePathname()
13
+ const { pathname } = useLocation();
45
14
 
46
15
  const { prev, next, crumbs } = useMemo(() => {
47
- const pages = flattenTree(tree.children)
48
- const currentIndex = pages.findIndex((p) => p.url === pathname)
16
+ const pages = flattenTree(tree.children);
17
+ const currentIndex = pages.findIndex(p => p.url === pathname);
18
+ const breadcrumbItems = getBreadcrumbItems(
19
+ pathname,
20
+ tree,
21
+ { includePage: true }
22
+ );
49
23
  return {
50
24
  prev: currentIndex > 0 ? pages[currentIndex - 1] : null,
51
25
  next: currentIndex < pages.length - 1 ? pages[currentIndex + 1] : null,
52
- crumbs: findBreadcrumb(tree.children, page.slug),
53
- }
54
- }, [tree, pathname, page.slug])
26
+ crumbs: breadcrumbItems.map(item => ({
27
+ label: item.name,
28
+ href: item.url ?? pathname,
29
+ })),
30
+ };
31
+ }, [tree, pathname]);
55
32
 
56
33
  return (
57
34
  <>
58
35
  <main className={styles.main}>
59
- <Flex align="center" className={styles.navbar}>
60
- <Flex align="center" gap="small" className={styles.navLeft}>
36
+ <Flex align='center' className={styles.navbar}>
37
+ <Flex align='center' gap='small' className={styles.navLeft}>
61
38
  {prev ? (
62
- <NextLink href={prev.url!} className={styles.arrow} aria-label="Previous page">
39
+ <RouterLink
40
+ to={prev.url}
41
+ className={styles.arrow}
42
+ aria-label='Previous page'
43
+ >
63
44
  <ChevronLeftIcon width={14} height={14} />
64
- </NextLink>
45
+ </RouterLink>
65
46
  ) : (
66
- <button disabled className={styles.arrowDisabled} aria-label="Previous page">
47
+ <button
48
+ disabled
49
+ className={styles.arrowDisabled}
50
+ aria-label='Previous page'
51
+ >
67
52
  <ChevronLeftIcon width={14} height={14} />
68
53
  </button>
69
54
  )}
70
55
  {next ? (
71
- <NextLink href={next.url!} className={styles.arrow} aria-label="Next page">
56
+ <RouterLink
57
+ to={next.url}
58
+ className={styles.arrow}
59
+ aria-label='Next page'
60
+ >
72
61
  <ChevronRightIcon width={14} height={14} />
73
- </NextLink>
62
+ </RouterLink>
74
63
  ) : (
75
- <button disabled className={styles.arrowDisabled} aria-label="Next page">
64
+ <button
65
+ disabled
66
+ className={styles.arrowDisabled}
67
+ aria-label='Next page'
68
+ >
76
69
  <ChevronRightIcon width={14} height={14} />
77
70
  </button>
78
71
  )}
@@ -83,25 +76,25 @@ export function Page({ page, config, tree }: ThemePageProps) {
83
76
  {i === crumbs.length - 1 ? (
84
77
  <span className={styles.crumbActive}>{crumb.label}</span>
85
78
  ) : (
86
- <NextLink href={crumb.href} className={styles.crumbLink}>
79
+ <RouterLink to={crumb.href} className={styles.crumbLink}>
87
80
  {crumb.label}
88
- </NextLink>
81
+ </RouterLink>
89
82
  )}
90
83
  </span>
91
84
  ))}
92
85
  </nav>
93
86
  </Flex>
94
- <Flex align="center" className={styles.navRight}>
95
- {config.search?.enabled && <Search className={styles.searchButton} />}
87
+ <Flex align='center' className={styles.navRight}>
88
+ {config.search?.enabled && (
89
+ <Search className={styles.searchButton} />
90
+ )}
96
91
  </Flex>
97
92
  </Flex>
98
93
  <article className={styles.article} data-article-content>
99
- <div className={styles.content}>
100
- {page.content}
101
- </div>
94
+ <div className={styles.content}>{page.content}</div>
102
95
  </article>
103
96
  </main>
104
97
  <ReadingProgress items={page.toc} />
105
98
  </>
106
- )
99
+ );
107
100
  }
@@ -1,46 +1,46 @@
1
- 'use client'
1
+ 'use client';
2
2
 
3
- import { cx } from 'class-variance-authority'
4
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
- import type { TocItem } from '@/types'
6
- import styles from './ReadingProgress.module.css'
3
+ import { cx } from 'class-variance-authority';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import type { TOCItemType } from 'fumadocs-core/toc';
6
+ import styles from './ReadingProgress.module.css';
7
7
 
8
8
  interface Heading {
9
- title: string
10
- level: number
11
- id: string
12
- url: string
13
- yPosition: number
9
+ title: string;
10
+ level: number;
11
+ id: string;
12
+ url: string;
13
+ yPosition: number;
14
14
  }
15
15
 
16
- const ARTICLE_SELECTOR = '[data-article-content]'
17
- const TICK_HEIGHT = 20
18
- const NAV_HEIGHT = 60
16
+ const ARTICLE_SELECTOR = '[data-article-content]';
17
+ const TICK_HEIGHT = 20;
18
+ const NAV_HEIGHT = 60;
19
19
 
20
20
  function calculateTickBounds(containerHeight: number) {
21
- const numTicks = Math.floor(containerHeight / TICK_HEIGHT) + 1
22
- const maxPosition = (numTicks - 1) * TICK_HEIGHT
23
- return { numTicks, maxPosition }
21
+ const numTicks = Math.floor(containerHeight / TICK_HEIGHT) + 1;
22
+ const maxPosition = (numTicks - 1) * TICK_HEIGHT;
23
+ return { numTicks, maxPosition };
24
24
  }
25
25
 
26
26
  function snapToTick(value: number, maxPosition: number): number {
27
- const snapped = Math.round(value / TICK_HEIGHT) * TICK_HEIGHT
28
- return Math.max(0, Math.min(snapped, maxPosition))
27
+ const snapped = Math.round(value / TICK_HEIGHT) * TICK_HEIGHT;
28
+ return Math.max(0, Math.min(snapped, maxPosition));
29
29
  }
30
30
 
31
31
  function resolveOverlaps(headings: Heading[], maxPosition: number): Heading[] {
32
- if (headings.length <= 1) return headings
32
+ if (headings.length <= 1) return headings;
33
33
 
34
- const resolved: Heading[] = []
35
- let lastUsedPos = -TICK_HEIGHT
34
+ const resolved: Heading[] = [];
35
+ let lastUsedPos = -TICK_HEIGHT;
36
36
 
37
37
  for (const heading of headings) {
38
- let newPos = heading.yPosition
38
+ let newPos = heading.yPosition;
39
39
  if (newPos <= lastUsedPos) {
40
- newPos = lastUsedPos + TICK_HEIGHT
40
+ newPos = lastUsedPos + TICK_HEIGHT;
41
41
  }
42
- resolved.push({ ...heading, yPosition: newPos })
43
- lastUsedPos = newPos
42
+ resolved.push({ ...heading, yPosition: newPos });
43
+ lastUsedPos = newPos;
44
44
  }
45
45
 
46
46
  // Backward pass: clamp-and-shift to prevent overlapping positions
@@ -48,193 +48,212 @@ function resolveOverlaps(headings: Heading[], maxPosition: number): Heading[] {
48
48
  const maxAllowed =
49
49
  i === resolved.length - 1
50
50
  ? maxPosition
51
- : resolved[i + 1].yPosition - TICK_HEIGHT
51
+ : resolved[i + 1].yPosition - TICK_HEIGHT;
52
52
 
53
- const clampedPos = Math.max(0, maxAllowed)
53
+ const clampedPos = Math.max(0, maxAllowed);
54
54
  if (resolved[i].yPosition > clampedPos) {
55
- resolved[i] = { ...resolved[i], yPosition: clampedPos }
55
+ resolved[i] = { ...resolved[i], yPosition: clampedPos };
56
56
  for (let j = i - 1; j >= 0; j--) {
57
- const upperBound = resolved[j + 1].yPosition - TICK_HEIGHT
57
+ const upperBound = resolved[j + 1].yPosition - TICK_HEIGHT;
58
58
  if (resolved[j].yPosition > upperBound) {
59
- resolved[j] = { ...resolved[j], yPosition: Math.max(0, upperBound) }
59
+ resolved[j] = { ...resolved[j], yPosition: Math.max(0, upperBound) };
60
60
  } else {
61
- break
61
+ break;
62
62
  }
63
63
  }
64
64
  }
65
65
  }
66
66
 
67
- return resolved
67
+ return resolved;
68
68
  }
69
69
 
70
70
  interface ReadingProgressProps {
71
- items: TocItem[]
71
+ items: TOCItemType[];
72
72
  }
73
73
 
74
74
  export function ReadingProgress({ items }: ReadingProgressProps) {
75
- const [headings, setHeadings] = useState<Heading[]>([])
76
- const [containerHeight, setContainerHeight] = useState<number>(0)
77
- const [ready, setReady] = useState<boolean>(false)
78
- const [isScrollable, setIsScrollable] = useState<boolean>(true)
79
- const containerRef = useRef<HTMLDivElement>(null)
80
- const scrollMarkerRef = useRef<HTMLDivElement>(null)
81
- const scrollPosRef = useRef<number>(0)
75
+ const [headings, setHeadings] = useState<Heading[]>([]);
76
+ const [containerHeight, setContainerHeight] = useState<number>(0);
77
+ const [ready, setReady] = useState<boolean>(false);
78
+ const [isScrollable, setIsScrollable] = useState<boolean>(true);
79
+ const containerRef = useRef<HTMLDivElement>(null);
80
+ const scrollMarkerRef = useRef<HTMLDivElement>(null);
81
+ const scrollPosRef = useRef<number>(0);
82
82
 
83
83
  const { numTicks, maxPosition } = useMemo(
84
84
  () => calculateTickBounds(containerHeight),
85
85
  [containerHeight]
86
- )
86
+ );
87
87
 
88
88
  const recalcHeadings = useCallback(() => {
89
- const article = document.querySelector(ARTICLE_SELECTOR)
90
- const container = containerRef.current
91
- if (!article || !container || !items.length) return
89
+ const article = document.querySelector(ARTICLE_SELECTOR);
90
+ const container = containerRef.current;
91
+ if (!article || !container || !items.length) return;
92
92
 
93
- const articleBox = article.getBoundingClientRect()
94
- const containerBox = container.getBoundingClientRect()
95
- const articleTop = articleBox.top + window.scrollY
93
+ const articleBox = article.getBoundingClientRect();
94
+ const containerBox = container.getBoundingClientRect();
95
+ const articleTop = articleBox.top + window.scrollY;
96
96
 
97
- const hasScroll = articleBox.height > window.innerHeight
98
- setIsScrollable(hasScroll)
99
- setContainerHeight(containerBox.height)
97
+ const hasScroll = articleBox.height > window.innerHeight;
98
+ setIsScrollable(hasScroll);
99
+ setContainerHeight(containerBox.height);
100
100
 
101
- const { maxPosition: maxPos } = calculateTickBounds(containerBox.height)
101
+ const { maxPosition: maxPos } = calculateTickBounds(containerBox.height);
102
102
 
103
103
  const mapped = items
104
- .map((tocItem) => {
105
- const id = tocItem.url.startsWith('#') ? tocItem.url.slice(1) : tocItem.url
106
- const node = document.getElementById(id)
107
- if (!node) return null
108
-
109
- const { top } = node.getBoundingClientRect()
110
- const headingPosInArticle = top + window.scrollY - articleTop
111
- const progress = headingPosInArticle / articleBox.height
112
- const rawY = progress * maxPos
113
- const yPos = snapToTick(rawY, maxPos)
104
+ .map(tocItem => {
105
+ const id = tocItem.url.startsWith('#')
106
+ ? tocItem.url.slice(1)
107
+ : tocItem.url;
108
+ const node = document.getElementById(id);
109
+ if (!node) return null;
110
+
111
+ const { top } = node.getBoundingClientRect();
112
+ const headingPosInArticle = top + window.scrollY - articleTop;
113
+ const progress = headingPosInArticle / articleBox.height;
114
+ const rawY = progress * maxPos;
115
+ const yPos = snapToTick(rawY, maxPos);
114
116
 
115
117
  return {
116
118
  title: tocItem.title,
117
119
  level: tocItem.depth,
118
120
  id,
119
121
  url: tocItem.url,
120
- yPosition: yPos,
121
- }
122
+ yPosition: yPos
123
+ };
122
124
  })
123
- .filter((item): item is Heading => item !== null)
125
+ .filter((item): item is Heading => item !== null);
124
126
 
125
- const resolvedItems = resolveOverlaps(mapped, maxPos)
126
- setHeadings(resolvedItems)
127
- }, [items])
127
+ const resolvedItems = resolveOverlaps(mapped, maxPos);
128
+ setHeadings(resolvedItems);
129
+ }, [items]);
128
130
 
129
131
  // Imperative DOM updates to avoid React re-render on every scroll event
130
132
  const handleScroll = useCallback(() => {
131
- const article = document.querySelector(ARTICLE_SELECTOR)
132
- const container = containerRef.current
133
- const scrollMarker = scrollMarkerRef.current
134
- if (!article || !container || !scrollMarker) return
133
+ const article = document.querySelector(ARTICLE_SELECTOR);
134
+ const container = containerRef.current;
135
+ const scrollMarker = scrollMarkerRef.current;
136
+ if (!article || !container || !scrollMarker) return;
135
137
 
136
- const { top, height } = article.getBoundingClientRect()
137
- const { height: cHeight } = container.getBoundingClientRect()
138
- const viewportHeight = window.innerHeight
139
- const { maxPosition: maxPos } = calculateTickBounds(cHeight)
138
+ const { top, height } = article.getBoundingClientRect();
139
+ const { height: cHeight } = container.getBoundingClientRect();
140
+ const viewportHeight = window.innerHeight;
141
+ const { maxPosition: maxPos } = calculateTickBounds(cHeight);
140
142
 
141
- let newScrollPos: number
143
+ let newScrollPos: number;
142
144
  if (top > 0) {
143
- newScrollPos = 0
145
+ newScrollPos = 0;
144
146
  } else {
145
- const scrolled = Math.abs(top)
146
- const scrollRange = height - viewportHeight
147
- const progress = scrollRange > 0 ? Math.min(1, scrolled / scrollRange) : 0
148
- const rawPos = progress * maxPos
149
- newScrollPos = snapToTick(rawPos, maxPos)
147
+ const scrolled = Math.abs(top);
148
+ const scrollRange = height - viewportHeight;
149
+ const progress =
150
+ scrollRange > 0 ? Math.min(1, scrolled / scrollRange) : 0;
151
+ const rawPos = progress * maxPos;
152
+ newScrollPos = snapToTick(rawPos, maxPos);
150
153
  }
151
154
 
152
- const prevScrollPos = scrollPosRef.current
155
+ const prevScrollPos = scrollPosRef.current;
153
156
  if (newScrollPos !== prevScrollPos) {
154
- scrollPosRef.current = newScrollPos
155
- scrollMarker.style.top = `${newScrollPos}px`
157
+ scrollPosRef.current = newScrollPos;
158
+ scrollMarker.style.top = `${newScrollPos}px`;
156
159
 
157
- const textEl = scrollMarker.querySelector('[data-scroll-text]')
160
+ const textEl = scrollMarker.querySelector('[data-scroll-text]');
158
161
  if (textEl) {
159
- textEl.textContent = (maxPos > 0 ? newScrollPos / maxPos : 0).toFixed(2)
162
+ textEl.textContent = (maxPos > 0 ? newScrollPos / maxPos : 0).toFixed(
163
+ 2
164
+ );
160
165
  }
161
166
 
162
- const tickLines = container.querySelectorAll('[data-tick-line]')
163
- tickLines.forEach((tick) => {
164
- const tickPos = Number(tick.getAttribute('data-tick-pos'))
167
+ const tickLines = container.querySelectorAll('[data-tick-line]');
168
+ tickLines.forEach(tick => {
169
+ const tickPos = Number(tick.getAttribute('data-tick-pos'));
165
170
  if (tickPos < newScrollPos) {
166
- tick.classList.remove(styles.tickLineAfter)
167
- tick.classList.add(styles.tickLineBefore)
171
+ tick.classList.remove(styles.tickLineAfter);
172
+ tick.classList.add(styles.tickLineBefore);
168
173
  } else {
169
- tick.classList.remove(styles.tickLineBefore)
170
- tick.classList.add(styles.tickLineAfter)
174
+ tick.classList.remove(styles.tickLineBefore);
175
+ tick.classList.add(styles.tickLineAfter);
171
176
  }
172
- })
177
+ });
173
178
  }
174
- }, [])
179
+ }, []);
175
180
 
176
181
  useEffect(() => {
177
- recalcHeadings()
178
- handleScroll()
179
- setReady(true)
182
+ recalcHeadings();
183
+ handleScroll();
184
+ setReady(true);
180
185
 
181
- const article = document.querySelector(ARTICLE_SELECTOR)
182
- let ro: ResizeObserver | undefined
186
+ const article = document.querySelector(ARTICLE_SELECTOR);
187
+ let ro: ResizeObserver | undefined;
183
188
  if (article) {
184
189
  ro = new ResizeObserver(() => {
185
- recalcHeadings()
186
- handleScroll()
187
- })
188
- ro.observe(article)
190
+ recalcHeadings();
191
+ handleScroll();
192
+ });
193
+ ro.observe(article);
189
194
  }
190
195
 
191
- window.addEventListener('resize', recalcHeadings)
192
- window.addEventListener('scroll', handleScroll, { passive: true })
196
+ window.addEventListener('resize', recalcHeadings);
197
+ window.addEventListener('scroll', handleScroll, { passive: true });
193
198
 
194
199
  return () => {
195
- ro?.disconnect()
196
- window.removeEventListener('resize', recalcHeadings)
197
- window.removeEventListener('scroll', handleScroll)
198
- }
199
- }, [recalcHeadings, handleScroll])
200
+ ro?.disconnect();
201
+ window.removeEventListener('resize', recalcHeadings);
202
+ window.removeEventListener('scroll', handleScroll);
203
+ };
204
+ }, [recalcHeadings, handleScroll]);
200
205
 
201
206
  const scrollToTick = (y: number): void => {
202
- const article = document.querySelector(ARTICLE_SELECTOR)
203
- if (!article || maxPosition === 0) return
207
+ const article = document.querySelector(ARTICLE_SELECTOR);
208
+ if (!article || maxPosition === 0) return;
204
209
 
205
- const articleBox = article.getBoundingClientRect()
206
- const articleTop = articleBox.top + window.scrollY
207
- const progress = y / maxPosition
208
- const articlePos = progress * articleBox.height
209
- const targetScroll = articleTop + articlePos - NAV_HEIGHT
210
+ const articleBox = article.getBoundingClientRect();
211
+ const articleTop = articleBox.top + window.scrollY;
212
+ const progress = y / maxPosition;
213
+ const articlePos = progress * articleBox.height;
214
+ const targetScroll = articleTop + articlePos - NAV_HEIGHT;
210
215
 
211
- window.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' })
212
- }
216
+ window.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' });
217
+ };
213
218
 
214
219
  const scrollToHeading = (id: string): void => {
215
- const element = document.getElementById(id)
216
- if (!element) return
220
+ const element = document.getElementById(id);
221
+ if (!element) return;
217
222
 
218
- const elementTop = element.getBoundingClientRect().top + window.scrollY
219
- window.scrollTo({ top: Math.max(0, elementTop - NAV_HEIGHT), behavior: 'smooth' })
220
- }
223
+ const elementTop = element.getBoundingClientRect().top + window.scrollY;
224
+ window.scrollTo({
225
+ top: Math.max(0, elementTop - NAV_HEIGHT),
226
+ behavior: 'smooth'
227
+ });
228
+ };
221
229
 
222
230
  const ticks = useMemo(
223
231
  () => Array.from({ length: numTicks }, (_, i) => i * TICK_HEIGHT),
224
232
  [numTicks]
225
- )
233
+ );
226
234
 
227
235
  if (!isScrollable || ticks.length < 2) {
228
- return <div ref={containerRef} className={styles.container} />
236
+ return <div ref={containerRef} className={styles.container} />;
229
237
  }
230
238
 
231
239
  return (
232
240
  <div ref={containerRef} className={styles.container}>
233
241
  <div className={styles.inner}>
234
242
  {ticks.map((y, i) => (
235
- <div key={`tick-${i}`} style={{ top: `${y}px` }} className={styles.tickContainer}>
236
- <div data-tick-line data-tick-pos={y} className={cx(styles.tickLine, styles.tickLineAfter)} />
237
- <div className={styles.tickClickable} onClick={() => scrollToTick(y)} />
243
+ <div
244
+ key={`tick-${i}`}
245
+ style={{ top: `${y}px` }}
246
+ className={styles.tickContainer}
247
+ >
248
+ <div
249
+ data-tick-line
250
+ data-tick-pos={y}
251
+ className={cx(styles.tickLine, styles.tickLineAfter)}
252
+ />
253
+ <div
254
+ className={styles.tickClickable}
255
+ onClick={() => scrollToTick(y)}
256
+ />
238
257
  </div>
239
258
  ))}
240
259
 
@@ -246,16 +265,16 @@ export function ReadingProgress({ items }: ReadingProgressProps) {
246
265
  top: `${h.yPosition - 6}px`,
247
266
  right: '24px',
248
267
  zIndex: h.level < 4 ? 10 : 0,
249
- transitionDelay: `${50 * i}ms`,
268
+ transitionDelay: `${50 * i}ms`
250
269
  }}
251
270
  >
252
271
  <a
253
272
  href={h.url}
254
273
  className={styles.headingLink}
255
- onClick={(e) => {
256
- e.preventDefault()
257
- e.stopPropagation()
258
- scrollToHeading(h.id)
274
+ onClick={e => {
275
+ e.preventDefault();
276
+ e.stopPropagation();
277
+ scrollToHeading(h.id);
259
278
  }}
260
279
  >
261
280
  {h.title}
@@ -270,7 +289,7 @@ export function ReadingProgress({ items }: ReadingProgressProps) {
270
289
  className={styles.connectingLine}
271
290
  style={{
272
291
  top: `${h.yPosition}px`,
273
- width: `${Math.max(4, (3 - h.level) * 4 + 12)}px`,
292
+ width: `${Math.max(4, (3 - h.level) * 4 + 12)}px`
274
293
  }}
275
294
  />
276
295
  ))}
@@ -279,7 +298,9 @@ export function ReadingProgress({ items }: ReadingProgressProps) {
279
298
  ref={scrollMarkerRef}
280
299
  className={cx(
281
300
  styles.scrollMarkerContainer,
282
- ready ? styles.scrollMarkerContainerReady : styles.scrollMarkerContainerNotReady
301
+ ready
302
+ ? styles.scrollMarkerContainerReady
303
+ : styles.scrollMarkerContainerNotReady
283
304
  )}
284
305
  style={{ top: '0px' }}
285
306
  >
@@ -290,5 +311,5 @@ export function ReadingProgress({ items }: ReadingProgressProps) {
290
311
  </div>
291
312
  </div>
292
313
  </div>
293
- )
314
+ );
294
315
  }
@@ -1,8 +1,8 @@
1
- import { Layout } from './Layout'
2
- import { Page } from './Page'
3
- import type { Theme } from '@/types'
1
+ import type { Theme } from '@/types';
2
+ import { Layout } from './Layout';
3
+ import { Page } from './Page';
4
4
 
5
5
  export const paperTheme: Theme = {
6
6
  Layout,
7
- Page,
8
- }
7
+ Page
8
+ };