@raystack/chronicle 0.6.1 → 0.7.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 +174 -3
- package/package.json +1 -1
- package/src/lib/config.ts +5 -0
- package/src/lib/source.ts +12 -3
- package/src/pages/LandingPage.module.css +137 -24
- package/src/pages/LandingPage.tsx +23 -7
- package/src/server/entry-server.tsx +4 -1
- package/src/server/vite-config.ts +3 -0
- package/src/themes/paper/ChapterNav.module.css +23 -12
- package/src/themes/paper/ChapterNav.tsx +1 -17
- package/src/themes/paper/Layout.module.css +57 -16
- package/src/themes/paper/Layout.tsx +71 -17
- package/src/themes/paper/Page.module.css +89 -37
- package/src/themes/paper/Page.tsx +89 -53
- package/src/themes/paper/ReaderModeContext.tsx +28 -0
- package/src/themes/paper/ReadingProgress.tsx +1 -0
- package/src/themes/paper/fonts/DepartureMono-Regular.woff2 +0 -0
- package/src/themes/registry.ts +1 -1
- package/src/types/config.ts +1 -0
- package/src/types/content.ts +1 -0
|
@@ -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
|
-
{
|
|
31
|
+
{item.name}
|
|
48
32
|
</span>
|
|
49
33
|
<ul className={styles.chapterItems}>
|
|
50
34
|
{item.children.map(child => (
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
@import url("https://fonts.googleapis.com/css2?family=Hanuman:wght@400;700&display=swap");
|
|
2
|
+
|
|
3
|
+
@font-face {
|
|
4
|
+
font-family: "Departure Mono";
|
|
5
|
+
src: url("./fonts/DepartureMono-Regular.woff2") format("woff2");
|
|
6
|
+
font-weight: 400;
|
|
7
|
+
font-style: normal;
|
|
8
|
+
font-display: swap;
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
.layout {
|
|
2
|
-
--paper-sidebar-width:
|
|
12
|
+
--paper-sidebar-width: 262px;
|
|
13
|
+
--paper-font-mono: "Departure Mono", "SF Mono", "Fira Code", monospace;
|
|
14
|
+
--paper-font-body: "Hanuman", sans-serif;
|
|
3
15
|
|
|
4
16
|
min-height: 100vh;
|
|
5
17
|
}
|
|
@@ -8,33 +20,62 @@
|
|
|
8
20
|
flex: 1;
|
|
9
21
|
}
|
|
10
22
|
|
|
23
|
+
:global(body) {
|
|
24
|
+
background: var(--rs-color-background-neutral-primary);
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
.sidebar {
|
|
12
28
|
width: var(--paper-sidebar-width);
|
|
13
|
-
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
position: sticky;
|
|
33
|
+
top: 0;
|
|
14
34
|
background: var(--rs-color-background-neutral-primary);
|
|
15
|
-
overflow-y: auto;
|
|
16
|
-
font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", monospace;
|
|
17
35
|
}
|
|
18
36
|
|
|
19
|
-
.
|
|
37
|
+
.header {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
height: 48px;
|
|
41
|
+
padding: 0 var(--rs-space-5);
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.contentDirTrigger {
|
|
46
|
+
border: none;
|
|
47
|
+
outline: none;
|
|
48
|
+
background: var(--rs-color-background-neutral-primary);
|
|
49
|
+
color: var(--rs-color-foreground-accent-primary);
|
|
50
|
+
font-family: var(--paper-font-mono);
|
|
51
|
+
font-size: var(--rs-font-size-regular);
|
|
52
|
+
font-weight: var(--rs-font-weight-medium);
|
|
53
|
+
line-height: var(--rs-line-height-mini);
|
|
54
|
+
letter-spacing: var(--rs-letter-spacing-mini);
|
|
20
55
|
text-transform: uppercase;
|
|
21
|
-
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.title {
|
|
59
|
+
font-size: var(--rs-font-size-regular);
|
|
60
|
+
letter-spacing: var(--rs-letter-spacing-mini);
|
|
22
61
|
color: var(--rs-color-foreground-accent-primary);
|
|
23
|
-
font-family:
|
|
24
|
-
|
|
25
|
-
margin-bottom: var(--rs-space-7);
|
|
62
|
+
font-family: var(--paper-font-mono);
|
|
63
|
+
text-transform: uppercase;
|
|
26
64
|
}
|
|
27
65
|
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
66
|
+
.navScroll {
|
|
67
|
+
flex: 1;
|
|
68
|
+
overflow-y: auto;
|
|
69
|
+
padding: var(--rs-space-5) var(--rs-space-5);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.footer {
|
|
73
|
+
flex-shrink: 0;
|
|
74
|
+
padding: var(--rs-space-4) var(--rs-space-5);
|
|
75
|
+
border-top: 1px solid var(--rs-color-border-base-primary);
|
|
33
76
|
}
|
|
34
77
|
|
|
35
78
|
.content {
|
|
36
79
|
flex: 1;
|
|
37
|
-
overflow-y: auto;
|
|
38
80
|
background: var(--rs-color-background-neutral-primary);
|
|
39
|
-
padding-right: var(--paper-sidebar-width);
|
|
40
81
|
}
|
|
@@ -1,40 +1,86 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Flex,
|
|
3
|
+
import { Flex, Select, Text } from '@raystack/apsara';
|
|
4
4
|
import { cx } from 'class-variance-authority';
|
|
5
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
6
|
+
import { getLandingEntries } from '@/lib/config';
|
|
7
|
+
import { getActiveContentDir } from '@/lib/navigation';
|
|
8
|
+
import { usePageContext } from '@/lib/page-context';
|
|
5
9
|
import type { ThemeLayoutProps } from '@/types';
|
|
6
10
|
import { ChapterNav } from './ChapterNav';
|
|
7
|
-
import { ContentDirDropdown } from './ContentDirDropdown';
|
|
8
11
|
import styles from './Layout.module.css';
|
|
12
|
+
import { ReaderModeProvider, useReaderMode } from './ReaderModeContext';
|
|
9
13
|
import { VersionSwitcher } from './VersionSwitcher';
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
function SidebarHeader({ config }: { config: ThemeLayoutProps['config'] }) {
|
|
16
|
+
const { version } = usePageContext();
|
|
17
|
+
const { pathname } = useLocation();
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
|
|
20
|
+
const entries = getLandingEntries(config, version.dir);
|
|
21
|
+
|
|
22
|
+
if (entries.length <= 1) {
|
|
23
|
+
return (
|
|
24
|
+
<Text size={2} weight={500} className={styles.title}>
|
|
25
|
+
{config.site.title}
|
|
26
|
+
</Text>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const activeDir = getActiveContentDir(pathname, config);
|
|
31
|
+
const activeEntry =
|
|
32
|
+
entries.find(e => e.contentDir === activeDir) ?? entries[0];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Select
|
|
36
|
+
value={activeEntry.contentDir}
|
|
37
|
+
onValueChange={(val: string) => {
|
|
38
|
+
const entry = entries.find(e => e.contentDir === val);
|
|
39
|
+
if (entry) navigate(entry.href);
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Select.Trigger size='small' className={styles.contentDirTrigger}>
|
|
43
|
+
<Select.Value placeholder={activeEntry.label} className={styles.title} />
|
|
44
|
+
</Select.Trigger>
|
|
45
|
+
<Select.Content>
|
|
46
|
+
{entries.map(entry => (
|
|
47
|
+
<Select.Item key={entry.href} value={entry.contentDir}>
|
|
48
|
+
{entry.label}
|
|
49
|
+
</Select.Item>
|
|
50
|
+
))}
|
|
51
|
+
</Select.Content>
|
|
52
|
+
</Select>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function LayoutInner({
|
|
12
57
|
children,
|
|
13
58
|
config,
|
|
14
59
|
tree,
|
|
15
60
|
hideSidebar,
|
|
16
61
|
classNames
|
|
17
62
|
}: ThemeLayoutProps) {
|
|
63
|
+
const { readerMode } = useReaderMode();
|
|
64
|
+
const showSidebar = !hideSidebar && !readerMode;
|
|
65
|
+
|
|
18
66
|
return (
|
|
19
67
|
<Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
|
|
20
68
|
<Flex className={cx(styles.body, classNames?.body)}>
|
|
21
|
-
{
|
|
69
|
+
{showSidebar ? (
|
|
22
70
|
<aside className={cx(styles.sidebar, classNames?.sidebar)}>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
weight='medium'
|
|
26
|
-
as='h1'
|
|
27
|
-
className={styles.title}
|
|
28
|
-
>
|
|
29
|
-
{config.site.title}
|
|
30
|
-
</Headline>
|
|
31
|
-
<div className={styles.nav}>
|
|
32
|
-
<VersionSwitcher />
|
|
33
|
-
<ContentDirDropdown />
|
|
71
|
+
<div className={styles.header}>
|
|
72
|
+
<SidebarHeader config={config} />
|
|
34
73
|
</div>
|
|
35
|
-
<
|
|
74
|
+
<div className={styles.navScroll}>
|
|
75
|
+
<ChapterNav tree={tree} />
|
|
76
|
+
</div>
|
|
77
|
+
{config.versions?.length ? (
|
|
78
|
+
<div className={styles.footer}>
|
|
79
|
+
<VersionSwitcher />
|
|
80
|
+
</div>
|
|
81
|
+
) : null}
|
|
36
82
|
</aside>
|
|
37
|
-
)}
|
|
83
|
+
) : null}
|
|
38
84
|
<div className={cx(styles.content, classNames?.content)}>
|
|
39
85
|
{children}
|
|
40
86
|
</div>
|
|
@@ -42,3 +88,11 @@ export function Layout({
|
|
|
42
88
|
</Flex>
|
|
43
89
|
);
|
|
44
90
|
}
|
|
91
|
+
|
|
92
|
+
export function Layout(props: ThemeLayoutProps) {
|
|
93
|
+
return (
|
|
94
|
+
<ReaderModeProvider>
|
|
95
|
+
<LayoutInner {...props} />
|
|
96
|
+
</ReaderModeProvider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -1,76 +1,89 @@
|
|
|
1
1
|
.main {
|
|
2
|
-
--paper-navbar-height: 40px;
|
|
3
|
-
--paper-navbar-padding: var(--rs-space-3);
|
|
4
|
-
--paper-navbar-total: calc(
|
|
5
|
-
var(--paper-navbar-height) +
|
|
6
|
-
var(--paper-navbar-padding) *
|
|
7
|
-
2 +
|
|
8
|
-
1px
|
|
9
|
-
);
|
|
10
|
-
|
|
11
2
|
flex: 1;
|
|
12
|
-
|
|
3
|
+
width: 90%;
|
|
4
|
+
max-width: calc(1024px + var(--rs-space-17));
|
|
5
|
+
margin: 0 auto;
|
|
6
|
+
padding-top: var(--rs-space-12);
|
|
7
|
+
padding-right: var(--rs-space-17);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.readerMode {
|
|
11
|
+
padding-right: 0;
|
|
13
12
|
margin: 0 auto;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
.navbar {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
border-bottom: 1px solid var(--rs-color-border-base-primary);
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
20
18
|
justify-content: space-between;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
top: 0;
|
|
19
|
+
height: 48px;
|
|
20
|
+
padding: var(--rs-space-2) var(--rs-space-7);
|
|
24
21
|
background: var(--rs-color-background-neutral-primary);
|
|
22
|
+
backdrop-filter: blur(8px);
|
|
23
|
+
border-bottom: 0.5px solid var(--rs-color-border-base-primary);
|
|
24
|
+
position: sticky;
|
|
25
|
+
top: 0;
|
|
25
26
|
z-index: 10;
|
|
26
|
-
max-width: 1024px;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
.navLeft {
|
|
30
|
+
display: flex;
|
|
30
31
|
align-items: center;
|
|
32
|
+
gap: var(--rs-space-3);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
.navRight {
|
|
36
|
+
display: flex;
|
|
34
37
|
align-items: center;
|
|
38
|
+
gap: var(--rs-space-3);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
.
|
|
41
|
+
.arrows {
|
|
38
42
|
display: flex;
|
|
39
43
|
align-items: center;
|
|
40
|
-
|
|
44
|
+
gap: var(--rs-space-2);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.arrowLink {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
41
51
|
text-decoration: none;
|
|
52
|
+
color: var(--rs-color-foreground-base-primary);
|
|
53
|
+
padding: var(--rs-space-1);
|
|
54
|
+
border-radius: var(--rs-radius-2);
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
.
|
|
57
|
+
.arrowLink:hover {
|
|
45
58
|
color: var(--rs-color-foreground-accent-primary);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
.arrowDisabled {
|
|
49
62
|
display: flex;
|
|
50
63
|
align-items: center;
|
|
64
|
+
justify-content: center;
|
|
51
65
|
color: var(--rs-color-foreground-base-tertiary);
|
|
52
66
|
opacity: 0.4;
|
|
53
|
-
|
|
54
|
-
border: none;
|
|
55
|
-
background: none;
|
|
56
|
-
padding: 0;
|
|
67
|
+
padding: var(--rs-space-1);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
.breadcrumb {
|
|
60
|
-
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
font-family: var(--paper-font-mono);
|
|
61
74
|
font-size: var(--rs-font-size-small);
|
|
62
|
-
|
|
63
|
-
letter-spacing:
|
|
64
|
-
margin-left: var(--rs-space-3);
|
|
75
|
+
line-height: var(--rs-line-height-small);
|
|
76
|
+
letter-spacing: var(--rs-letter-spacing-small);
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
.separator {
|
|
68
|
-
margin: 0 var(--rs-space-
|
|
80
|
+
margin: 0 var(--rs-space-1);
|
|
69
81
|
color: var(--rs-color-foreground-base-tertiary);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
.crumbLink {
|
|
73
85
|
color: var(--rs-color-foreground-base-tertiary);
|
|
86
|
+
font-weight: var(--rs-font-weight-medium);
|
|
74
87
|
text-decoration: none;
|
|
75
88
|
}
|
|
76
89
|
|
|
@@ -80,29 +93,67 @@
|
|
|
80
93
|
|
|
81
94
|
.crumbActive {
|
|
82
95
|
color: var(--rs-color-foreground-base-primary);
|
|
83
|
-
font-weight:
|
|
96
|
+
font-weight: var(--rs-font-weight-medium);
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
.article {
|
|
87
100
|
flex: 1;
|
|
88
101
|
min-width: 0;
|
|
89
|
-
|
|
102
|
+
width: 100%;
|
|
90
103
|
padding: 0 var(--rs-space-7);
|
|
91
104
|
}
|
|
92
105
|
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
.articleHeader {
|
|
107
|
+
text-align: center;
|
|
108
|
+
max-width: 656px;
|
|
109
|
+
margin: 0 auto;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.readingTime {
|
|
113
|
+
display: block;
|
|
114
|
+
font-family: var(--paper-font-mono);
|
|
96
115
|
font-size: var(--rs-font-size-small);
|
|
116
|
+
font-weight: var(--rs-font-weight-regular);
|
|
117
|
+
line-height: 1.67;
|
|
118
|
+
letter-spacing: var(--rs-letter-spacing-t1);
|
|
119
|
+
color: var(--rs-color-foreground-base-tertiary);
|
|
120
|
+
margin-bottom: var(--rs-space-5);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.articleTitle {
|
|
124
|
+
font-family: var(--paper-font-body);
|
|
125
|
+
font-size: var(--rs-space-8);
|
|
126
|
+
font-weight: var(--rs-font-weight-medium);
|
|
127
|
+
line-height: var(--rs-space-10);
|
|
128
|
+
letter-spacing: var(--rs-letter-spacing-t1);
|
|
129
|
+
text-align: center;
|
|
130
|
+
color: var(--rs-color-foreground-base-primary);
|
|
131
|
+
margin: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.articleSeparator {
|
|
135
|
+
width: 55px;
|
|
97
136
|
border: none;
|
|
98
|
-
|
|
137
|
+
border-top: 1px solid var(--rs-color-border-base-primary);
|
|
138
|
+
margin: var(--rs-space-10) auto;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.articleDescription {
|
|
142
|
+
font-family: var(--paper-font-body);
|
|
143
|
+
font-size: var(--rs-space-4);
|
|
144
|
+
font-weight: var(--rs-font-weight-medium);
|
|
145
|
+
line-height: var(--rs-space-7);
|
|
146
|
+
letter-spacing: var(--rs-letter-spacing-t1);
|
|
147
|
+
text-align: center;
|
|
148
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
149
|
+
margin: var(--rs-space-4) 0 0;
|
|
99
150
|
}
|
|
100
151
|
|
|
101
152
|
.content {
|
|
102
|
-
font-family:
|
|
153
|
+
font-family: var(--paper-font-body);
|
|
103
154
|
line-height: 1.8;
|
|
104
155
|
background: var(--rs-color-background-base-primary);
|
|
105
|
-
padding: var(--rs-space-9);
|
|
156
|
+
padding: var(--rs-space-15) var(--rs-space-9) var(--rs-space-9);
|
|
106
157
|
border-left: 1px solid var(--rs-color-border-base-primary);
|
|
107
158
|
border-right: 1px solid var(--rs-color-border-base-primary);
|
|
108
159
|
box-shadow:
|
|
@@ -152,6 +203,7 @@
|
|
|
152
203
|
|
|
153
204
|
.content p {
|
|
154
205
|
margin: 0.75rem 0;
|
|
206
|
+
line-height: 2;
|
|
155
207
|
}
|
|
156
208
|
|
|
157
209
|
.content ul,
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
ArrowLeftIcon,
|
|
3
|
+
ArrowRightIcon,
|
|
4
|
+
ChevronRightIcon,
|
|
5
|
+
AdjustmentsHorizontalIcon,
|
|
6
|
+
EyeIcon,
|
|
7
|
+
SunIcon,
|
|
8
|
+
MoonIcon,
|
|
9
|
+
XMarkIcon,
|
|
10
|
+
} from '@heroicons/react/24/outline';
|
|
11
|
+
import { IconButton, useTheme } from '@raystack/apsara';
|
|
12
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
4
13
|
import { Link as RouterLink, useLocation } from 'react-router';
|
|
5
14
|
import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb';
|
|
6
15
|
import { flattenTree } from 'fumadocs-core/page-tree';
|
|
7
|
-
import { Search } from '@/components/ui/search';
|
|
8
16
|
import type { ThemePageProps } from '@/types';
|
|
9
17
|
import styles from './Page.module.css';
|
|
18
|
+
import { useReaderMode } from './ReaderModeContext';
|
|
10
19
|
import { ReadingProgress } from './ReadingProgress';
|
|
11
20
|
|
|
12
|
-
export function Page({ page,
|
|
21
|
+
export function Page({ page, tree }: ThemePageProps) {
|
|
13
22
|
const { pathname } = useLocation();
|
|
23
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
24
|
+
const [isClient, setIsClient] = useState(false);
|
|
25
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
26
|
+
const { readerMode, toggleReaderMode } = useReaderMode();
|
|
27
|
+
|
|
28
|
+
useEffect(() => { setIsClient(true); }, []);
|
|
14
29
|
|
|
15
30
|
const { prev, next, crumbs } = useMemo(() => {
|
|
16
31
|
const pages = flattenTree(tree.children);
|
|
@@ -32,47 +47,33 @@ export function Page({ page, config, tree }: ThemePageProps) {
|
|
|
32
47
|
|
|
33
48
|
return (
|
|
34
49
|
<>
|
|
35
|
-
<main className={styles.main}>
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
to={prev.url}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
className={styles.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
className={styles.arrow}
|
|
59
|
-
aria-label='Next page'
|
|
60
|
-
>
|
|
61
|
-
<ChevronRightIcon width={14} height={14} />
|
|
62
|
-
</RouterLink>
|
|
63
|
-
) : (
|
|
64
|
-
<button
|
|
65
|
-
disabled
|
|
66
|
-
className={styles.arrowDisabled}
|
|
67
|
-
aria-label='Next page'
|
|
68
|
-
>
|
|
69
|
-
<ChevronRightIcon width={14} height={14} />
|
|
70
|
-
</button>
|
|
71
|
-
)}
|
|
50
|
+
<main className={`${styles.main} ${readerMode ? styles.readerMode : ''}`}>
|
|
51
|
+
<div className={styles.navbar}>
|
|
52
|
+
<div className={styles.navLeft}>
|
|
53
|
+
<div className={styles.arrows}>
|
|
54
|
+
{prev ? (
|
|
55
|
+
<RouterLink to={prev.url} className={styles.arrowLink} aria-label='Previous page'>
|
|
56
|
+
<ArrowLeftIcon width={14} height={14} />
|
|
57
|
+
</RouterLink>
|
|
58
|
+
) : (
|
|
59
|
+
<span className={styles.arrowDisabled} aria-hidden='true'>
|
|
60
|
+
<ArrowLeftIcon width={14} height={14} />
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
{next ? (
|
|
64
|
+
<RouterLink to={next.url} className={styles.arrowLink} aria-label='Next page'>
|
|
65
|
+
<ArrowRightIcon width={14} height={14} />
|
|
66
|
+
</RouterLink>
|
|
67
|
+
) : (
|
|
68
|
+
<span className={styles.arrowDisabled} aria-hidden='true'>
|
|
69
|
+
<ArrowRightIcon width={14} height={14} />
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
72
73
|
<nav className={styles.breadcrumb}>
|
|
73
74
|
{crumbs.map((crumb, i) => (
|
|
74
75
|
<span key={crumb.href}>
|
|
75
|
-
{i > 0 && <
|
|
76
|
+
{i > 0 && <ChevronRightIcon width={12} height={12} className={styles.separator} />}
|
|
76
77
|
{i === crumbs.length - 1 ? (
|
|
77
78
|
<span className={styles.crumbActive}>{crumb.label}</span>
|
|
78
79
|
) : (
|
|
@@ -83,18 +84,53 @@ export function Page({ page, config, tree }: ThemePageProps) {
|
|
|
83
84
|
</span>
|
|
84
85
|
))}
|
|
85
86
|
</nav>
|
|
86
|
-
</
|
|
87
|
-
<
|
|
88
|
-
{
|
|
89
|
-
|
|
87
|
+
</div>
|
|
88
|
+
<div className={styles.navRight}>
|
|
89
|
+
{settingsOpen ? (
|
|
90
|
+
<>
|
|
91
|
+
<IconButton size={2} onClick={toggleReaderMode} aria-label='Toggle reader mode'>
|
|
92
|
+
<EyeIcon width={14} height={14} />
|
|
93
|
+
</IconButton>
|
|
94
|
+
{isClient && (
|
|
95
|
+
<IconButton
|
|
96
|
+
size={2}
|
|
97
|
+
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
|
98
|
+
aria-label={resolvedTheme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
99
|
+
>
|
|
100
|
+
{resolvedTheme === 'dark'
|
|
101
|
+
? <SunIcon width={14} height={14} />
|
|
102
|
+
: <MoonIcon width={14} height={14} />
|
|
103
|
+
}
|
|
104
|
+
</IconButton>
|
|
105
|
+
)}
|
|
106
|
+
<IconButton size={2} onClick={() => setSettingsOpen(false)} aria-label='Close settings'>
|
|
107
|
+
<XMarkIcon width={14} height={14} />
|
|
108
|
+
</IconButton>
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
<IconButton size={2} onClick={() => setSettingsOpen(true)} aria-label='Open settings'>
|
|
112
|
+
<AdjustmentsHorizontalIcon width={14} height={14} />
|
|
113
|
+
</IconButton>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div className={styles.content}>
|
|
118
|
+
<header className={styles.articleHeader}>
|
|
119
|
+
{page.frontmatter._readingTime && (
|
|
120
|
+
<span className={styles.readingTime}>{page.frontmatter._readingTime}min Read</span>
|
|
121
|
+
)}
|
|
122
|
+
<h1 className={styles.articleTitle}>{page.frontmatter.title}</h1>
|
|
123
|
+
{page.frontmatter.description && (
|
|
124
|
+
<p className={styles.articleDescription}>{page.frontmatter.description}</p>
|
|
90
125
|
)}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
<hr className={styles.articleSeparator} />
|
|
127
|
+
</header>
|
|
128
|
+
<article className={styles.article} data-article-content>
|
|
129
|
+
{page.content}
|
|
130
|
+
</article>
|
|
131
|
+
</div>
|
|
96
132
|
</main>
|
|
97
|
-
<ReadingProgress items={page.toc} />
|
|
133
|
+
{!readerMode && <ReadingProgress items={page.toc} />}
|
|
98
134
|
</>
|
|
99
135
|
);
|
|
100
136
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useState, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ReaderModeContextValue {
|
|
6
|
+
readerMode: boolean;
|
|
7
|
+
toggleReaderMode: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ReaderModeContext = createContext<ReaderModeContextValue>({
|
|
11
|
+
readerMode: false,
|
|
12
|
+
toggleReaderMode: () => {},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function ReaderModeProvider({ children }: { children: ReactNode }) {
|
|
16
|
+
const [readerMode, setReaderMode] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<ReaderModeContext.Provider
|
|
19
|
+
value={{ readerMode, toggleReaderMode: () => setReaderMode(v => !v) }}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</ReaderModeContext.Provider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useReaderMode() {
|
|
27
|
+
return useContext(ReaderModeContext);
|
|
28
|
+
}
|
|
@@ -220,6 +220,7 @@ export function ReadingProgress({ items }: ReadingProgressProps) {
|
|
|
220
220
|
const element = document.getElementById(id);
|
|
221
221
|
if (!element) return;
|
|
222
222
|
|
|
223
|
+
history.pushState(null, '', `#${id}`);
|
|
223
224
|
const elementTop = element.getBoundingClientRect().top + window.scrollY;
|
|
224
225
|
window.scrollTo({
|
|
225
226
|
top: Math.max(0, elementTop - NAV_HEIGHT),
|
|
Binary file
|
package/src/themes/registry.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function getTheme(name?: string): Theme {
|
|
|
15
15
|
|
|
16
16
|
export function getThemeConfig(name?: string) {
|
|
17
17
|
if (name === 'paper') {
|
|
18
|
-
return { enableSystem:
|
|
18
|
+
return { enableSystem: true };
|
|
19
19
|
}
|
|
20
20
|
return { enableSystem: true };
|
|
21
21
|
}
|