@raystack/chronicle 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +425 -9937
- package/package.json +19 -10
- package/src/cli/commands/build.ts +33 -31
- package/src/cli/commands/dev.ts +23 -31
- package/src/cli/commands/init.ts +38 -132
- package/src/cli/commands/serve.ts +41 -55
- package/src/cli/commands/start.ts +20 -31
- package/src/cli/index.ts +14 -14
- package/src/cli/utils/config.ts +58 -30
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/resolve.ts +7 -3
- package/src/cli/utils/scaffold.ts +11 -130
- package/src/components/mdx/code.tsx +10 -1
- package/src/components/mdx/details.module.css +1 -26
- package/src/components/mdx/details.tsx +2 -3
- package/src/components/mdx/image.tsx +5 -34
- package/src/components/mdx/index.tsx +15 -1
- package/src/components/mdx/link.tsx +18 -15
- package/src/components/ui/breadcrumbs.tsx +8 -42
- package/src/components/ui/search.tsx +63 -51
- package/src/lib/api-routes.ts +6 -8
- package/src/lib/config.ts +12 -36
- package/src/lib/head.tsx +49 -0
- package/src/lib/openapi.ts +8 -8
- package/src/lib/page-context.tsx +111 -0
- package/src/lib/remark-strip-md-extensions.ts +14 -0
- package/src/lib/source.ts +139 -63
- package/src/pages/ApiLayout.tsx +33 -0
- package/src/pages/ApiPage.tsx +73 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +17 -0
- package/src/server/App.tsx +72 -0
- package/src/server/api/apis-proxy.ts +69 -0
- package/src/server/api/health.ts +5 -0
- package/src/server/api/page/[...slug].ts +18 -0
- package/src/server/api/search.ts +118 -0
- package/src/server/api/specs.ts +9 -0
- package/src/server/build-search-index.ts +117 -0
- package/src/server/entry-client.tsx +88 -0
- package/src/server/entry-server.tsx +102 -0
- package/src/server/routes/llms.txt.ts +21 -0
- package/src/server/routes/og.tsx +75 -0
- package/src/server/routes/robots.txt.ts +11 -0
- package/src/server/routes/sitemap.xml.ts +40 -0
- package/src/server/utils/safe-path.ts +17 -0
- package/src/server/vite-config.ts +133 -0
- package/src/themes/default/Layout.tsx +78 -48
- package/src/themes/default/Page.module.css +44 -0
- package/src/themes/default/Page.tsx +9 -11
- package/src/themes/default/Toc.tsx +25 -39
- package/src/themes/default/index.ts +7 -9
- package/src/themes/paper/ChapterNav.tsx +64 -45
- package/src/themes/paper/Layout.module.css +1 -1
- package/src/themes/paper/Layout.tsx +24 -12
- package/src/themes/paper/Page.module.css +16 -4
- package/src/themes/paper/Page.tsx +56 -63
- package/src/themes/paper/ReadingProgress.tsx +160 -139
- package/src/themes/paper/index.ts +5 -5
- package/src/themes/registry.ts +14 -7
- package/src/types/config.ts +86 -67
- package/src/types/content.ts +5 -21
- package/src/types/globals.d.ts +4 -0
- package/src/types/theme.ts +4 -3
- package/tsconfig.json +2 -3
- package/next.config.mjs +0 -10
- package/source.config.ts +0 -51
- package/src/app/[[...slug]]/layout.tsx +0 -15
- package/src/app/[[...slug]]/page.tsx +0 -106
- package/src/app/api/apis-proxy/route.ts +0 -59
- package/src/app/api/health/route.ts +0 -3
- package/src/app/api/search/route.ts +0 -90
- package/src/app/apis/[[...slug]]/layout.tsx +0 -26
- package/src/app/apis/[[...slug]]/page.tsx +0 -117
- package/src/app/layout.tsx +0 -57
- package/src/app/llms-full.txt/route.ts +0 -18
- package/src/app/llms.txt/route.ts +0 -15
- package/src/app/og/route.tsx +0 -62
- package/src/app/providers.tsx +0 -8
- package/src/app/robots.ts +0 -10
- package/src/app/sitemap.ts +0 -29
- package/src/cli/utils/process.ts +0 -7
- package/src/themes/default/font.ts +0 -6
- /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(
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
import { useMemo } from 'react'
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import type { ThemePageProps
|
|
9
|
-
import
|
|
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 =
|
|
13
|
+
const { pathname } = useLocation();
|
|
45
14
|
|
|
46
15
|
const { prev, next, crumbs } = useMemo(() => {
|
|
47
|
-
const pages = flattenTree(tree.children)
|
|
48
|
-
const currentIndex = pages.findIndex(
|
|
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:
|
|
53
|
-
|
|
54
|
-
|
|
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=
|
|
60
|
-
<Flex align=
|
|
36
|
+
<Flex align='center' className={styles.navbar}>
|
|
37
|
+
<Flex align='center' gap='small' className={styles.navLeft}>
|
|
61
38
|
{prev ? (
|
|
62
|
-
<
|
|
39
|
+
<RouterLink
|
|
40
|
+
to={prev.url}
|
|
41
|
+
className={styles.arrow}
|
|
42
|
+
aria-label='Previous page'
|
|
43
|
+
>
|
|
63
44
|
<ChevronLeftIcon width={14} height={14} />
|
|
64
|
-
</
|
|
45
|
+
</RouterLink>
|
|
65
46
|
) : (
|
|
66
|
-
<button
|
|
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
|
-
<
|
|
56
|
+
<RouterLink
|
|
57
|
+
to={next.url}
|
|
58
|
+
className={styles.arrow}
|
|
59
|
+
aria-label='Next page'
|
|
60
|
+
>
|
|
72
61
|
<ChevronRightIcon width={14} height={14} />
|
|
73
|
-
</
|
|
62
|
+
</RouterLink>
|
|
74
63
|
) : (
|
|
75
|
-
<button
|
|
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
|
-
<
|
|
79
|
+
<RouterLink to={crumb.href} className={styles.crumbLink}>
|
|
87
80
|
{crumb.label}
|
|
88
|
-
</
|
|
81
|
+
</RouterLink>
|
|
89
82
|
)}
|
|
90
83
|
</span>
|
|
91
84
|
))}
|
|
92
85
|
</nav>
|
|
93
86
|
</Flex>
|
|
94
|
-
<Flex align=
|
|
95
|
-
{config.search?.enabled &&
|
|
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 {
|
|
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:
|
|
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(
|
|
105
|
-
const id = tocItem.url.startsWith('#')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
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 =
|
|
148
|
-
|
|
149
|
-
|
|
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(
|
|
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(
|
|
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({
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
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={
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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
|
+
};
|