@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,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: 260px;
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,66 @@
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
- padding: var(--rs-space-7) var(--rs-space-5);
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
- .title {
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
- letter-spacing: 0.08em;
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: inherit;
24
- font-size: var(--rs-font-size-mono-large);
25
- margin-bottom: var(--rs-space-7);
62
+ font-family: var(--paper-font-mono);
63
+ text-transform: uppercase;
26
64
  }
27
65
 
28
- .nav {
29
- display: flex;
30
- flex-direction: column;
31
- gap: var(--rs-space-3);
32
- margin-bottom: var(--rs-space-7);
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);
81
+ }
82
+
83
+ .hiddenTrigger {
84
+ display: none;
40
85
  }
@@ -1,44 +1,100 @@
1
1
  'use client';
2
2
 
3
- import { Flex, Headline } from '@raystack/apsara';
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';
9
+ import { Search } from '@/components/ui/search';
5
10
  import type { ThemeLayoutProps } from '@/types';
6
11
  import { ChapterNav } from './ChapterNav';
7
- import { ContentDirDropdown } from './ContentDirDropdown';
8
12
  import styles from './Layout.module.css';
13
+ import { ReaderModeProvider, useReaderMode } from './ReaderModeContext';
9
14
  import { VersionSwitcher } from './VersionSwitcher';
10
15
 
11
- export function Layout({
16
+ function SidebarHeader({ config }: { config: ThemeLayoutProps['config'] }) {
17
+ const { version } = usePageContext();
18
+ const { pathname } = useLocation();
19
+ const navigate = useNavigate();
20
+
21
+ const entries = getLandingEntries(config, version.dir);
22
+
23
+ if (entries.length <= 1) {
24
+ return (
25
+ <Text size={2} weight={500} className={styles.title}>
26
+ {config.site.title}
27
+ </Text>
28
+ );
29
+ }
30
+
31
+ const activeDir = getActiveContentDir(pathname, config);
32
+ const activeEntry =
33
+ entries.find(e => e.contentDir === activeDir) ?? entries[0];
34
+
35
+ return (
36
+ <Select
37
+ value={activeEntry.contentDir}
38
+ onValueChange={(val: string) => {
39
+ const entry = entries.find(e => e.contentDir === val);
40
+ if (entry) navigate(entry.href);
41
+ }}
42
+ >
43
+ <Select.Trigger size='small' className={styles.contentDirTrigger}>
44
+ <Select.Value placeholder={activeEntry.label} className={styles.title} />
45
+ </Select.Trigger>
46
+ <Select.Content>
47
+ {entries.map(entry => (
48
+ <Select.Item key={entry.href} value={entry.contentDir}>
49
+ {entry.label}
50
+ </Select.Item>
51
+ ))}
52
+ </Select.Content>
53
+ </Select>
54
+ );
55
+ }
56
+
57
+ function LayoutInner({
12
58
  children,
13
59
  config,
14
60
  tree,
15
61
  hideSidebar,
16
62
  classNames
17
63
  }: ThemeLayoutProps) {
64
+ const { readerMode } = useReaderMode();
65
+ const showSidebar = !hideSidebar && !readerMode;
66
+
18
67
  return (
19
68
  <Flex direction='column' className={cx(styles.layout, classNames?.layout)}>
20
69
  <Flex className={cx(styles.body, classNames?.body)}>
21
- {hideSidebar ? null : (
70
+ {showSidebar ? (
22
71
  <aside className={cx(styles.sidebar, classNames?.sidebar)}>
23
- <Headline
24
- size='small'
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 />
72
+ <div className={styles.header}>
73
+ <SidebarHeader config={config} />
34
74
  </div>
35
- <ChapterNav tree={tree} />
75
+ <div className={styles.navScroll}>
76
+ <ChapterNav tree={tree} />
77
+ </div>
78
+ {config.versions?.length ? (
79
+ <div className={styles.footer}>
80
+ <VersionSwitcher />
81
+ </div>
82
+ ) : null}
36
83
  </aside>
37
- )}
84
+ ) : null}
38
85
  <div className={cx(styles.content, classNames?.content)}>
86
+ {config.search?.enabled && <Search classNames={{ trigger: styles.hiddenTrigger }} />}
39
87
  {children}
40
88
  </div>
41
89
  </Flex>
42
90
  </Flex>
43
91
  );
44
92
  }
93
+
94
+ export function Layout(props: ThemeLayoutProps) {
95
+ return (
96
+ <ReaderModeProvider>
97
+ <LayoutInner {...props} />
98
+ </ReaderModeProvider>
99
+ );
100
+ }
@@ -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
- max-width: 1024px;
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
- height: var(--paper-navbar-height);
18
- padding: var(--paper-navbar-padding) 0;
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
- width: 100%;
22
- position: fixed;
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
- .arrow {
41
+ .arrows {
38
42
  display: flex;
39
43
  align-items: center;
40
- color: var(--rs-color-foreground-base-primary);
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
- .arrow:hover {
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
- cursor: default;
54
- border: none;
55
- background: none;
56
- padding: 0;
67
+ padding: var(--rs-space-1);
57
68
  }
58
69
 
59
70
  .breadcrumb {
60
- font-family: "SF Mono", "Fira Code", monospace;
71
+ display: flex;
72
+ align-items: center;
73
+ font-family: var(--paper-font-mono);
61
74
  font-size: var(--rs-font-size-small);
62
- text-transform: uppercase;
63
- letter-spacing: 0.05em;
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-2);
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: 600;
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
- margin-top: var(--paper-navbar-total);
102
+ width: 100%;
90
103
  padding: 0 var(--rs-space-7);
91
104
  }
92
105
 
93
- .searchButton {
94
- height: 28px;
95
- padding: 0 var(--rs-space-3);
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
- box-shadow: none;
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: Georgia, "Times New Roman", serif;
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 { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
2
- import { Flex } from '@raystack/apsara';
3
- import { useMemo } from 'react';
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, config, tree }: ThemePageProps) {
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
- <Flex align='center' className={styles.navbar}>
37
- <Flex align='center' gap='small' className={styles.navLeft}>
38
- {prev ? (
39
- <RouterLink
40
- to={prev.url}
41
- className={styles.arrow}
42
- aria-label='Previous page'
43
- >
44
- <ChevronLeftIcon width={14} height={14} />
45
- </RouterLink>
46
- ) : (
47
- <button
48
- disabled
49
- className={styles.arrowDisabled}
50
- aria-label='Previous page'
51
- >
52
- <ChevronLeftIcon width={14} height={14} />
53
- </button>
54
- )}
55
- {next ? (
56
- <RouterLink
57
- to={next.url}
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 && <span className={styles.separator}>/</span>}
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
- </Flex>
87
- <Flex align='center' className={styles.navRight}>
88
- {config.search?.enabled && (
89
- <Search className={styles.searchButton} />
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
- </Flex>
92
- </Flex>
93
- <article className={styles.article} data-article-content>
94
- <div className={styles.content}>{page.content}</div>
95
- </article>
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),
@@ -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: false, forcedTheme: 'light' };
18
+ return { enableSystem: true };
19
19
  }
20
20
  return { enableSystem: true };
21
21
  }
@@ -84,6 +84,7 @@ const dirNameSchema = z
84
84
  const contentEntrySchema = z.object({
85
85
  dir: dirNameSchema,
86
86
  label: z.string().min(1),
87
+ description: z.string().optional(),
87
88
  icon: z.string().optional(),
88
89
  })
89
90
 
@@ -10,6 +10,7 @@ export interface Frontmatter {
10
10
  order?: number
11
11
  icon?: string
12
12
  lastModified?: string
13
+ _readingTime?: number
13
14
  }
14
15
 
15
16
  export interface PageNavLink {
@@ -1,14 +0,0 @@
1
- import { visit } from 'unist-util-visit'
2
- import type { Plugin } from 'unified'
3
-
4
- const remarkStripMdExtensions: Plugin = () => {
5
- return (tree) => {
6
- visit(tree, 'link', (node: any) => {
7
- if (!node.url) return
8
- if (node.url.startsWith('http://') || node.url.startsWith('https://')) return
9
- node.url = node.url.replace(/\.mdx?(#|$)/, '$1')
10
- })
11
- }
12
- }
13
-
14
- export default remarkStripMdExtensions