@abreen/tada 1.5.8 → 1.6.2
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/README.md +3 -0
- package/build/build-manifest.ts +2 -1
- package/build/bundle.ts +18 -6
- package/build/copy.ts +4 -9
- package/build/external-links-plugin.ts +181 -22
- package/build/generate-content-assets.ts +2 -4
- package/build/utils/content-files.ts +3 -9
- package/build/utils/derive-theme.ts +8 -0
- package/build/utils/paths.ts +16 -1
- package/build/utils/render.ts +31 -12
- package/build/utils/trace.ts +2 -5
- package/build/validate-internal-links-plugin.ts +12 -1
- package/build/watch-reload-client.ts +1 -0
- package/build/watch.ts +7 -24
- package/package.json +8 -7
- package/src/_alerts.scss +11 -6
- package/src/_base.scss +7 -10
- package/src/_layout.scss +23 -1
- package/src/_mixins.scss +41 -0
- package/src/anchor/style.scss +17 -24
- package/src/header/_base.scss +8 -0
- package/src/header/style.scss +1 -18
- package/src/index.ts +12 -0
- package/src/navigate/index.ts +47 -29
- package/src/print/style.scss +1 -1
- package/src/style.scss +18 -47
- package/src/toc/index.ts +1 -1
- package/src/toc/style.scss +4 -33
- package/src/top/index.ts +6 -2
- package/templates/_theme.scss +11 -24
- package/templates/_top.html +2 -0
package/build/watch.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getPublicDir,
|
|
13
13
|
getDistDir,
|
|
14
14
|
getPackageDir,
|
|
15
|
+
toPosix,
|
|
15
16
|
} from './utils/paths';
|
|
16
17
|
import { isFeatureEnabled } from './features';
|
|
17
18
|
import { bundle } from './bundle';
|
|
@@ -55,10 +56,7 @@ export async function runWatch(options: {
|
|
|
55
56
|
define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
|
|
56
57
|
});
|
|
57
58
|
return result.outputs.map(output =>
|
|
58
|
-
path
|
|
59
|
-
.relative(getDistDir(), output.path)
|
|
60
|
-
.split(path.sep)
|
|
61
|
-
.join(path.posix.sep),
|
|
59
|
+
toPosix(path.relative(getDistDir(), output.path)),
|
|
62
60
|
);
|
|
63
61
|
}
|
|
64
62
|
|
|
@@ -154,10 +152,7 @@ export async function runWatch(options: {
|
|
|
154
152
|
return null;
|
|
155
153
|
}
|
|
156
154
|
|
|
157
|
-
return path
|
|
158
|
-
.relative(contentDir, normalizedFilePath)
|
|
159
|
-
.split(path.sep)
|
|
160
|
-
.join(path.posix.sep);
|
|
155
|
+
return toPosix(path.relative(contentDir, normalizedFilePath));
|
|
161
156
|
}
|
|
162
157
|
|
|
163
158
|
function toPublicRelativePath(filePath: string): string | null {
|
|
@@ -169,10 +164,7 @@ export async function runWatch(options: {
|
|
|
169
164
|
if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
|
|
170
165
|
return null;
|
|
171
166
|
}
|
|
172
|
-
return path
|
|
173
|
-
.relative(publicDir, normalizedFilePath)
|
|
174
|
-
.split(path.sep)
|
|
175
|
-
.join(path.posix.sep);
|
|
167
|
+
return toPosix(path.relative(publicDir, normalizedFilePath));
|
|
176
168
|
}
|
|
177
169
|
|
|
178
170
|
// Watch mode
|
|
@@ -354,10 +346,7 @@ export async function runWatch(options: {
|
|
|
354
346
|
const absPath = path.resolve(filePath);
|
|
355
347
|
if (fs.existsSync(absPath)) {
|
|
356
348
|
copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
|
|
357
|
-
const rel = path
|
|
358
|
-
.relative(publicDir, absPath)
|
|
359
|
-
.split(path.sep)
|
|
360
|
-
.join(path.posix.sep);
|
|
349
|
+
const rel = toPosix(path.relative(publicDir, absPath));
|
|
361
350
|
publicRelPaths.add(rel);
|
|
362
351
|
}
|
|
363
352
|
}
|
|
@@ -397,10 +386,7 @@ export async function runWatch(options: {
|
|
|
397
386
|
const absPath = path.resolve(filePath);
|
|
398
387
|
if (fs.existsSync(absPath)) {
|
|
399
388
|
copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
|
|
400
|
-
const rel = path
|
|
401
|
-
.relative(publicDir, absPath)
|
|
402
|
-
.split(path.sep)
|
|
403
|
-
.join(path.posix.sep);
|
|
389
|
+
const rel = toPosix(path.relative(publicDir, absPath));
|
|
404
390
|
publicRelPaths.add(rel);
|
|
405
391
|
}
|
|
406
392
|
}
|
|
@@ -417,10 +403,7 @@ export async function runWatch(options: {
|
|
|
417
403
|
const ext = path.extname(absPath).slice(1).toLowerCase();
|
|
418
404
|
if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
|
|
419
405
|
copyContentFile(contentDir, distDir, absPath, publicRelPaths);
|
|
420
|
-
const rel = path
|
|
421
|
-
.relative(contentDir, absPath)
|
|
422
|
-
.split(path.sep)
|
|
423
|
-
.join(path.posix.sep);
|
|
406
|
+
const rel = toPosix(path.relative(contentDir, absPath));
|
|
424
407
|
contentAssetRelPaths.add(rel);
|
|
425
408
|
}
|
|
426
409
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abreen/tada",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A static site generator",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"prepack": "bun run scripts/prepack.ts",
|
|
30
30
|
"lint": "eslint src/ build/ bin/",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
|
-
"test:functional": "
|
|
33
|
-
"test:all": "bun
|
|
34
|
-
"test:coverage": "
|
|
35
|
-
"test:playwright": "
|
|
32
|
+
"test:functional": "bun run scripts/test.ts functional",
|
|
33
|
+
"test:all": "bun run scripts/test.ts all",
|
|
34
|
+
"test:coverage": "bun run scripts/test.ts coverage",
|
|
35
|
+
"test:playwright": "bun run scripts/test.ts playwright",
|
|
36
36
|
"init-example": "bun run bin/tada.ts init example --no-interactive",
|
|
37
37
|
"iew": "if [ -d example ]; then cd example; else bun run init-example && cd example; fi && ../bin/tada.ts watch"
|
|
38
38
|
},
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
},
|
|
42
42
|
"os": [
|
|
43
43
|
"darwin",
|
|
44
|
-
"linux"
|
|
44
|
+
"linux",
|
|
45
|
+
"win32"
|
|
45
46
|
],
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@vscode/markdown-it-katex": "^1.1.2",
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
},
|
|
78
79
|
"devDependencies": {
|
|
79
80
|
"@eslint/js": "^10.0.1",
|
|
80
|
-
"@playwright/test": "
|
|
81
|
+
"@playwright/test": "1.56.1",
|
|
81
82
|
"@saintedlama/lcov-parse": "^1.0.1",
|
|
82
83
|
"@types/bun": "^1.3.10",
|
|
83
84
|
"@types/culori": "^4.0.1",
|
package/src/_alerts.scss
CHANGED
|
@@ -39,10 +39,15 @@
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
a {
|
|
42
|
-
|
|
42
|
+
@include link-underline;
|
|
43
|
+
color: var(--fg-color);
|
|
43
44
|
|
|
44
45
|
&:hover {
|
|
45
|
-
|
|
46
|
+
color: var(--fg2-color);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&:active {
|
|
50
|
+
color: var(--link-active-color);
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -60,6 +65,7 @@
|
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
.alert .title::before {
|
|
68
|
+
content: '';
|
|
63
69
|
left: 8px;
|
|
64
70
|
top: 8px;
|
|
65
71
|
position: absolute;
|
|
@@ -68,12 +74,11 @@
|
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
.alert.warning .title::before {
|
|
71
|
-
|
|
77
|
+
@include contained-mask(var(--icon-warning));
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
i.warning {
|
|
75
|
-
|
|
76
|
-
background-size: contain;
|
|
81
|
+
@include contained-mask(var(--icon-warning));
|
|
77
82
|
display: inline-block;
|
|
78
83
|
height: 1em;
|
|
79
84
|
width: 1em;
|
|
@@ -81,7 +86,7 @@ i.warning {
|
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
.alert.note .title::before {
|
|
84
|
-
|
|
89
|
+
@include contained-mask(var(--icon-note));
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
.alert .title {
|
package/src/_base.scss
CHANGED
|
@@ -66,22 +66,18 @@ html {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
a {
|
|
69
|
-
color: var(--
|
|
70
|
-
text-decoration-
|
|
71
|
-
text-decoration-thickness: 1.5px;
|
|
69
|
+
color: var(--link-color);
|
|
70
|
+
text-decoration-line: none;
|
|
72
71
|
|
|
73
72
|
@media (hover: hover) {
|
|
74
73
|
&:hover {
|
|
75
|
-
color: var(--
|
|
74
|
+
color: var(--link-color-hover);
|
|
75
|
+
@include link-underline;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
}
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@media (hover: hover) {
|
|
84
|
-
text-decoration-color: var(--link-active-color);
|
|
79
|
+
&:active {
|
|
80
|
+
color: var(--link-active-color);
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
|
|
@@ -146,6 +142,7 @@ details .content {
|
|
|
146
142
|
details summary {
|
|
147
143
|
&:hover {
|
|
148
144
|
color: var(--fg2-color);
|
|
145
|
+
@include link-underline;
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
&:active {
|
package/src/_layout.scss
CHANGED
|
@@ -156,10 +156,31 @@ body.literate main table {
|
|
|
156
156
|
.text {
|
|
157
157
|
color: var(--fg2-color);
|
|
158
158
|
text-decoration: underline;
|
|
159
|
-
text-decoration-color: var(--fg2-color);
|
|
160
159
|
text-decoration-thickness: 1px;
|
|
161
160
|
text-underline-offset: 3px;
|
|
162
161
|
}
|
|
162
|
+
|
|
163
|
+
&:hover .text {
|
|
164
|
+
color: var(--link-color-hover);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
&:active .text {
|
|
168
|
+
color: var(--link-active-color);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&:active .avatar {
|
|
172
|
+
border-color: var(--link-active-color);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&:focus-visible {
|
|
176
|
+
outline: none;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&:focus-visible .text,
|
|
180
|
+
&:focus-visible .avatar {
|
|
181
|
+
outline: 2px solid var(--theme-color);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
}
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
.avatar {
|
|
@@ -167,6 +188,7 @@ body.literate main table {
|
|
|
167
188
|
height: 24px;
|
|
168
189
|
vertical-align: middle;
|
|
169
190
|
margin-left: 4px;
|
|
191
|
+
border: 1px solid transparent;
|
|
170
192
|
}
|
|
171
193
|
}
|
|
172
194
|
}
|
package/src/_mixins.scss
CHANGED
|
@@ -2,3 +2,44 @@
|
|
|
2
2
|
-webkit-user-select: $val;
|
|
3
3
|
user-select: $val;
|
|
4
4
|
}
|
|
5
|
+
|
|
6
|
+
@mixin link-underline {
|
|
7
|
+
text-decoration-line: underline;
|
|
8
|
+
text-decoration-color: currentColor;
|
|
9
|
+
text-decoration-thickness: 1px;
|
|
10
|
+
text-underline-offset: 0.11rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@mixin subdued-link {
|
|
14
|
+
color: var(--fg-color);
|
|
15
|
+
text-decoration-line: none;
|
|
16
|
+
|
|
17
|
+
@media (hover: hover) {
|
|
18
|
+
&:hover {
|
|
19
|
+
color: var(--fg2-color);
|
|
20
|
+
@include link-underline;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&:active {
|
|
25
|
+
color: var(--link-active-color);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@mixin contained-icon {
|
|
30
|
+
background-size: contain;
|
|
31
|
+
background-repeat: no-repeat;
|
|
32
|
+
background-position: center center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@mixin contained-mask($url) {
|
|
36
|
+
-webkit-mask-image: $url;
|
|
37
|
+
mask-image: $url;
|
|
38
|
+
-webkit-mask-size: contain;
|
|
39
|
+
mask-size: contain;
|
|
40
|
+
-webkit-mask-repeat: no-repeat;
|
|
41
|
+
mask-repeat: no-repeat;
|
|
42
|
+
-webkit-mask-position: center center;
|
|
43
|
+
mask-position: center center;
|
|
44
|
+
background-color: currentColor;
|
|
45
|
+
}
|
package/src/anchor/style.scss
CHANGED
|
@@ -1,40 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
@use '../mixins' as *;
|
|
2
|
+
|
|
3
|
+
a.heading-anchor {
|
|
2
4
|
position: relative;
|
|
3
5
|
display: inline-block;
|
|
4
|
-
|
|
5
|
-
text-decoration: none;
|
|
6
|
+
@include subdued-link;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
.heading-anchor::after {
|
|
9
|
-
content:
|
|
9
|
+
a.heading-anchor::after {
|
|
10
|
+
content: '';
|
|
10
11
|
display: inline-block;
|
|
11
|
-
height:
|
|
12
|
-
width:
|
|
12
|
+
height: 1em;
|
|
13
|
+
width: 1em;
|
|
13
14
|
margin-left: 4px;
|
|
15
|
+
vertical-align: middle;
|
|
14
16
|
opacity: 0;
|
|
17
|
+
@include contained-mask(var(--icon-anchor));
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
.heading-anchor:focus::after
|
|
18
|
-
.heading-anchor:focus-visible::after {
|
|
20
|
+
a.heading-anchor:focus-visible::after {
|
|
19
21
|
opacity: 1;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
@media (hover: hover) {
|
|
23
|
-
h1:hover .heading-anchor::after,
|
|
24
|
-
h2:hover .heading-anchor::after,
|
|
25
|
-
h3:hover .heading-anchor::after,
|
|
26
|
-
h4:hover .heading-anchor::after,
|
|
27
|
-
h5:hover .heading-anchor::after,
|
|
28
|
-
h6:hover .heading-anchor::after {
|
|
25
|
+
h1:hover a.heading-anchor::after,
|
|
26
|
+
h2:hover a.heading-anchor::after,
|
|
27
|
+
h3:hover a.heading-anchor::after,
|
|
28
|
+
h4:hover a.heading-anchor::after,
|
|
29
|
+
h5:hover a.heading-anchor::after,
|
|
30
|
+
h6:hover a.heading-anchor::after {
|
|
29
31
|
opacity: 1;
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
h1:hover .heading-anchor,
|
|
33
|
-
h2:hover .heading-anchor,
|
|
34
|
-
h3:hover .heading-anchor,
|
|
35
|
-
h4:hover .heading-anchor,
|
|
36
|
-
h5:hover .heading-anchor,
|
|
37
|
-
h6:hover .heading-anchor {
|
|
38
|
-
color: var(--fg-color);
|
|
39
|
-
}
|
|
40
33
|
}
|
package/src/header/_base.scss
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@use '../mixins' as *;
|
|
2
|
+
|
|
1
3
|
:root {
|
|
2
4
|
--header-collapsed-height: 45px;
|
|
3
5
|
}
|
|
@@ -31,6 +33,12 @@ header details summary {
|
|
|
31
33
|
@media (hover: hover) {
|
|
32
34
|
&:hover {
|
|
33
35
|
color: inherit;
|
|
36
|
+
text-decoration-line: none;
|
|
37
|
+
background: var(--bg2-color);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&:hover .site-title {
|
|
41
|
+
@include link-underline;
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
|
package/src/header/style.scss
CHANGED
|
@@ -26,20 +26,7 @@ header nav ul li {
|
|
|
26
26
|
header nav a {
|
|
27
27
|
border: none;
|
|
28
28
|
display: inline;
|
|
29
|
-
|
|
30
|
-
text-decoration: none;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@media (hover: hover) {
|
|
34
|
-
header nav a {
|
|
35
|
-
&:hover {
|
|
36
|
-
color: var(--fg2-color);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
&:active {
|
|
40
|
-
color: var(--link-active-color);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
29
|
+
@include subdued-link;
|
|
43
30
|
}
|
|
44
31
|
|
|
45
32
|
header nav a.external {
|
|
@@ -60,10 +47,6 @@ header nav div > p {
|
|
|
60
47
|
}
|
|
61
48
|
|
|
62
49
|
@media (hover: hover) {
|
|
63
|
-
header details summary:hover {
|
|
64
|
-
background: var(--bg2-color);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
50
|
header:has(details[open]):hover {
|
|
68
51
|
background: var(--bg-color-translucent);
|
|
69
52
|
}
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,18 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
59
59
|
// Mount per-page components
|
|
60
60
|
await mountPerPageComponents(window);
|
|
61
61
|
|
|
62
|
+
// On reload with manual scrollRestoration, browsers skip the initial
|
|
63
|
+
// fragment scroll. Do it ourselves once per-page components have
|
|
64
|
+
// finished mutating the DOM. On a fresh cold load the browser already
|
|
65
|
+
// scrolled, so this is a harmless re-align.
|
|
66
|
+
if (window.location.hash) {
|
|
67
|
+
const id = window.location.hash.slice(1);
|
|
68
|
+
const el = window.document.getElementById(id);
|
|
69
|
+
if (el) {
|
|
70
|
+
el.scrollIntoView();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
for (const [name, reason] of Object.entries(failed)) {
|
|
63
75
|
console.error(`Failed to mount ${name} component:`, reason);
|
|
64
76
|
}
|
package/src/navigate/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ let currentAbortController: AbortController | null = null;
|
|
|
5
5
|
let historyIndex = 0;
|
|
6
6
|
let currentPath = '';
|
|
7
7
|
|
|
8
|
+
// In-memory scroll positions keyed by navIndex. Populated on scroll and
|
|
9
|
+
// consulted by handlePopState so returning to a previous entry via
|
|
10
|
+
// back/forward restores the user's scroll, not just scroll 0.
|
|
11
|
+
const scrollByIndex = new Map<number, number>();
|
|
12
|
+
|
|
8
13
|
type Direction = 'forward' | 'back';
|
|
9
14
|
|
|
10
15
|
function findAnchor(event: MouseEvent): HTMLAnchorElement | null {
|
|
@@ -145,27 +150,35 @@ async function performNavigation(
|
|
|
145
150
|
teardownPerPageComponents();
|
|
146
151
|
swapContent(newDoc);
|
|
147
152
|
updateHead(newDoc);
|
|
153
|
+
const parsed = new URL(url);
|
|
154
|
+
currentPath = parsed.pathname + parsed.search;
|
|
155
|
+
|
|
148
156
|
if (isPush) {
|
|
149
157
|
historyIndex++;
|
|
150
|
-
|
|
158
|
+
// Push without the hash; we add it back below via location.replace
|
|
159
|
+
// so the browser performs a real fragment navigation that updates
|
|
160
|
+
// :target and fires hashchange. pushState alone would not do
|
|
161
|
+
// either of those things.
|
|
162
|
+
const urlWithoutHash = parsed.origin + parsed.pathname + parsed.search;
|
|
163
|
+
window.history.pushState({ navIndex: historyIndex }, '', urlWithoutHash);
|
|
151
164
|
}
|
|
152
|
-
const parsed = new URL(url);
|
|
153
|
-
currentPath = parsed.pathname + parsed.search;
|
|
154
165
|
|
|
155
166
|
if (typeof scrollTarget === 'string') {
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
167
|
+
// Trigger a real fragment navigation. location.replace overwrites
|
|
168
|
+
// the current entry's URL in place and runs fragment navigation
|
|
169
|
+
// (updating :target, firing hashchange, scrolling to the element).
|
|
170
|
+
// It wipes the entry's state object, so reapply it via
|
|
171
|
+
// replaceState.
|
|
172
|
+
const urlWithHash = parsed.pathname + parsed.search + '#' + scrollTarget;
|
|
173
|
+
window.location.replace(urlWithHash);
|
|
174
|
+
window.history.replaceState({ navIndex: historyIndex }, '', urlWithHash);
|
|
163
175
|
} else if (typeof scrollTarget === 'number') {
|
|
164
176
|
// Restore scroll position (popstate)
|
|
165
177
|
window.scrollTo({ top: scrollTarget });
|
|
166
178
|
} else {
|
|
167
179
|
window.scrollTo({ top: 0 });
|
|
168
180
|
}
|
|
181
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
169
182
|
};
|
|
170
183
|
|
|
171
184
|
const doc = document as Document & {
|
|
@@ -261,6 +274,16 @@ async function performNavigation(
|
|
|
261
274
|
export default function mountNavigate(window: Window): () => void {
|
|
262
275
|
window.history.scrollRestoration = 'manual';
|
|
263
276
|
currentPath = window.location.pathname + window.location.search;
|
|
277
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
278
|
+
|
|
279
|
+
// Track scroll position on every scroll event. We keep the latest
|
|
280
|
+
// scrollY per navIndex so that back/forward navigation can restore
|
|
281
|
+
// where the user was, not just scroll 0. Map writes are cheap; no
|
|
282
|
+
// need to debounce.
|
|
283
|
+
const saveScroll = () => {
|
|
284
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
285
|
+
};
|
|
286
|
+
window.addEventListener('scroll', saveScroll, { passive: true });
|
|
264
287
|
|
|
265
288
|
function handleClick(event: MouseEvent) {
|
|
266
289
|
const anchor = findAnchor(event);
|
|
@@ -278,7 +301,8 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
278
301
|
return;
|
|
279
302
|
}
|
|
280
303
|
|
|
281
|
-
// Same-page hash links:
|
|
304
|
+
// Same-page hash links: handle locally so we can clear search and
|
|
305
|
+
// close the header without a full SPA fetch.
|
|
282
306
|
const url = new URL(anchor.href);
|
|
283
307
|
if (
|
|
284
308
|
url.pathname === window.location.pathname &&
|
|
@@ -293,11 +317,11 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
293
317
|
details.open = false;
|
|
294
318
|
}
|
|
295
319
|
if (url.hash) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
320
|
+
// location.hash assignment performs a real fragment navigation:
|
|
321
|
+
// updates :target, fires hashchange (which the TOC listens for),
|
|
322
|
+
// creates a new history entry, and scrolls to the element.
|
|
323
|
+
// pushState would do none of those things.
|
|
324
|
+
window.location.hash = url.hash.slice(1);
|
|
301
325
|
}
|
|
302
326
|
return;
|
|
303
327
|
}
|
|
@@ -314,15 +338,8 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
314
338
|
details.open = false;
|
|
315
339
|
}
|
|
316
340
|
|
|
317
|
-
// Save current scroll position
|
|
318
|
-
window.
|
|
319
|
-
{
|
|
320
|
-
...window.history.state,
|
|
321
|
-
scrollY: window.scrollY,
|
|
322
|
-
navIndex: historyIndex,
|
|
323
|
-
},
|
|
324
|
-
'',
|
|
325
|
-
);
|
|
341
|
+
// Save current scroll position for the entry we're leaving
|
|
342
|
+
scrollByIndex.set(historyIndex, window.scrollY);
|
|
326
343
|
|
|
327
344
|
const hash = url.hash ? url.hash.slice(1) : null;
|
|
328
345
|
|
|
@@ -337,23 +354,23 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
337
354
|
if (hash) {
|
|
338
355
|
const el = window.document.getElementById(hash);
|
|
339
356
|
if (el) {
|
|
340
|
-
el.scrollIntoView(
|
|
357
|
+
el.scrollIntoView();
|
|
341
358
|
}
|
|
342
359
|
} else {
|
|
343
|
-
window.scrollTo({ top: 0
|
|
360
|
+
window.scrollTo({ top: 0 });
|
|
344
361
|
}
|
|
345
362
|
return;
|
|
346
363
|
}
|
|
347
364
|
currentPath = newPath;
|
|
348
365
|
|
|
349
|
-
const scrollY = event.state?.scrollY ?? null;
|
|
350
366
|
const targetIndex = event.state?.navIndex ?? 0;
|
|
351
367
|
const direction: Direction =
|
|
352
368
|
targetIndex >= historyIndex ? 'forward' : 'back';
|
|
353
369
|
historyIndex = targetIndex;
|
|
370
|
+
const savedY = scrollByIndex.get(targetIndex);
|
|
354
371
|
performNavigation(
|
|
355
372
|
window.location.href,
|
|
356
|
-
typeof
|
|
373
|
+
typeof savedY === 'number' ? savedY : null,
|
|
357
374
|
direction,
|
|
358
375
|
false,
|
|
359
376
|
);
|
|
@@ -367,5 +384,6 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
367
384
|
return () => {
|
|
368
385
|
window.document.removeEventListener('click', handleClick, true);
|
|
369
386
|
window.removeEventListener('popstate', handlePopState);
|
|
387
|
+
window.removeEventListener('scroll', saveScroll);
|
|
370
388
|
};
|
|
371
389
|
}
|
package/src/print/style.scss
CHANGED
package/src/style.scss
CHANGED
|
@@ -55,42 +55,23 @@ a.disabled {
|
|
|
55
55
|
@media (hover: hover) {
|
|
56
56
|
&:hover {
|
|
57
57
|
color: var(--fg-color-translucent);
|
|
58
|
+
text-decoration-line: none;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
background: var(--icon-external-link);
|
|
65
|
-
background-size: contain;
|
|
66
|
-
background-repeat: no-repeat;
|
|
67
|
-
background-position: center center;
|
|
68
|
-
margin-left: 4px;
|
|
69
|
-
padding-right: 18px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@media (hover: hover) {
|
|
73
|
-
a.external:hover::after {
|
|
74
|
-
background: var(--icon-external-link-hover);
|
|
75
|
-
background-size: contain;
|
|
76
|
-
background-repeat: no-repeat;
|
|
77
|
-
background-position: center center;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
a.external:active::after {
|
|
82
|
-
background: var(--icon-external-link-active) !important;
|
|
83
|
-
background-size: contain !important;
|
|
84
|
-
background-repeat: no-repeat !important;
|
|
85
|
-
background-position: center center !important;
|
|
63
|
+
.external-link-tail {
|
|
64
|
+
white-space: nowrap;
|
|
86
65
|
}
|
|
87
66
|
|
|
88
|
-
|
|
89
|
-
a.external.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
.external-link-tail::after,
|
|
68
|
+
a.external:not(:has(.external-link-tail))::after {
|
|
69
|
+
content: '';
|
|
70
|
+
display: inline-block;
|
|
71
|
+
width: 1em;
|
|
72
|
+
height: 1em;
|
|
73
|
+
vertical-align: middle;
|
|
74
|
+
@include contained-mask(var(--icon-external-link));
|
|
94
75
|
}
|
|
95
76
|
|
|
96
77
|
.footnotes {
|
|
@@ -379,21 +360,6 @@ dl {
|
|
|
379
360
|
dt {
|
|
380
361
|
font-weight: 600;
|
|
381
362
|
color: var(--theme-color-text);
|
|
382
|
-
|
|
383
|
-
a {
|
|
384
|
-
color: var(--theme-color-text);
|
|
385
|
-
text-decoration-color: var(--theme-color-text);
|
|
386
|
-
|
|
387
|
-
@media (hover: hover) {
|
|
388
|
-
&:hover {
|
|
389
|
-
text-decoration-color: var(--fg2-color);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
&:active {
|
|
394
|
-
text-decoration-color: var(--link-active-color);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
363
|
}
|
|
398
364
|
|
|
399
365
|
dd > *:first-child {
|
|
@@ -407,12 +373,17 @@ dfn {
|
|
|
407
373
|
font-style: italic;
|
|
408
374
|
}
|
|
409
375
|
|
|
410
|
-
.breadcrumb {
|
|
376
|
+
a.breadcrumb {
|
|
411
377
|
@include user-select(none);
|
|
378
|
+
@include subdued-link;
|
|
412
379
|
|
|
413
380
|
&::after {
|
|
414
|
-
content:
|
|
381
|
+
content: '';
|
|
382
|
+
display: inline-block;
|
|
383
|
+
width: 1em;
|
|
384
|
+
height: 1em;
|
|
415
385
|
vertical-align: middle;
|
|
386
|
+
@include contained-mask(var(--icon-breadcrumb));
|
|
416
387
|
}
|
|
417
388
|
}
|
|
418
389
|
|