@abreen/tada 1.19.1 → 1.19.3
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/watch/engine.ts +62 -6
- package/package.json +1 -1
- package/src/hash-target.ts +21 -2
- package/src/navigate/_index.scss +17 -6
- package/src/navigate/index.ts +23 -12
- package/src/navigate/runtime.ts +159 -1
- package/src/toc/_index.scss +6 -1
- package/templates/_theme.scss +1 -1
package/build/watch/engine.ts
CHANGED
|
@@ -22,6 +22,10 @@ export function runWatchEngine<Meta>(
|
|
|
22
22
|
const debounceMs = options.debounceMs ?? 300;
|
|
23
23
|
const watchers: ReturnType<typeof chokidar.watch>[] = [];
|
|
24
24
|
const pending = new Set<string>();
|
|
25
|
+
const removed = new Map<
|
|
26
|
+
string,
|
|
27
|
+
{ watcher: ReturnType<typeof chokidar.watch>; root: string }
|
|
28
|
+
>();
|
|
25
29
|
let uncommitted = new Set<string>();
|
|
26
30
|
let closed = false;
|
|
27
31
|
let fatal: { error: unknown } | undefined;
|
|
@@ -33,6 +37,7 @@ export function runWatchEngine<Meta>(
|
|
|
33
37
|
function stop(): void {
|
|
34
38
|
closed = true;
|
|
35
39
|
pending.clear();
|
|
40
|
+
removed.clear();
|
|
36
41
|
if (timer !== undefined) {
|
|
37
42
|
clock.clearTimeout(timer);
|
|
38
43
|
}
|
|
@@ -78,6 +83,52 @@ export function runWatchEngine<Meta>(
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
|
|
86
|
+
function isFile(filePath: string): boolean {
|
|
87
|
+
try {
|
|
88
|
+
return dependencies.stat(filePath)?.isFile() ?? false;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOTDIR') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isInside(root: string, filePath: string): boolean {
|
|
98
|
+
const relative = path.relative(root, filePath);
|
|
99
|
+
return (
|
|
100
|
+
relative !== '' &&
|
|
101
|
+
relative !== '..' &&
|
|
102
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
103
|
+
!path.isAbsolute(relative)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Chokidar tracks entries by name, so a directory replaced by a file keeps
|
|
109
|
+
* its directory subscription and edits to the file are never reported. When
|
|
110
|
+
* a poll misses the moment between the two, the only events are removals of
|
|
111
|
+
* what was inside the directory. Once changes have settled, resubscribe any
|
|
112
|
+
* parent of a removed path that is now a file.
|
|
113
|
+
*/
|
|
114
|
+
function resubscribeReplacedDirectories(): void {
|
|
115
|
+
const checked = new Set<string>();
|
|
116
|
+
for (const [removedPath, { watcher, root }] of removed) {
|
|
117
|
+
for (
|
|
118
|
+
let dir = path.dirname(removedPath);
|
|
119
|
+
isInside(root, dir) && !checked.has(dir);
|
|
120
|
+
dir = path.dirname(dir)
|
|
121
|
+
) {
|
|
122
|
+
checked.add(dir);
|
|
123
|
+
if (isFile(dir)) {
|
|
124
|
+
watcher.unwatch(dir);
|
|
125
|
+
watcher.add(dir);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
removed.clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
81
132
|
async function emit(event: WatchLifecycleEvent<Meta>): Promise<void> {
|
|
82
133
|
if (!closed) {
|
|
83
134
|
await options.onEvent?.(event);
|
|
@@ -139,7 +190,13 @@ export function runWatchEngine<Meta>(
|
|
|
139
190
|
fail(error);
|
|
140
191
|
}
|
|
141
192
|
};
|
|
142
|
-
|
|
193
|
+
const removedPath = (filePath: string) => {
|
|
194
|
+
if (!closed) {
|
|
195
|
+
removed.set(filePath, { watcher, root: target.path });
|
|
196
|
+
}
|
|
197
|
+
included(filePath);
|
|
198
|
+
};
|
|
199
|
+
watcher.on('add', included).on('unlink', removedPath);
|
|
143
200
|
watcher.on('change', (filePath, stats) => {
|
|
144
201
|
if (closed) {
|
|
145
202
|
return;
|
|
@@ -156,20 +213,18 @@ export function runWatchEngine<Meta>(
|
|
|
156
213
|
});
|
|
157
214
|
watcher.on('addDir', included);
|
|
158
215
|
watcher.on('unlinkDir', filePath => {
|
|
159
|
-
|
|
216
|
+
removedPath(filePath);
|
|
160
217
|
// Wait until Chokidar finishes closing the old directory subscription.
|
|
161
218
|
queueMicrotask(() => {
|
|
162
219
|
if (closed) {
|
|
163
220
|
return;
|
|
164
221
|
}
|
|
165
222
|
try {
|
|
166
|
-
if (
|
|
223
|
+
if (isFile(filePath)) {
|
|
167
224
|
watcher.add(filePath);
|
|
168
225
|
}
|
|
169
226
|
} catch (error) {
|
|
170
|
-
|
|
171
|
-
fail(error);
|
|
172
|
-
}
|
|
227
|
+
fail(error);
|
|
173
228
|
}
|
|
174
229
|
});
|
|
175
230
|
});
|
|
@@ -197,6 +252,7 @@ export function runWatchEngine<Meta>(
|
|
|
197
252
|
while (!closed && pending.size) {
|
|
198
253
|
const paths = new Set([...uncommitted, ...pending]);
|
|
199
254
|
pending.clear();
|
|
255
|
+
resubscribeReplacedDirectories();
|
|
200
256
|
const outcome = await build(paths);
|
|
201
257
|
if (!outcome?.ok) {
|
|
202
258
|
uncommitted = paths;
|
package/package.json
CHANGED
package/src/hash-target.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Tada also allows raw HTML, so an author can create a decoded ID like
|
|
6
6
|
* <h2 id="hello world">Hello</h2>. A link to that target becomes
|
|
7
7
|
* #hello%20world, and getElementById('hello%20world') would not find it.
|
|
8
|
+
*
|
|
9
|
+
* Like the browser, fall back to a legacy <a name="..."> anchor when no
|
|
10
|
+
* element has the ID.
|
|
8
11
|
*/
|
|
9
12
|
export function getHashTarget(
|
|
10
13
|
document: Document,
|
|
@@ -15,15 +18,31 @@ export function getHashTarget(
|
|
|
15
18
|
return null;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
const rawTarget = document
|
|
21
|
+
const rawTarget = findIndicatedElement(document, id);
|
|
19
22
|
if (rawTarget) {
|
|
20
23
|
return rawTarget;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
try {
|
|
24
27
|
const decodedId = decodeURIComponent(id);
|
|
25
|
-
return decodedId === id ? null : document
|
|
28
|
+
return decodedId === id ? null : findIndicatedElement(document, decodedId);
|
|
26
29
|
} catch {
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
33
|
+
|
|
34
|
+
function findIndicatedElement(
|
|
35
|
+
document: Document,
|
|
36
|
+
fragment: string,
|
|
37
|
+
): HTMLElement | null {
|
|
38
|
+
const byId = document.getElementById(fragment);
|
|
39
|
+
if (byId) {
|
|
40
|
+
return byId;
|
|
41
|
+
}
|
|
42
|
+
for (const element of document.getElementsByName(fragment)) {
|
|
43
|
+
if (element.localName === 'a') {
|
|
44
|
+
return element;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
package/src/navigate/_index.scss
CHANGED
|
@@ -105,12 +105,10 @@ html.navigation-loading * {
|
|
|
105
105
|
mix-blend-mode: normal;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// The footer and appearance controls get their own
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
108
|
+
// The footer and appearance controls get their own layer while they are in the
|
|
109
|
+
// viewport. view-transition-name is set via inline style by the navigator. On
|
|
110
|
+
// screen on both pages, the layer stays put and shows only the incoming
|
|
111
|
+
// snapshot so disabled defaults and stale hover states cannot blend.
|
|
114
112
|
::view-transition-group(page-bottom),
|
|
115
113
|
::view-transition-old(page-bottom),
|
|
116
114
|
::view-transition-new(page-bottom) {
|
|
@@ -125,6 +123,19 @@ html.navigation-loading * {
|
|
|
125
123
|
mix-blend-mode: normal;
|
|
126
124
|
}
|
|
127
125
|
|
|
126
|
+
// On screen on only one page, such as between a short page and the top of a
|
|
127
|
+
// long page: fade out or in where they are, in step with the page. Fill both
|
|
128
|
+
// ways, like the UA default, so the faded-out snapshot cannot reappear in the
|
|
129
|
+
// transition's last frame.
|
|
130
|
+
::view-transition-old(page-bottom):only-child {
|
|
131
|
+
display: block;
|
|
132
|
+
animation: page-fade-out $duration ease-in both;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
::view-transition-new(page-bottom):only-child {
|
|
136
|
+
animation: page-fade-in $duration ease-out both;
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
// Forward navigation: old content slides left, new content slides in from right
|
|
129
140
|
.nav-forward::view-transition-old(root) {
|
|
130
141
|
animation:
|
package/src/navigate/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isEligibleLink } from './eligible';
|
|
2
2
|
import {
|
|
3
|
+
applyFragmentTarget,
|
|
3
4
|
clearSearch,
|
|
4
5
|
closeHeaderDetails,
|
|
5
6
|
getCurrentPath,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
getHistoryIndex,
|
|
8
9
|
getSavedScroll,
|
|
9
10
|
initNavigation,
|
|
11
|
+
isApplyingFragment,
|
|
10
12
|
navigateToUrl,
|
|
11
13
|
saveScrollPosition,
|
|
12
14
|
setCurrentPath,
|
|
@@ -134,6 +136,10 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function handlePopState(event: PopStateEvent) {
|
|
139
|
+
if (isApplyingFragment()) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
if (clearingFragment) {
|
|
138
144
|
clearingFragment = false;
|
|
139
145
|
if (!window.location.hash) {
|
|
@@ -145,19 +151,24 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
145
151
|
if (newPath === getCurrentPath()) {
|
|
146
152
|
const locationKey = newPath + window.location.hash;
|
|
147
153
|
const savedY = getSavedLocationScroll(locationKey);
|
|
154
|
+
// Fragment traversal can update :target and apply its native target
|
|
155
|
+
// scroll after popstate. Correct both in the next task so the URL and our
|
|
156
|
+
// saved position win. Capture savedY now because the native scroll event
|
|
157
|
+
// can update the location map first.
|
|
158
|
+
window.setTimeout(() => {
|
|
159
|
+
const currentLocationKey =
|
|
160
|
+
window.location.pathname +
|
|
161
|
+
window.location.search +
|
|
162
|
+
window.location.hash;
|
|
163
|
+
if (currentLocationKey !== locationKey) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
applyFragmentTarget(window);
|
|
167
|
+
if (typeof savedY === 'number') {
|
|
168
|
+
window.scrollTo({ top: savedY });
|
|
169
|
+
}
|
|
170
|
+
}, 0);
|
|
148
171
|
if (typeof savedY === 'number') {
|
|
149
|
-
// Fragment traversal can apply its native target scroll after popstate.
|
|
150
|
-
// Restore in the next task so our saved position wins. Capture savedY
|
|
151
|
-
// now because the native scroll event can update the location map first.
|
|
152
|
-
window.setTimeout(() => {
|
|
153
|
-
const currentLocationKey =
|
|
154
|
-
window.location.pathname +
|
|
155
|
-
window.location.search +
|
|
156
|
-
window.location.hash;
|
|
157
|
-
if (currentLocationKey === locationKey) {
|
|
158
|
-
window.scrollTo({ top: savedY });
|
|
159
|
-
}
|
|
160
|
-
}, 0);
|
|
161
172
|
return;
|
|
162
173
|
}
|
|
163
174
|
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -5,18 +5,31 @@ import {
|
|
|
5
5
|
teardownPerPageComponents,
|
|
6
6
|
} from './lifecycle';
|
|
7
7
|
import { globals } from '../globals';
|
|
8
|
+
import { getHashTarget } from '../hash-target';
|
|
8
9
|
import { swapHeaderTitle } from '../header';
|
|
9
10
|
|
|
10
11
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
11
12
|
|
|
12
13
|
const LOADING_CURSOR_DELAY = 400;
|
|
14
|
+
const HOLD_SCROLL_FRAMES = 6;
|
|
15
|
+
const HOLD_SCROLL_RELEASE_EVENTS = [
|
|
16
|
+
'wheel',
|
|
17
|
+
'touchstart',
|
|
18
|
+
'keydown',
|
|
19
|
+
'pointerdown',
|
|
20
|
+
] as const;
|
|
13
21
|
|
|
14
22
|
let currentAbortController: AbortController | null = null;
|
|
15
23
|
let historyIndex = 0;
|
|
16
24
|
let currentPath = '';
|
|
25
|
+
let applyingFragment = false;
|
|
26
|
+
let fragmentTargetRequest = 0;
|
|
17
27
|
|
|
18
28
|
const scrollByIndex = new Map<number, number>();
|
|
19
29
|
const scrollByLocation = new Map<string, number>();
|
|
30
|
+
// Stylesheets the navigator added that have not loaded or failed yet. Those in
|
|
31
|
+
// the initial document have settled before the deferred client script runs.
|
|
32
|
+
const pendingStylesheets = new Map<Element, Promise<void>>();
|
|
20
33
|
|
|
21
34
|
type Direction = 'forward' | 'back';
|
|
22
35
|
|
|
@@ -65,7 +78,19 @@ function updateHead(document: Document, newDoc: Document): void {
|
|
|
65
78
|
|
|
66
79
|
for (const link of newDoc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
67
80
|
if (!existingHrefs.has(link.getAttribute('href'))) {
|
|
68
|
-
|
|
81
|
+
const clone = link.cloneNode(true) as Element;
|
|
82
|
+
pendingStylesheets.set(
|
|
83
|
+
clone,
|
|
84
|
+
new Promise(resolve => {
|
|
85
|
+
const settle = () => {
|
|
86
|
+
pendingStylesheets.delete(clone);
|
|
87
|
+
resolve();
|
|
88
|
+
};
|
|
89
|
+
clone.addEventListener('load', settle, { once: true });
|
|
90
|
+
clone.addEventListener('error', settle, { once: true });
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
document.head.appendChild(clone);
|
|
69
94
|
}
|
|
70
95
|
}
|
|
71
96
|
}
|
|
@@ -138,11 +163,29 @@ function setTitleTransitionNames(titleEl: Element, enabled: boolean): void {
|
|
|
138
163
|
}
|
|
139
164
|
}
|
|
140
165
|
|
|
166
|
+
// The footer and appearance pickers get their own layer only while they are in
|
|
167
|
+
// the viewport, so the layer stays put when they are on screen on both pages
|
|
168
|
+
// and fades on its own when they are on screen on only one.
|
|
169
|
+
function setPageBottomTransitionName(window: Window): void {
|
|
170
|
+
const bottom = window.document.querySelector<HTMLElement>('.page-bottom');
|
|
171
|
+
if (!bottom) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const rect = bottom.getBoundingClientRect();
|
|
175
|
+
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
|
176
|
+
bottom.style.viewTransitionName = 'page-bottom';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
141
180
|
function cleanupViewTransitionNames(document: Document): void {
|
|
142
181
|
const titleEl = document.querySelector('.title-and-info');
|
|
143
182
|
if (titleEl) {
|
|
144
183
|
setTitleTransitionNames(titleEl, false);
|
|
145
184
|
}
|
|
185
|
+
const bottom = document.querySelector<HTMLElement>('.page-bottom');
|
|
186
|
+
if (bottom) {
|
|
187
|
+
bottom.style.viewTransitionName = '';
|
|
188
|
+
}
|
|
146
189
|
}
|
|
147
190
|
|
|
148
191
|
function getLocationKey(window: Window): string {
|
|
@@ -213,6 +256,111 @@ export function getSavedLocationScroll(
|
|
|
213
256
|
return scrollByLocation.get(locationKey);
|
|
214
257
|
}
|
|
215
258
|
|
|
259
|
+
// Browsers fire popstate synchronously during applyFragmentTarget
|
|
260
|
+
export function isApplyingFragment(): boolean {
|
|
261
|
+
return applyingFragment;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Restoring a history entry can leave :target stale. With manual scroll
|
|
265
|
+
// restoration, WebKit skips fragment processing on Back/Forward, and swapped
|
|
266
|
+
// content loses it in every browser. A fragment navigation sets it in every
|
|
267
|
+
// browser, but it can reset the entry's history state and scroll position, so
|
|
268
|
+
// restore both.
|
|
269
|
+
//
|
|
270
|
+
// WebKit defers fragment scrolling until pending stylesheets load, which would
|
|
271
|
+
// later move the page away from the restored scroll position. Wait for them
|
|
272
|
+
// first, and skip the update if another request or navigation replaced it.
|
|
273
|
+
//
|
|
274
|
+
// That deferred scroll can also set :target before the wait ends, which would
|
|
275
|
+
// leave nothing to replace. WebKit then scrolls to the target again on the next
|
|
276
|
+
// frame, undoing a scroll back to the saved position. A fragment navigation of
|
|
277
|
+
// our own does not have that problem, so after waiting, repeat it even when
|
|
278
|
+
// :target matches, restoring the saved position the caller passed in. When the
|
|
279
|
+
// last stylesheet finishes loading just after the content swap, WebKit can
|
|
280
|
+
// still scroll to the target in the next rendering update, after our restore,
|
|
281
|
+
// so hold that position for a few frames.
|
|
282
|
+
export function applyFragmentTarget(
|
|
283
|
+
window: Window,
|
|
284
|
+
restoredTop?: number,
|
|
285
|
+
): void {
|
|
286
|
+
const request = ++fragmentTargetRequest;
|
|
287
|
+
const { href } = window.location;
|
|
288
|
+
let waited = false;
|
|
289
|
+
const apply = (): void => {
|
|
290
|
+
if (request !== fragmentTargetRequest || window.location.href !== href) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const pending = Array.from(pendingStylesheets.values());
|
|
294
|
+
if (pending.length > 0) {
|
|
295
|
+
waited = true;
|
|
296
|
+
void Promise.all(pending).then(apply);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
replaceFragmentTarget(window, waited ? restoredTop : undefined);
|
|
300
|
+
};
|
|
301
|
+
apply();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function replaceFragmentTarget(window: Window, forcedTop?: number): void {
|
|
305
|
+
const { document, history, location } = window;
|
|
306
|
+
if (
|
|
307
|
+
forcedTop === undefined &&
|
|
308
|
+
document.querySelector(':target') === getHashTarget(document, location.hash)
|
|
309
|
+
) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const { href } = location;
|
|
313
|
+
const state = history.state;
|
|
314
|
+
const { scrollX } = window;
|
|
315
|
+
const scrollY = forcedTop ?? window.scrollY;
|
|
316
|
+
applyingFragment = true;
|
|
317
|
+
try {
|
|
318
|
+
globals.replaceLocation(
|
|
319
|
+
window,
|
|
320
|
+
location.pathname + location.search + (location.hash || '#'),
|
|
321
|
+
);
|
|
322
|
+
} finally {
|
|
323
|
+
applyingFragment = false;
|
|
324
|
+
}
|
|
325
|
+
history.replaceState(state, '', href);
|
|
326
|
+
window.scrollTo({ left: scrollX, top: scrollY });
|
|
327
|
+
if (forcedTop !== undefined) {
|
|
328
|
+
holdScroll(window, scrollX, scrollY);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Scroll back to the given position on each of the next few frames if the
|
|
333
|
+
// browser moved the page, until the visitor scrolls or the URL changes
|
|
334
|
+
function holdScroll(window: Window, left: number, top: number): void {
|
|
335
|
+
const { href } = window.location;
|
|
336
|
+
let held = true;
|
|
337
|
+
const release = (): void => {
|
|
338
|
+
held = false;
|
|
339
|
+
for (const type of HOLD_SCROLL_RELEASE_EVENTS) {
|
|
340
|
+
window.removeEventListener(type, release);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
for (const type of HOLD_SCROLL_RELEASE_EVENTS) {
|
|
344
|
+
window.addEventListener(type, release, { passive: true });
|
|
345
|
+
}
|
|
346
|
+
let frames = 0;
|
|
347
|
+
const tick = (): void => {
|
|
348
|
+
if (!held || window.location.href !== href) {
|
|
349
|
+
release();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (Math.abs(window.scrollY - top) > 1) {
|
|
353
|
+
window.scrollTo({ left, top });
|
|
354
|
+
}
|
|
355
|
+
if (++frames < HOLD_SCROLL_FRAMES) {
|
|
356
|
+
window.requestAnimationFrame(tick);
|
|
357
|
+
} else {
|
|
358
|
+
release();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
window.requestAnimationFrame(tick);
|
|
362
|
+
}
|
|
363
|
+
|
|
216
364
|
export async function navigateToUrl(
|
|
217
365
|
window: Window,
|
|
218
366
|
options: NavigationOptions,
|
|
@@ -323,6 +471,14 @@ export async function navigateToUrl(
|
|
|
323
471
|
|
|
324
472
|
teardownPerPageComponents();
|
|
325
473
|
swapContent(document, newDoc);
|
|
474
|
+
if (!pushHistory) {
|
|
475
|
+
// Restore the entry's :target before updateHead adds stylesheets, so it
|
|
476
|
+
// applies right away unless an earlier stylesheet is still loading
|
|
477
|
+
applyFragmentTarget(
|
|
478
|
+
window,
|
|
479
|
+
typeof scrollTarget === 'number' ? scrollTarget : undefined,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
326
482
|
updateHead(document, newDoc);
|
|
327
483
|
mountAppearancePickerForPage(window);
|
|
328
484
|
currentPath = parsed.pathname + parsed.search;
|
|
@@ -369,6 +525,7 @@ export async function navigateToUrl(
|
|
|
369
525
|
if (titleVisible) {
|
|
370
526
|
setTitleTransitionNames(titleEl, true);
|
|
371
527
|
}
|
|
528
|
+
setPageBottomTransitionName(window);
|
|
372
529
|
|
|
373
530
|
document.documentElement.classList.add(
|
|
374
531
|
direction === 'forward' ? 'nav-forward' : 'nav-back',
|
|
@@ -389,6 +546,7 @@ export async function navigateToUrl(
|
|
|
389
546
|
if (newTitleVisible && newTitleEl) {
|
|
390
547
|
setTitleTransitionNames(newTitleEl, true);
|
|
391
548
|
}
|
|
549
|
+
setPageBottomTransitionName(window);
|
|
392
550
|
});
|
|
393
551
|
|
|
394
552
|
await transition.finished;
|
package/src/toc/_index.scss
CHANGED
|
@@ -149,10 +149,15 @@ body.code .toc ol a {
|
|
|
149
149
|
width: var(--toc-alert-icon-size);
|
|
150
150
|
height: var(--toc-alert-icon-size);
|
|
151
151
|
margin-right: 4px;
|
|
152
|
-
vertical-align:
|
|
152
|
+
vertical-align: middle;
|
|
153
153
|
content: '';
|
|
154
154
|
|
|
155
155
|
@include contained-mask(var(--toc-alert-icon));
|
|
156
|
+
|
|
157
|
+
// Center the icon on the cap height, which scales with each level's font
|
|
158
|
+
@supports (width: 1cap) {
|
|
159
|
+
vertical-align: calc(0.5cap - var(--toc-alert-icon-size) / 2);
|
|
160
|
+
}
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
.toc ol li.label {
|
package/templates/_theme.scss
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
'Segoe UI Symbol';
|
|
55
55
|
--sans-mono-font: 'Google Sans Code', 'Consolas', 'Courier', monospace;
|
|
56
56
|
--serif-font: <%= serifFontStack %>;
|
|
57
|
-
--serif-preview-font: 'Times New Roman', 'Times', serif;
|
|
57
|
+
--serif-preview-font: 'Georgia', 'Times New Roman', 'Times', serif;
|
|
58
58
|
--serif-mono-font: <%= serifMonoFontStack %>;
|
|
59
59
|
--sans-font-feature-settings:
|
|
60
60
|
'liga', 'calt', 'ss07', 'ss08', 'cv01', 'cv08', 'cv10';
|