@abreen/tada 1.17.1 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -7
- package/assets/material-symbols/chevron_right_24px.svg +1 -0
- package/bin/tada.ts +16 -7
- package/bin/validators.ts +33 -0
- package/build/branding.ts +41 -0
- package/build/build-manifest.ts +9 -4
- package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
- package/build/build-validation.ts +176 -0
- package/build/bundle.ts +75 -65
- package/build/config-loader.ts +3 -3
- package/build/custom-fonts.ts +8 -5
- package/build/generate-favicon.ts +7 -6
- package/build/generate-fonts.ts +8 -15
- package/build/generate-katex-assets.ts +19 -17
- package/build/generate-web-app-manifest.ts +3 -11
- package/build/lodash-template.ts +14 -0
- package/build/material-symbols.ts +7 -4
- package/build/output-publication.ts +238 -0
- package/build/pagefind.ts +49 -21
- package/build/pdf-text.ts +41 -1
- package/build/pipeline.ts +95 -101
- package/build/site-assets.ts +46 -0
- package/build/site-build.ts +267 -0
- package/build/source-model.ts +223 -244
- package/build/source-records.ts +69 -81
- package/build/template-globals.ts +3 -0
- package/build/templates.ts +5 -2
- package/build/toc-plugin.ts +21 -21
- package/build/types.d.ts +44 -38
- package/build/util.ts +0 -4
- package/build/utils/code.ts +38 -101
- package/build/utils/final-html.ts +171 -47
- package/build/utils/literate-java.ts +11 -7
- package/build/utils/markdown-partials.ts +4 -3
- package/build/utils/markdown.ts +6 -17
- package/build/utils/paths.ts +3 -0
- package/build/utils/plain-text.ts +18 -0
- package/build/utils/render.ts +227 -165
- package/build/utils/shiki-highlighter.ts +11 -0
- package/build/utils/source-template.ts +2 -2
- package/build/utils/trace-core.ts +8 -83
- package/build/utils/trace.ts +38 -95
- package/build/validate-config-links.ts +60 -20
- package/build/watch/build-incremental.ts +46 -103
- package/build/watch/compiler.ts +158 -37
- package/build/watch/engine.ts +207 -176
- package/build/watch/index.ts +9 -2
- package/build/watch/planner.ts +88 -144
- package/build/watch/runtime.ts +21 -34
- package/build/watch/types.d.ts +32 -64
- package/init/content/labs/00/VowelCounter.java.md +5 -2
- package/init/content/labs/00/index.md +3 -2
- package/init/content/labs/01/index.md +3 -2
- package/init/content/labs/02/index.md +3 -2
- package/init/content/lectures/01/index.md +3 -2
- package/init/content/lectures/02/index.md +3 -2
- package/package.json +4 -4
- package/schema/site.schema.json +18 -2
- package/src/_content.scss +463 -0
- package/src/_fonts.scss +90 -0
- package/src/_layout.scss +50 -35
- package/src/{material-symbols.scss → _material-symbols.scss} +7 -0
- package/src/appearance-picker/{style.scss → _index.scss} +1 -1
- package/src/code/{style.scss → _index.scss} +61 -0
- package/src/header/{_base.scss → _index.scss} +98 -6
- package/src/index.ts +1 -17
- package/src/navigate/{style.scss → _index.scss} +29 -3
- package/src/navigate/breadcrumbs.ts +129 -0
- package/src/navigate/eligible.ts +0 -40
- package/src/navigate/index.ts +38 -4
- package/src/navigate/lifecycle.ts +19 -2
- package/src/navigate/runtime.ts +57 -22
- package/src/page-update/index.ts +33 -20
- package/src/print/{style.scss → _index.scss} +3 -1
- package/src/search/index.ts +7 -0
- package/src/slides/{style.scss → _index.scss} +5 -0
- package/src/style.scss +24 -536
- package/src/toc/index.ts +2 -2
- package/templates/_author.html +2 -2
- package/templates/_download.html +1 -1
- package/templates/_footer.html +1 -1
- package/templates/_heading.html +17 -4
- package/templates/_nav.html +2 -2
- package/templates/_page-bottom.html +6 -0
- package/templates/_top.html +18 -14
- package/templates/code.html +22 -21
- package/templates/default.html +12 -11
- package/templates/literate.html +6 -5
- package/assets/material-symbols/south_east_20px.svg +0 -1
- package/build/copy.ts +0 -82
- package/build/generate-content-assets.ts +0 -121
- package/build/utils/content-files.ts +0 -170
- package/build/watch/assets.ts +0 -103
- package/build/watch/build-full.ts +0 -97
- package/build/watch/build-helpers.ts +0 -66
- package/build/watch/build-result.ts +0 -19
- package/build/watch/fs-commit.ts +0 -218
- package/build/watch/mutations.ts +0 -73
- package/build/watch/snapshot.ts +0 -201
- package/build/watch/validation.ts +0 -57
- package/src/code.scss +0 -60
- package/src/critical.scss +0 -5
- package/src/header/style.scss +0 -91
- /package/src/{literate.scss → _literate.scss} +0 -0
- /package/src/anchor/{style.scss → _index.scss} +0 -0
- /package/src/page-update/{style.scss → _index.scss} +0 -0
- /package/src/question/{style.scss → _index.scss} +0 -0
- /package/src/search/{style.scss → _index.scss} +0 -0
- /package/src/timezone/{style.scss → _index.scss} +0 -0
- /package/src/toc/{style.scss → _index.scss} +0 -0
- /package/src/trace/{style.scss → _index.scss} +0 -0
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
@use '../mixins' as *;
|
|
2
|
+
|
|
3
|
+
body.code {
|
|
4
|
+
--code-gutter-width: 4rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.file-header,
|
|
8
|
+
.slides-header {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: var(--gap);
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
padding: var(--gap);
|
|
14
|
+
margin-bottom: var(--gap);
|
|
15
|
+
border: 1px solid var(--fg2-color);
|
|
16
|
+
border-radius: var(--border-radius);
|
|
17
|
+
|
|
18
|
+
tt,
|
|
19
|
+
code {
|
|
20
|
+
padding: 0;
|
|
21
|
+
background: none;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.code-row {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: var(--code-gutter-width) 1fr;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.line-number {
|
|
31
|
+
position: sticky;
|
|
32
|
+
left: 0;
|
|
33
|
+
z-index: 1;
|
|
34
|
+
display: block;
|
|
35
|
+
padding: 0 0.75em;
|
|
36
|
+
margin-right: var(--gap);
|
|
37
|
+
font-family: var(--mono-font);
|
|
38
|
+
font-size-adjust: var(--mono-font-size-adjust);
|
|
39
|
+
font-feature-settings: var(--mono-font-feature-settings);
|
|
40
|
+
color: var(--fg2-color);
|
|
41
|
+
text-align: right;
|
|
42
|
+
text-decoration: none;
|
|
43
|
+
background: var(--bg2-color);
|
|
44
|
+
border-right: 1px solid var(--fg2-color);
|
|
45
|
+
|
|
46
|
+
@include user-select(none);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
a.line-number {
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
|
|
52
|
+
&:hover {
|
|
53
|
+
color: var(--fg-color);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:target {
|
|
57
|
+
color: var(--bg-color);
|
|
58
|
+
background: var(--fg2-color);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
1
62
|
body.code .file-header {
|
|
2
63
|
h1 {
|
|
3
64
|
padding: 0;
|
|
@@ -191,6 +191,16 @@ header summary .logo {
|
|
|
191
191
|
background: var(--theme-color);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
header summary .logo-image {
|
|
195
|
+
flex: 0 0 auto;
|
|
196
|
+
width: auto;
|
|
197
|
+
max-width: 96px;
|
|
198
|
+
height: 24px;
|
|
199
|
+
margin: 0 8px 0 0;
|
|
200
|
+
pointer-events: none;
|
|
201
|
+
object-fit: contain;
|
|
202
|
+
}
|
|
203
|
+
|
|
194
204
|
header summary .site-title {
|
|
195
205
|
flex: 1 1 0;
|
|
196
206
|
min-width: 0;
|
|
@@ -230,12 +240,6 @@ header .to-top-container {
|
|
|
230
240
|
}
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
// Hide until the full stylesheet loads (prevents FOUC where the container
|
|
234
|
-
// is visible as a block element before it gets position: absolute)
|
|
235
|
-
header .results-container {
|
|
236
|
-
display: none;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
243
|
header .search-controls {
|
|
240
244
|
position: relative;
|
|
241
245
|
display: flex;
|
|
@@ -298,3 +302,91 @@ header.loading::after {
|
|
|
298
302
|
background-size: 200% 100%;
|
|
299
303
|
animation: shimmer 3s ease-in-out infinite;
|
|
300
304
|
}
|
|
305
|
+
|
|
306
|
+
header nav {
|
|
307
|
+
display: flex;
|
|
308
|
+
flex-wrap: wrap;
|
|
309
|
+
|
|
310
|
+
@include user-select(none);
|
|
311
|
+
|
|
312
|
+
gap: var(--gap);
|
|
313
|
+
margin: var(--gap);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
header nav div {
|
|
317
|
+
flex: 1 1 150px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
header nav ul {
|
|
321
|
+
padding: 0;
|
|
322
|
+
margin: 8px 0 0;
|
|
323
|
+
list-style-type: none;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
header nav ul li {
|
|
327
|
+
margin: 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
header nav a {
|
|
331
|
+
display: inline;
|
|
332
|
+
border: none;
|
|
333
|
+
|
|
334
|
+
@include subdued-link;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
header nav a.external {
|
|
338
|
+
border: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@media only screen and (width >= 740px) {
|
|
342
|
+
header details nav {
|
|
343
|
+
min-height: 100px;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
header nav div > p {
|
|
348
|
+
padding-bottom: 2px;
|
|
349
|
+
font-style: italic;
|
|
350
|
+
border-bottom: 1px solid var(--fg-color);
|
|
351
|
+
|
|
352
|
+
@include user-select(none);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
:root[data-contrast-preference='high'] header:has(details[open]) {
|
|
356
|
+
background: var(--bg2-color);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
@media (hover: hover) {
|
|
360
|
+
header:has(details[open]):hover {
|
|
361
|
+
background: var(--bg-color-translucent);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:root[data-contrast-preference='high'] header:hover {
|
|
365
|
+
background: var(--bg2-color);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
header .results-container {
|
|
370
|
+
position: absolute;
|
|
371
|
+
top: var(--header-collapsed-height);
|
|
372
|
+
right: 0;
|
|
373
|
+
left: 0;
|
|
374
|
+
z-index: 1000;
|
|
375
|
+
box-sizing: border-box;
|
|
376
|
+
max-width: 640px;
|
|
377
|
+
padding: var(--gap);
|
|
378
|
+
margin: 0 auto;
|
|
379
|
+
pointer-events: none;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
header ol {
|
|
383
|
+
flex: 1;
|
|
384
|
+
min-height: 0;
|
|
385
|
+
padding: 0;
|
|
386
|
+
overflow-y: auto;
|
|
387
|
+
|
|
388
|
+
.excerpt {
|
|
389
|
+
height: 5ex;
|
|
390
|
+
line-height: 2.5ex;
|
|
391
|
+
}
|
|
392
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
+
// The single Sass entry point; it loads every other stylesheet
|
|
1
2
|
import './style.scss';
|
|
2
3
|
|
|
3
|
-
import './material-symbols.scss';
|
|
4
|
-
import './anchor/style.scss';
|
|
5
|
-
import './code.scss';
|
|
6
|
-
import './code/style.scss';
|
|
7
|
-
import './literate.scss';
|
|
8
|
-
import './header/style.scss';
|
|
9
|
-
import './print/style.scss';
|
|
10
|
-
import './question/style.scss';
|
|
11
|
-
import './search/style.scss';
|
|
12
|
-
import './slides/style.scss';
|
|
13
|
-
import './timezone/style.scss';
|
|
14
|
-
import './toc/style.scss';
|
|
15
|
-
import './trace/style.scss';
|
|
16
|
-
import './appearance-picker/style.scss';
|
|
17
|
-
import './navigate/style.scss';
|
|
18
|
-
import './page-update/style.scss';
|
|
19
|
-
|
|
20
4
|
import mountSearch from './search';
|
|
21
5
|
import mountHeader from './header';
|
|
22
6
|
import mountTop from './top';
|
|
@@ -2,6 +2,11 @@ $shift: 8px;
|
|
|
2
2
|
$duration: 150ms;
|
|
3
3
|
$movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
4
4
|
|
|
5
|
+
:root {
|
|
6
|
+
--page-transition-duration: #{$duration};
|
|
7
|
+
--page-transition-movement-easing: #{$movement-easing};
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
@keyframes page-fade-out {
|
|
6
11
|
to {
|
|
7
12
|
opacity: 0;
|
|
@@ -45,12 +50,13 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
45
50
|
::view-transition-old(page-title),
|
|
46
51
|
::view-transition-new(page-title),
|
|
47
52
|
::view-transition-old(page-info),
|
|
48
|
-
::view-transition-new(page-info)
|
|
49
|
-
::view-transition-old(page-breadcrumb),
|
|
50
|
-
::view-transition-new(page-breadcrumb) {
|
|
53
|
+
::view-transition-new(page-info) {
|
|
51
54
|
animation-duration: $duration;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
// Breadcrumbs use per-item named rules installed by the navigator. Shared
|
|
58
|
+
// items stay opaque and preserve their position; removed/added suffixes fade.
|
|
59
|
+
|
|
54
60
|
// The site-wide banner does not change between pages. Keep it out of the root
|
|
55
61
|
// slide and pin its named transition layer in place without a crossfade.
|
|
56
62
|
.site-banner {
|
|
@@ -68,6 +74,26 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
68
74
|
mix-blend-mode: normal;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
// The footer and appearance controls get their own stationary layer. Show only
|
|
78
|
+
// the incoming snapshot so disabled defaults and stale hover states cannot blend.
|
|
79
|
+
.page-bottom {
|
|
80
|
+
view-transition-name: page-bottom;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
::view-transition-group(page-bottom),
|
|
84
|
+
::view-transition-old(page-bottom),
|
|
85
|
+
::view-transition-new(page-bottom) {
|
|
86
|
+
animation: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
::view-transition-old(page-bottom) {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
::view-transition-new(page-bottom) {
|
|
94
|
+
mix-blend-mode: normal;
|
|
95
|
+
}
|
|
96
|
+
|
|
71
97
|
// Forward navigation: old content slides left, new content slides in from right
|
|
72
98
|
.nav-forward::view-transition-old(root) {
|
|
73
99
|
animation:
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
interface BreadcrumbIdentity {
|
|
2
|
+
label: string;
|
|
3
|
+
url: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface BreadcrumbItem extends BreadcrumbIdentity {
|
|
7
|
+
element: HTMLElement;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function breadcrumbPageKey(href: string, pageUrl: string): string {
|
|
11
|
+
const url = new URL(href, pageUrl);
|
|
12
|
+
// Fragments identify positions on the same page; query strings can identify
|
|
13
|
+
// different content. URL serialization also resolves relative paths/spaces.
|
|
14
|
+
return url.origin + url.pathname + url.search;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sharedBreadcrumbCount(
|
|
18
|
+
oldItems: readonly BreadcrumbIdentity[],
|
|
19
|
+
newItems: readonly BreadcrumbIdentity[],
|
|
20
|
+
): number {
|
|
21
|
+
let count = 0;
|
|
22
|
+
while (
|
|
23
|
+
count < Math.min(oldItems.length, newItems.length) &&
|
|
24
|
+
oldItems[count].url === newItems[count].url &&
|
|
25
|
+
oldItems[count].label === newItems[count].label
|
|
26
|
+
) {
|
|
27
|
+
count++;
|
|
28
|
+
}
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getBreadcrumbItems(
|
|
33
|
+
document: Document,
|
|
34
|
+
pageUrl: string,
|
|
35
|
+
): BreadcrumbItem[] {
|
|
36
|
+
return Array.from(
|
|
37
|
+
document.querySelectorAll<HTMLElement>('nav.breadcrumbs ol > li'),
|
|
38
|
+
).flatMap(element => {
|
|
39
|
+
const text = element.querySelector('a, [aria-current="page"]');
|
|
40
|
+
if (!text) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const href = text.getAttribute('href') ?? pageUrl;
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
element,
|
|
47
|
+
label: (text.textContent ?? '').trim().replace(/\s+/g, ' '),
|
|
48
|
+
url: breadcrumbPageKey(href, pageUrl),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setBreadcrumbTransitionNames(
|
|
55
|
+
document: Document,
|
|
56
|
+
pageUrl: string,
|
|
57
|
+
sharedCount: number,
|
|
58
|
+
side: 'enter' | 'leave',
|
|
59
|
+
): void {
|
|
60
|
+
const breadcrumb = document.querySelector<HTMLElement>('nav.breadcrumbs');
|
|
61
|
+
if (!breadcrumb) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
getBreadcrumbItems(document, pageUrl).forEach((item, index) => {
|
|
65
|
+
const kind = index < sharedCount ? 'shared' : side;
|
|
66
|
+
item.element.style.viewTransitionName = `page-breadcrumb-${kind}-${index}`;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function prepareBreadcrumbTransitionStyles(
|
|
71
|
+
document: Document,
|
|
72
|
+
oldCount: number,
|
|
73
|
+
newCount: number,
|
|
74
|
+
sharedCount: number,
|
|
75
|
+
): void {
|
|
76
|
+
if (!oldCount && !newCount) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const style = document.createElement('style');
|
|
80
|
+
style.setAttribute('data-breadcrumb-transition-styles', '');
|
|
81
|
+
const rules: string[] = [];
|
|
82
|
+
const duration = 'var(--page-transition-duration)';
|
|
83
|
+
for (let index = 0; index < sharedCount; index++) {
|
|
84
|
+
const name = `page-breadcrumb-shared-${index}`;
|
|
85
|
+
rules.push(`
|
|
86
|
+
::view-transition-group(${name}) {
|
|
87
|
+
animation-duration: ${duration};
|
|
88
|
+
animation-timing-function: var(--page-transition-movement-easing);
|
|
89
|
+
}
|
|
90
|
+
::view-transition-old(${name}), ::view-transition-new(${name}) {
|
|
91
|
+
mix-blend-mode: normal;
|
|
92
|
+
animation: none;
|
|
93
|
+
}
|
|
94
|
+
::view-transition-old(${name}) { display: none; }
|
|
95
|
+
::view-transition-old(${name}):only-child {
|
|
96
|
+
display: block;
|
|
97
|
+
animation: page-fade-out ${duration} ease-in both;
|
|
98
|
+
}
|
|
99
|
+
::view-transition-new(${name}):only-child {
|
|
100
|
+
animation: page-fade-in ${duration} ease-out both;
|
|
101
|
+
}
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
for (let index = sharedCount; index < oldCount; index++) {
|
|
105
|
+
rules.push(`::view-transition-old(page-breadcrumb-leave-${index}) {
|
|
106
|
+
mix-blend-mode: normal;
|
|
107
|
+
animation: page-fade-out ${duration} ease-in both;
|
|
108
|
+
}`);
|
|
109
|
+
}
|
|
110
|
+
for (let index = sharedCount; index < newCount; index++) {
|
|
111
|
+
rules.push(`::view-transition-new(page-breadcrumb-enter-${index}) {
|
|
112
|
+
mix-blend-mode: normal;
|
|
113
|
+
animation: page-fade-in ${duration} ease-out both;
|
|
114
|
+
}`);
|
|
115
|
+
}
|
|
116
|
+
// Exact names keep these rules compatible with the bundled CSS parser, which
|
|
117
|
+
// does not yet accept view-transition-class selectors. Names use indices only.
|
|
118
|
+
style.textContent = rules.join('\n');
|
|
119
|
+
document.head.appendChild(style);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function cleanupBreadcrumbTransitionNames(document: Document): void {
|
|
123
|
+
for (const element of document.querySelectorAll<HTMLElement>(
|
|
124
|
+
'nav.breadcrumbs, nav.breadcrumbs li',
|
|
125
|
+
)) {
|
|
126
|
+
element.style.removeProperty('view-transition-name');
|
|
127
|
+
}
|
|
128
|
+
document.querySelector('[data-breadcrumb-transition-styles]')?.remove();
|
|
129
|
+
}
|
package/src/navigate/eligible.ts
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
const NON_HTML_EXTENSIONS = new Set([
|
|
2
|
-
'.pdf',
|
|
3
|
-
'.java',
|
|
4
|
-
'.py',
|
|
5
|
-
'.png',
|
|
6
|
-
'.jpg',
|
|
7
|
-
'.jpeg',
|
|
8
|
-
'.gif',
|
|
9
|
-
'.svg',
|
|
10
|
-
'.zip',
|
|
11
|
-
'.tar',
|
|
12
|
-
'.gz',
|
|
13
|
-
'.json',
|
|
14
|
-
'.xml',
|
|
15
|
-
'.css',
|
|
16
|
-
'.js',
|
|
17
|
-
'.ts',
|
|
18
|
-
'.c',
|
|
19
|
-
'.h',
|
|
20
|
-
'.cpp',
|
|
21
|
-
'.rb',
|
|
22
|
-
'.go',
|
|
23
|
-
'.rs',
|
|
24
|
-
'.txt',
|
|
25
|
-
'.csv',
|
|
26
|
-
'.ico',
|
|
27
|
-
'.woff2',
|
|
28
|
-
'.mp4',
|
|
29
|
-
'.mp3',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
1
|
export function isEligibleLink(
|
|
33
2
|
href: string,
|
|
34
3
|
origin: string,
|
|
@@ -56,14 +25,5 @@ export function isEligibleLink(
|
|
|
56
25
|
return false;
|
|
57
26
|
}
|
|
58
27
|
|
|
59
|
-
const path = url.pathname;
|
|
60
|
-
const dotIndex = path.lastIndexOf('.');
|
|
61
|
-
if (dotIndex !== -1) {
|
|
62
|
-
const ext = path.slice(dotIndex).toLowerCase();
|
|
63
|
-
if (NON_HTML_EXTENSIONS.has(ext)) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
28
|
return true;
|
|
69
29
|
}
|
package/src/navigate/index.ts
CHANGED
|
@@ -24,11 +24,17 @@ function shouldIgnoreClick(
|
|
|
24
24
|
event: MouseEvent,
|
|
25
25
|
anchor: HTMLAnchorElement,
|
|
26
26
|
): boolean {
|
|
27
|
-
if (
|
|
27
|
+
if (
|
|
28
|
+
event.button !== 0 ||
|
|
29
|
+
event.ctrlKey ||
|
|
30
|
+
event.metaKey ||
|
|
31
|
+
event.shiftKey ||
|
|
32
|
+
event.altKey
|
|
33
|
+
) {
|
|
28
34
|
return true;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
if (anchor.target || anchor.hasAttribute('download')) {
|
|
37
|
+
if (anchor.hasAttribute('target') || anchor.hasAttribute('download')) {
|
|
32
38
|
return true;
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -42,6 +48,7 @@ function shouldIgnoreClick(
|
|
|
42
48
|
|
|
43
49
|
export default function mountNavigate(window: Window): () => void {
|
|
44
50
|
initNavigation(window);
|
|
51
|
+
let clearingFragment = false;
|
|
45
52
|
|
|
46
53
|
// Track scroll position on every scroll event. We keep the latest
|
|
47
54
|
// scrollY per navIndex so that back/forward navigation can restore
|
|
@@ -88,6 +95,10 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
88
95
|
saveScrollPosition(window);
|
|
89
96
|
if (window.location.hash) {
|
|
90
97
|
const state = window.history.state;
|
|
98
|
+
// WebKit fires popstate synchronously while clearing the fragment.
|
|
99
|
+
// Ignore that internal navigation so it cannot restore the previous
|
|
100
|
+
// fragment-free entry's saved scroll position.
|
|
101
|
+
clearingFragment = true;
|
|
91
102
|
globals.setLocationHash(window, '');
|
|
92
103
|
// Native fragment clearing leaves a trailing #. Keep the authored
|
|
93
104
|
// URL and the current SPA history state on the new entry.
|
|
@@ -99,6 +110,10 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
99
110
|
return;
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
if (!anchor.hasAttribute('data-tada-page')) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
102
117
|
event.preventDefault();
|
|
103
118
|
|
|
104
119
|
// Clear search and close header immediately, before the fetch
|
|
@@ -119,11 +134,30 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
119
134
|
}
|
|
120
135
|
|
|
121
136
|
function handlePopState(event: PopStateEvent) {
|
|
137
|
+
if (clearingFragment) {
|
|
138
|
+
clearingFragment = false;
|
|
139
|
+
if (!window.location.hash) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
const newPath = window.location.pathname + window.location.search;
|
|
123
145
|
if (newPath === getCurrentPath()) {
|
|
124
|
-
const
|
|
146
|
+
const locationKey = newPath + window.location.hash;
|
|
147
|
+
const savedY = getSavedLocationScroll(locationKey);
|
|
125
148
|
if (typeof savedY === 'number') {
|
|
126
|
-
|
|
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);
|
|
127
161
|
return;
|
|
128
162
|
}
|
|
129
163
|
|
|
@@ -21,15 +21,31 @@ const PER_PAGE_COMPONENTS: Record<
|
|
|
21
21
|
trace: mountTrace,
|
|
22
22
|
slides: mountSlides,
|
|
23
23
|
print: mountPrint,
|
|
24
|
-
appearancePicker: mountAppearancePicker,
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
let cleanups: (() => void)[] = [];
|
|
27
|
+
let appearancePickerMounted = false;
|
|
28
|
+
|
|
29
|
+
// Navigation calls this during the swap so the new transition snapshot shows
|
|
30
|
+
// enabled switches with the visitor's preferences, rather than build defaults.
|
|
31
|
+
export function mountAppearancePickerForPage(window: Window): void {
|
|
32
|
+
if (appearancePickerMounted) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
appearancePickerMounted = true;
|
|
36
|
+
try {
|
|
37
|
+
cleanups.push(mountAppearancePicker(window));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (__IS_DEV__) {
|
|
40
|
+
console.error('Failed to mount appearancePicker component:', String(err));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
|
|
29
45
|
export async function mountPerPageComponents(
|
|
30
46
|
window: Window,
|
|
31
47
|
): Promise<() => void> {
|
|
32
|
-
|
|
48
|
+
mountAppearancePickerForPage(window);
|
|
33
49
|
|
|
34
50
|
const entries = Object.entries(PER_PAGE_COMPONENTS);
|
|
35
51
|
|
|
@@ -66,4 +82,5 @@ export function teardownPerPageComponents(): void {
|
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
cleanups = [];
|
|
85
|
+
appearancePickerMounted = false;
|
|
69
86
|
}
|