@abreen/tada 1.6.0 → 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/build/bundle.ts +11 -1
- package/build/external-links-plugin.ts +181 -22
- package/build/utils/derive-theme.ts +8 -0
- package/build/utils/render.ts +15 -0
- package/package.json +1 -1
- 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/bundle.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getDistDir,
|
|
10
10
|
toPosix,
|
|
11
11
|
} from './utils/paths';
|
|
12
|
-
import { deriveTheme } from './utils/derive-theme';
|
|
12
|
+
import { deriveTheme, deriveLinkHue } from './utils/derive-theme';
|
|
13
13
|
import type { PluginBuilder } from 'bun';
|
|
14
14
|
import type { SiteVariables } from './types';
|
|
15
15
|
import timezones from '../src/timezone/timezones.json' with { type: 'json' };
|
|
@@ -27,6 +27,12 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
27
27
|
const iconColorHoverDark = `hsl(${tintHue}deg ${(6 * tintAmount) / 100}% 60%)`;
|
|
28
28
|
const iconColorTranslucentDark = `hsl(${tintHue}deg ${(85 * tintAmount) / 100}% 90%)`;
|
|
29
29
|
|
|
30
|
+
const linkHue = deriveLinkHue(tintHue);
|
|
31
|
+
const linkColor = `hsl(${linkHue}deg 44.4% 49.4%)`;
|
|
32
|
+
const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
|
|
33
|
+
const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
|
|
34
|
+
const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
|
|
35
|
+
|
|
30
36
|
const rendered = _.template(template)({
|
|
31
37
|
...theme,
|
|
32
38
|
tintHue,
|
|
@@ -36,6 +42,10 @@ function renderThemeScss(siteVariables: SiteVariables): string {
|
|
|
36
42
|
iconColorDark,
|
|
37
43
|
iconColorHoverDark,
|
|
38
44
|
iconColorTranslucentDark,
|
|
45
|
+
linkColor,
|
|
46
|
+
linkColorHover,
|
|
47
|
+
linkColorDark,
|
|
48
|
+
linkColorHoverDark,
|
|
39
49
|
});
|
|
40
50
|
|
|
41
51
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type MarkdownIt from 'markdown-it';
|
|
2
|
-
import
|
|
2
|
+
import Token from 'markdown-it/lib/token.mjs';
|
|
3
3
|
import { makeLogger } from './log';
|
|
4
4
|
import type { SiteVariables } from './types';
|
|
5
5
|
|
|
@@ -9,38 +9,197 @@ export default function externalLinks(
|
|
|
9
9
|
md: MarkdownIt,
|
|
10
10
|
siteVariables: SiteVariables,
|
|
11
11
|
): void {
|
|
12
|
-
function
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
function isExternal(href: string): boolean {
|
|
13
|
+
if (!href.match(/^https?:\/\/.*$/)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const url = new URL(href);
|
|
17
|
+
return !siteVariables.internalDomains?.includes(url.host);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findMatchingClose(children: Token[], openIdx: number): number {
|
|
21
|
+
let depth = 1;
|
|
22
|
+
for (let i = openIdx + 1; i < children.length; i++) {
|
|
23
|
+
if (children[i].type === 'link_open') {
|
|
24
|
+
depth++;
|
|
25
|
+
} else if (children[i].type === 'link_close') {
|
|
26
|
+
depth--;
|
|
27
|
+
if (depth === 0) {
|
|
28
|
+
return i;
|
|
29
|
+
}
|
|
17
30
|
}
|
|
31
|
+
}
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeSpanOpen(): Token {
|
|
36
|
+
const t = new Token('html_inline', '', 0);
|
|
37
|
+
t.content = '<span class="external-link-tail">';
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
18
40
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
function makeSpanClose(): Token {
|
|
42
|
+
const t = new Token('html_inline', '', 0);
|
|
43
|
+
t.content = '</span>';
|
|
44
|
+
return t;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Wrap the trailing word of an external link in
|
|
48
|
+
// <span class="external-link-tail"> so the icon (added via that span's
|
|
49
|
+
// ::after) stays glued to the last word and doesn't wrap on its own.
|
|
50
|
+
// The span is inserted inside any inline-formatting elements (em,
|
|
51
|
+
// strong, code, ...) that are still open at the split point, so the
|
|
52
|
+
// resulting markup stays nested.
|
|
53
|
+
function wrapTail(linkChildren: Token[]): Token[] | null {
|
|
54
|
+
if (linkChildren.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
24
57
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
58
|
+
// Find the rightmost split char (space or hyphen) in a text token,
|
|
59
|
+
// skipping any candidate that would produce an empty tail (e.g. a
|
|
60
|
+
// trailing hyphen with no content after it).
|
|
61
|
+
let splitTokenIdx = -1;
|
|
62
|
+
let splitCharIdx = -1;
|
|
63
|
+
for (let i = linkChildren.length - 1; i >= 0; i--) {
|
|
64
|
+
const tok = linkChildren[i];
|
|
65
|
+
if (tok.type !== 'text') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const text = tok.content;
|
|
69
|
+
const hasLaterTokens = i < linkChildren.length - 1;
|
|
70
|
+
let foundIdx = -1;
|
|
71
|
+
for (let j = text.length - 1; j >= 0; j--) {
|
|
72
|
+
const c = text[j];
|
|
73
|
+
if (c !== ' ' && c !== '-') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (j < text.length - 1 || hasLaterTokens) {
|
|
77
|
+
foundIdx = j;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (foundIdx !== -1) {
|
|
82
|
+
splitTokenIdx = i;
|
|
83
|
+
splitCharIdx = foundIdx;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
30
87
|
|
|
31
|
-
|
|
32
|
-
|
|
88
|
+
if (splitTokenIdx === -1) {
|
|
89
|
+
// No space anywhere in the link's text. Wrap the whole content.
|
|
90
|
+
return [makeSpanOpen(), ...linkChildren, makeSpanClose()];
|
|
91
|
+
}
|
|
33
92
|
|
|
34
|
-
|
|
35
|
-
|
|
93
|
+
// Find where to put the span close: walk forward from the split
|
|
94
|
+
// point tracking nesting depth (locally opened elements after the
|
|
95
|
+
// split). The span must close before any -1 token that would close
|
|
96
|
+
// an element that was already open at the split point.
|
|
97
|
+
let spanCloseIdx = linkChildren.length;
|
|
98
|
+
let depth = 0;
|
|
99
|
+
for (let i = splitTokenIdx + 1; i < linkChildren.length; i++) {
|
|
100
|
+
const tok = linkChildren[i];
|
|
101
|
+
if (tok.nesting === 1) {
|
|
102
|
+
depth++;
|
|
103
|
+
} else if (tok.nesting === -1) {
|
|
104
|
+
if (depth > 0) {
|
|
105
|
+
depth--;
|
|
106
|
+
} else {
|
|
107
|
+
spanCloseIdx = i;
|
|
108
|
+
break;
|
|
36
109
|
}
|
|
37
110
|
}
|
|
38
111
|
}
|
|
39
112
|
|
|
40
|
-
|
|
113
|
+
const splitToken = linkChildren[splitTokenIdx];
|
|
114
|
+
const before = splitToken.content.slice(0, splitCharIdx + 1);
|
|
115
|
+
const after = splitToken.content.slice(splitCharIdx + 1);
|
|
116
|
+
|
|
117
|
+
const result: Token[] = [];
|
|
118
|
+
|
|
119
|
+
// Tokens before the split, unchanged.
|
|
120
|
+
for (let i = 0; i < splitTokenIdx; i++) {
|
|
121
|
+
result.push(linkChildren[i]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The "before" part of the split text token.
|
|
125
|
+
if (before.length > 0) {
|
|
126
|
+
const beforeToken = new Token('text', '', 0);
|
|
127
|
+
beforeToken.content = before;
|
|
128
|
+
result.push(beforeToken);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result.push(makeSpanOpen());
|
|
132
|
+
|
|
133
|
+
// The "after" part of the split text token.
|
|
134
|
+
if (after.length > 0) {
|
|
135
|
+
const afterToken = new Token('text', '', 0);
|
|
136
|
+
afterToken.content = after;
|
|
137
|
+
result.push(afterToken);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Tokens between the split and the span close.
|
|
141
|
+
for (let i = splitTokenIdx + 1; i < spanCloseIdx; i++) {
|
|
142
|
+
result.push(linkChildren[i]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
result.push(makeSpanClose());
|
|
146
|
+
|
|
147
|
+
// Tokens from the span close position to the end.
|
|
148
|
+
for (let i = spanCloseIdx; i < linkChildren.length; i++) {
|
|
149
|
+
result.push(linkChildren[i]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function processChildren(children: Token[]): Token[] {
|
|
156
|
+
const result: Token[] = [];
|
|
157
|
+
let i = 0;
|
|
158
|
+
while (i < children.length) {
|
|
159
|
+
const token = children[i];
|
|
160
|
+
|
|
161
|
+
if (token.type !== 'link_open') {
|
|
162
|
+
result.push(token);
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const href = token.attrGet('href');
|
|
168
|
+
if (!href || !isExternal(href)) {
|
|
169
|
+
result.push(token);
|
|
170
|
+
i++;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const classAttr = token.attrGet('class');
|
|
175
|
+
const newClassAttr = classAttr ? `${classAttr} external` : 'external';
|
|
176
|
+
token.attrSet('class', newClassAttr);
|
|
177
|
+
token.attrSet('target', '_blank');
|
|
178
|
+
log.debug`${href} -> "${newClassAttr}"`;
|
|
179
|
+
|
|
180
|
+
const closeIdx = findMatchingClose(children, i);
|
|
181
|
+
if (closeIdx === -1) {
|
|
182
|
+
result.push(token);
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const linkContent = children.slice(i + 1, closeIdx);
|
|
188
|
+
const wrapped = wrapTail(linkContent);
|
|
189
|
+
|
|
190
|
+
result.push(token);
|
|
191
|
+
result.push(...(wrapped ?? linkContent));
|
|
192
|
+
result.push(children[closeIdx]);
|
|
193
|
+
i = closeIdx + 1;
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
41
196
|
}
|
|
42
197
|
|
|
43
198
|
md.core.ruler.push('external_links', state => {
|
|
44
|
-
state.tokens.
|
|
199
|
+
state.tokens.forEach(token => {
|
|
200
|
+
if (token.children) {
|
|
201
|
+
token.children = processChildren(token.children);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
45
204
|
});
|
|
46
205
|
}
|
|
@@ -14,6 +14,9 @@ const LIGHT_TEXT_L_MAX = 0.5;
|
|
|
14
14
|
const DARK_TEXT_L_MIN = 0.7;
|
|
15
15
|
const DARK_TEXT_L_MAX = 0.82;
|
|
16
16
|
|
|
17
|
+
const LINK_ANCHOR_HUE = 212;
|
|
18
|
+
const LINK_TINT_BLEND = 0.05;
|
|
19
|
+
|
|
17
20
|
function clamp(v: number, min: number, max: number): number {
|
|
18
21
|
return Math.min(Math.max(v, min), max);
|
|
19
22
|
}
|
|
@@ -75,6 +78,11 @@ export function deriveTheme(cssColor: string): DerivedTheme {
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
export function deriveLinkHue(tintHue: number): number {
|
|
82
|
+
const diff = ((tintHue - LINK_ANCHOR_HUE + 540) % 360) - 180;
|
|
83
|
+
return (LINK_ANCHOR_HUE + diff * LINK_TINT_BLEND + 360) % 360;
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
export function getTextOnColor(cssColor: string): '#fff' | '#000' {
|
|
79
87
|
const parsed = parse(cssColor);
|
|
80
88
|
if (!parsed) {
|
package/build/utils/render.ts
CHANGED
|
@@ -46,12 +46,17 @@ const log = makeLogger(import.meta.url);
|
|
|
46
46
|
|
|
47
47
|
const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
|
|
48
48
|
|
|
49
|
+
function isWatchMode(assetFiles: string[]): boolean {
|
|
50
|
+
return assetFiles.some(f => f.includes('watch-reload-client'));
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
interface TemplateParametersInput {
|
|
50
54
|
pageVariables: Record<string, unknown>;
|
|
51
55
|
siteVariables: SiteVariables;
|
|
52
56
|
content: string | null;
|
|
53
57
|
applyBasePath: (subPath: string) => string;
|
|
54
58
|
subPath: string;
|
|
59
|
+
isWatchMode: boolean;
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
function resolveAuthor(
|
|
@@ -101,6 +106,7 @@ function createTemplateParameters({
|
|
|
101
106
|
content,
|
|
102
107
|
applyBasePath,
|
|
103
108
|
subPath,
|
|
109
|
+
isWatchMode,
|
|
104
110
|
}: TemplateParametersInput): Record<string, unknown> {
|
|
105
111
|
return {
|
|
106
112
|
vars: siteVariables.vars || {},
|
|
@@ -111,6 +117,7 @@ function createTemplateParameters({
|
|
|
111
117
|
page: pageVariables,
|
|
112
118
|
content,
|
|
113
119
|
applyBasePath,
|
|
120
|
+
isWatchMode,
|
|
114
121
|
};
|
|
115
122
|
}
|
|
116
123
|
|
|
@@ -188,12 +195,14 @@ export function renderPlainTextPageAsset({
|
|
|
188
195
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
189
196
|
|
|
190
197
|
log.info`Rendering page ${B`${subPath + ext}`}`;
|
|
198
|
+
const watchMode = isWatchMode(assetFiles);
|
|
191
199
|
const { content, pageVariables, tocItems, alertIds } = renderPlainTextContent(
|
|
192
200
|
filePath,
|
|
193
201
|
subPath,
|
|
194
202
|
siteVariables,
|
|
195
203
|
applyBasePath,
|
|
196
204
|
validInternalTargets,
|
|
205
|
+
watchMode,
|
|
197
206
|
{
|
|
198
207
|
validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()),
|
|
199
208
|
traceCache,
|
|
@@ -223,6 +232,7 @@ export function renderPlainTextPageAsset({
|
|
|
223
232
|
content,
|
|
224
233
|
applyBasePath,
|
|
225
234
|
subPath,
|
|
235
|
+
isWatchMode: watchMode,
|
|
226
236
|
});
|
|
227
237
|
|
|
228
238
|
const templateHtml = render(
|
|
@@ -286,6 +296,7 @@ export function renderCodePageAsset({
|
|
|
286
296
|
content,
|
|
287
297
|
applyBasePath,
|
|
288
298
|
subPath,
|
|
299
|
+
isWatchMode: isWatchMode(assetFiles),
|
|
289
300
|
});
|
|
290
301
|
|
|
291
302
|
const html = injectAssetTags(
|
|
@@ -339,6 +350,7 @@ function renderPlainTextContent(
|
|
|
339
350
|
siteVariables: SiteVariables,
|
|
340
351
|
applyBasePath: (subPath: string) => string,
|
|
341
352
|
validInternalTargets: Set<string>,
|
|
353
|
+
isWatchMode: boolean,
|
|
342
354
|
{
|
|
343
355
|
validateInternalLinks = true,
|
|
344
356
|
traceCache,
|
|
@@ -396,6 +408,7 @@ function renderPlainTextContent(
|
|
|
396
408
|
content: null,
|
|
397
409
|
applyBasePath,
|
|
398
410
|
subPath,
|
|
411
|
+
isWatchMode,
|
|
399
412
|
});
|
|
400
413
|
const pageVariablesProcessed: Record<string, unknown> = Object.entries(
|
|
401
414
|
pageVariables,
|
|
@@ -446,6 +459,7 @@ function renderPlainTextContent(
|
|
|
446
459
|
content: strippedContent,
|
|
447
460
|
applyBasePath,
|
|
448
461
|
subPath,
|
|
462
|
+
isWatchMode,
|
|
449
463
|
});
|
|
450
464
|
|
|
451
465
|
if (traceCache && contentDir && distDir) {
|
|
@@ -649,6 +663,7 @@ export function renderLiterateJavaPageAsset({
|
|
|
649
663
|
content: contentHtml,
|
|
650
664
|
applyBasePath,
|
|
651
665
|
subPath,
|
|
666
|
+
isWatchMode: isWatchMode(assetFiles),
|
|
652
667
|
});
|
|
653
668
|
|
|
654
669
|
const templateHtml = render('literate.html', templateParameters) as string;
|
package/package.json
CHANGED
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
|
|
package/src/toc/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ function scrollIfNeeded(element: HTMLElement, doc: Document) {
|
|
|
37
37
|
elementRect.height / 2;
|
|
38
38
|
const desiredScrollTop = elementCenter - container.clientHeight / 2;
|
|
39
39
|
|
|
40
|
-
container.scrollTo({ top: desiredScrollTop
|
|
40
|
+
container.scrollTo({ top: desiredScrollTop });
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function getHighlightIndexes(items: (Heading | Alert | Dinkus)[]) {
|
package/src/toc/style.scss
CHANGED
|
@@ -127,51 +127,22 @@ body.code .toc ol a {
|
|
|
127
127
|
position: relative;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Preload hover icon variants so the browser decodes them before first hover
|
|
131
|
-
.toc ol li.alert-item.warning::after {
|
|
132
|
-
content: var(--icon-warning-sm-hover);
|
|
133
|
-
position: absolute;
|
|
134
|
-
width: 0;
|
|
135
|
-
height: 0;
|
|
136
|
-
overflow: hidden;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.toc ol li.alert-item.note::after {
|
|
140
|
-
content: var(--icon-note-sm-hover);
|
|
141
|
-
position: absolute;
|
|
142
|
-
width: 0;
|
|
143
|
-
height: 0;
|
|
144
|
-
overflow: hidden;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
130
|
.toc ol li.alert-item.warning a::before,
|
|
148
131
|
.toc ol li.alert-item.note a::before {
|
|
132
|
+
content: '';
|
|
149
133
|
display: inline-block;
|
|
134
|
+
width: 20px;
|
|
150
135
|
height: 20px;
|
|
151
136
|
vertical-align: bottom;
|
|
152
137
|
margin-right: 4px;
|
|
153
138
|
}
|
|
154
139
|
|
|
155
140
|
.toc ol li.alert-item.warning a::before {
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@media (hover: hover) {
|
|
160
|
-
.toc ol li.alert-item.warning:hover a::before,
|
|
161
|
-
.toc ol li.alert-item.warning a:hover::before {
|
|
162
|
-
content: var(--icon-warning-sm-hover);
|
|
163
|
-
}
|
|
141
|
+
@include contained-mask(var(--icon-warning));
|
|
164
142
|
}
|
|
165
143
|
|
|
166
144
|
.toc ol li.alert-item.note a::before {
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
@media (hover: hover) {
|
|
171
|
-
.toc ol li.alert-item.note:hover a::before,
|
|
172
|
-
.toc ol li.alert-item.note a:hover::before {
|
|
173
|
-
content: var(--icon-note-sm-hover);
|
|
174
|
-
}
|
|
145
|
+
@include contained-mask(var(--icon-note));
|
|
175
146
|
}
|
|
176
147
|
|
|
177
148
|
.toc ol li.label {
|
package/src/top/index.ts
CHANGED
|
@@ -30,11 +30,15 @@ export default (window: Window) => {
|
|
|
30
30
|
e.preventDefault();
|
|
31
31
|
const cleanUrl = window.location.pathname + window.location.search;
|
|
32
32
|
if (window.location.hash) {
|
|
33
|
-
|
|
33
|
+
// Clear the hash via fragment navigation so :target clears and
|
|
34
|
+
// hashchange fires. location.hash = '' may leave a trailing '#';
|
|
35
|
+
// strip it with replaceState.
|
|
36
|
+
window.location.hash = '';
|
|
37
|
+
window.history.replaceState(null, '', cleanUrl);
|
|
34
38
|
} else {
|
|
35
39
|
window.history.replaceState(null, '', cleanUrl);
|
|
36
40
|
}
|
|
37
|
-
window.scrollTo({ top: 0
|
|
41
|
+
window.scrollTo({ top: 0 });
|
|
38
42
|
const toc = window.document.querySelector('nav.toc') as HTMLElement | null;
|
|
39
43
|
if (toc) {
|
|
40
44
|
toc.scrollTop = 0;
|
package/templates/_theme.scss
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
// Secondary foreground color (lighter)
|
|
22
22
|
--fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 45%);
|
|
23
23
|
|
|
24
|
+
--link-color: <%= linkColor %>;
|
|
25
|
+
--link-color-hover: <%= linkColorHover %>;
|
|
26
|
+
|
|
24
27
|
// Main background color (the color of the page background)
|
|
25
28
|
--bg-color: hsl(var(--tint-hue) calc(5% * var(--tint-amount)) 97%);
|
|
26
29
|
--bg-color-translucent: hsl(
|
|
@@ -56,20 +59,14 @@
|
|
|
56
59
|
--mono-border-radius: 0.375em;
|
|
57
60
|
|
|
58
61
|
// SVG icons
|
|
59
|
-
|
|
60
|
-
--icon-external-link
|
|
61
|
-
--icon-
|
|
62
|
-
--icon-
|
|
63
|
-
--icon-
|
|
64
|
-
--icon-warning-sm: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='<%= iconColor %>' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
65
|
-
--icon-warning-sm-hover: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='<%= iconColorHover %>' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='<%= iconColorHover %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='<%= iconColorHover %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
66
|
-
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='<%= iconColor %>' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
67
|
-
--icon-note-sm: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='<%= iconColor %>' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
68
|
-
--icon-note-sm-hover: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='<%= iconColorHover %>' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='<%= iconColorHover %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='<%= iconColorHover %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
62
|
+
// Mask icons (color-agnostic, painted via background-color: currentColor)
|
|
63
|
+
--icon-external-link: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M6 18 L20 4 M10 4 H20 V14' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
64
|
+
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 28'><path d='M11 4 L8 20 M17 4 L14 20 M5 9 H21 M3 15 H19' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
65
|
+
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='black' stroke-width='1.5' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
66
|
+
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='black' stroke-width='1.5'/><line x1='12' y1='10' x2='12' y2='16' stroke='black' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
69
67
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColor %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColor %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
70
|
-
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='<%= iconColor %>' fill-rule='evenodd' clip-rule='evenodd' d='M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0zm2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z'/></svg>");
|
|
71
68
|
--icon-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6' stroke='<%= iconColor %>' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
|
72
|
-
--icon-breadcrumb: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M3 7 H17 V22' stroke='
|
|
69
|
+
--icon-breadcrumb: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M3 7 H17 V22' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/><polyline points='11 16 17 22 23 16' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
73
70
|
|
|
74
71
|
color-scheme: light dark;
|
|
75
72
|
}
|
|
@@ -85,6 +82,8 @@
|
|
|
85
82
|
var(--tint-hue) calc(85% * var(--tint-amount)) 90% / 33%
|
|
86
83
|
);
|
|
87
84
|
--fg2-color: hsl(var(--tint-hue) calc(6% * var(--tint-amount)) 60%);
|
|
85
|
+
--link-color: <%= linkColorDark %>;
|
|
86
|
+
--link-color-hover: <%= linkColorHoverDark %>;
|
|
88
87
|
--bg-color: hsl(var(--tint-hue) calc(3% * var(--tint-amount)) 5%);
|
|
89
88
|
--bg-color-translucent: hsl(
|
|
90
89
|
var(--tint-hue) calc(15% * var(--tint-amount)) 10% / 66%
|
|
@@ -103,19 +102,7 @@
|
|
|
103
102
|
--note-bg-color: hsl(209 90% 25%);
|
|
104
103
|
|
|
105
104
|
// SVG icons (dark overrides)
|
|
106
|
-
--icon-external-link: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'><rect x='3' y='3' width='18' height='18' stroke='<%= iconColorDark %>' stroke-width='2' fill='none' rx='2'/><path d='M9 15 L15 9' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round' fill='none'/><path d='M9 9 H15 V15' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round' fill='none'/></svg>");
|
|
107
|
-
--icon-external-link-hover: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'><rect x='3' y='3' width='18' height='18' stroke='<%= iconColorHoverDark %>' stroke-width='2' fill='none' rx='2'/><path d='M9 15 L15 9' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linecap='round' fill='none'/><path d='M9 9 H15 V15' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linecap='round' fill='none'/></svg>");
|
|
108
|
-
--icon-external-link-active: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'><rect x='3' y='3' width='18' height='18' stroke='white' stroke-width='2' fill='none' rx='2'/><path d='M9 15 L15 9' stroke='white' stroke-width='2' stroke-linecap='round' fill='none'/><path d='M9 9 H15 V15' stroke='white' stroke-width='2' stroke-linecap='round' fill='none'/></svg>");
|
|
109
|
-
--icon-external-translucent: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'><g opacity='0.3' stroke='<%= iconColorTranslucentDark %>' stroke-width='2' stroke-linecap='round' fill='none'><rect x='3' y='3' width='18' height='18' rx='2'/><path d='M9 15 L15 9' stroke-linecap='round'/><path d='M9 9 H15 V15' stroke-linecap='round'/></g></svg>");
|
|
110
|
-
--icon-warning: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
111
|
-
--icon-warning-sm: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
112
|
-
--icon-warning-sm-hover: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
113
|
-
--icon-note: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='<%= iconColorDark %>' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
114
|
-
--icon-note-sm: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='<%= iconColorDark %>' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
115
|
-
--icon-note-sm-hover: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='<%= iconColorHoverDark %>' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='<%= iconColorHoverDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
116
105
|
--icon-search: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><circle cx='11' cy='11' r='7' stroke='<%= iconColorDark %>' stroke-width='2' fill='none'/><line x1='16.5' y1='16.5' x2='22' y2='22' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round'/></svg>");
|
|
117
|
-
--icon-anchor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path fill='<%= iconColorDark %>' fill-rule='evenodd' clip-rule='evenodd' d='M10.975 14.51a1.05 1.05 0 0 0 0-1.485 2.95 2.95 0 0 1 0-4.172l3.536-3.535a2.95 2.95 0 1 1 4.172 4.172l-1.093 1.092a1.05 1.05 0 0 0 1.485 1.485l1.093-1.092a5.05 5.05 0 0 0-7.142-7.142L9.49 7.368a5.05 5.05 0 0 0 0 7.142c.41.41 1.075.41 1.485 0zm2.05-5.02a1.05 1.05 0 0 0 0 1.485 2.95 2.95 0 0 1 0 4.172l-3.5 3.5a2.95 2.95 0 1 1-4.171-4.172l1.025-1.025a1.05 1.05 0 0 0-1.485-1.485L3.87 12.99a5.05 5.05 0 0 0 7.142 7.142l3.5-3.5a5.05 5.05 0 0 0 0-7.142 1.05 1.05 0 0 0-1.485 0z'/></svg>");
|
|
118
106
|
--icon-chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><polyline points='9 18 15 12 9 6' stroke='<%= iconColorDark %>' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
|
|
119
|
-
--icon-breadcrumb: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'><path d='M3 7 H17 V22' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'/><polyline points='11 16 17 22 23 16' stroke='<%= iconColorDark %>' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'/></svg>");
|
|
120
107
|
}
|
|
121
108
|
}
|
package/templates/_top.html
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<meta property="og:author" content="<%= page.author.name %>" />
|
|
34
34
|
<% } %>
|
|
35
35
|
<title><%= page.title %><%= site.titlePostfix %></title>
|
|
36
|
+
<% if (!isWatchMode) { %>
|
|
36
37
|
<script type="speculationrules">
|
|
37
38
|
{
|
|
38
39
|
"prerender": [{
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
}]
|
|
44
45
|
}
|
|
45
46
|
</script>
|
|
47
|
+
<% } %>
|
|
46
48
|
<script>document.head.appendChild(document.createElement('style')).textContent='.time-zone-label[hidden],select.time-zone[hidden]{display:revert}'</script>
|
|
47
49
|
<noscript><style>
|
|
48
50
|
.question-a-body:not([data-revealed]) > * { color: unset; background-color: unset; }
|