@abreen/tada 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +41 -7
  2. package/bin/tada.ts +23 -9
  3. package/bin/validators.ts +34 -1
  4. package/build/branding.ts +41 -0
  5. package/build/build-manifest.ts +10 -5
  6. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  7. package/build/build-validation.ts +176 -0
  8. package/build/bundle.ts +75 -65
  9. package/build/columns-plugin.ts +27 -1
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/deflist-id-plugin.ts +2 -2
  13. package/build/generate-favicon.ts +7 -6
  14. package/build/generate-fonts.ts +8 -15
  15. package/build/generate-katex-assets.ts +19 -17
  16. package/build/generate-web-app-manifest.ts +3 -11
  17. package/build/lodash-template.ts +14 -0
  18. package/build/output-publication.ts +238 -0
  19. package/build/pagefind.ts +49 -21
  20. package/build/pdf-text.ts +41 -1
  21. package/build/pipeline.ts +95 -101
  22. package/build/site-assets.ts +46 -0
  23. package/build/site-build.ts +267 -0
  24. package/build/source-model.ts +223 -229
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +12 -8
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +38 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +51 -117
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/jdi-runner/LiterateRunner.java +9 -1
  34. package/build/utils/literate-java.ts +11 -7
  35. package/build/utils/markdown-partials.ts +4 -3
  36. package/build/utils/markdown.ts +6 -17
  37. package/build/utils/paths.ts +3 -0
  38. package/build/utils/plain-text.ts +18 -0
  39. package/build/utils/render.ts +197 -154
  40. package/build/utils/shiki-highlighter.ts +11 -0
  41. package/build/utils/source-template.ts +2 -2
  42. package/build/utils/trace-core.ts +8 -83
  43. package/build/utils/trace.ts +38 -95
  44. package/build/validate-config-links.ts +39 -6
  45. package/build/watch/build-incremental.ts +46 -103
  46. package/build/watch/compiler.ts +158 -37
  47. package/build/watch/engine.ts +207 -162
  48. package/build/watch/index.ts +9 -2
  49. package/build/watch/planner.ts +88 -144
  50. package/build/watch/runtime.ts +21 -34
  51. package/build/watch/types.d.ts +32 -64
  52. package/package.json +4 -5
  53. package/schema/site.schema.json +18 -2
  54. package/src/_content.scss +447 -0
  55. package/src/_fonts.scss +90 -0
  56. package/src/_layout.scss +50 -35
  57. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  58. package/src/code/{style.scss → _index.scss} +61 -0
  59. package/src/header/{_base.scss → _index.scss} +98 -6
  60. package/src/index.ts +1 -17
  61. package/src/navigate/{style.scss → _index.scss} +20 -0
  62. package/src/navigate/eligible.ts +0 -40
  63. package/src/navigate/index.ts +50 -5
  64. package/src/navigate/lifecycle.ts +19 -2
  65. package/src/navigate/runtime.ts +20 -4
  66. package/src/page-update/index.ts +33 -20
  67. package/src/print/{style.scss → _index.scss} +3 -1
  68. package/src/search/index.ts +30 -2
  69. package/src/slides/{style.scss → _index.scss} +5 -0
  70. package/src/style.scss +24 -536
  71. package/src/timezone/index.ts +29 -3
  72. package/src/timezone/time-format.ts +4 -0
  73. package/src/toc/index.ts +6 -5
  74. package/src/trace/index.ts +11 -1
  75. package/templates/_author.html +2 -2
  76. package/templates/_download.html +1 -1
  77. package/templates/_footer.html +1 -1
  78. package/templates/_heading.html +3 -3
  79. package/templates/_nav.html +2 -2
  80. package/templates/_page-bottom.html +6 -0
  81. package/templates/_top.html +18 -14
  82. package/templates/code.html +22 -21
  83. package/templates/default.html +12 -11
  84. package/templates/literate.html +6 -5
  85. package/build/copy.ts +0 -82
  86. package/build/generate-content-assets.ts +0 -121
  87. package/build/utils/content-files.ts +0 -170
  88. package/build/watch/assets.ts +0 -103
  89. package/build/watch/build-full.ts +0 -97
  90. package/build/watch/build-helpers.ts +0 -66
  91. package/build/watch/build-result.ts +0 -19
  92. package/build/watch/fs-commit.ts +0 -153
  93. package/build/watch/mutations.ts +0 -73
  94. package/build/watch/snapshot.ts +0 -201
  95. package/build/watch/validation.ts +0 -50
  96. package/src/code.scss +0 -60
  97. package/src/critical.scss +0 -5
  98. package/src/header/style.scss +0 -91
  99. /package/src/{literate.scss → _literate.scss} +0 -0
  100. /package/src/{material-symbols.scss → _material-symbols.scss} +0 -0
  101. /package/src/anchor/{style.scss → _index.scss} +0 -0
  102. /package/src/page-update/{style.scss → _index.scss} +0 -0
  103. /package/src/question/{style.scss → _index.scss} +0 -0
  104. /package/src/search/{style.scss → _index.scss} +0 -0
  105. /package/src/timezone/{style.scss → _index.scss} +0 -0
  106. /package/src/toc/{style.scss → _index.scss} +0 -0
  107. /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';
@@ -68,6 +68,26 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
68
68
  mix-blend-mode: normal;
69
69
  }
70
70
 
71
+ // The footer and appearance controls get their own stationary layer. Show only
72
+ // the incoming snapshot so disabled defaults and stale hover states cannot blend.
73
+ .page-bottom {
74
+ view-transition-name: page-bottom;
75
+ }
76
+
77
+ ::view-transition-group(page-bottom),
78
+ ::view-transition-old(page-bottom),
79
+ ::view-transition-new(page-bottom) {
80
+ animation: none;
81
+ }
82
+
83
+ ::view-transition-old(page-bottom) {
84
+ display: none;
85
+ }
86
+
87
+ ::view-transition-new(page-bottom) {
88
+ mix-blend-mode: normal;
89
+ }
90
+
71
91
  // Forward navigation: old content slides left, new content slides in from right
72
92
  .nav-forward::view-transition-old(root) {
73
93
  animation:
@@ -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
  }
@@ -24,11 +24,17 @@ function shouldIgnoreClick(
24
24
  event: MouseEvent,
25
25
  anchor: HTMLAnchorElement,
26
26
  ): boolean {
27
- if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
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) {
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
@@ -68,7 +75,7 @@ export default function mountNavigate(window: Window): () => void {
68
75
  return;
69
76
  }
70
77
 
71
- // Same-page hash links: handle locally so we can clear search and
78
+ // Same-page links: handle locally so we can clear search and
72
79
  // close the header without a full SPA fetch.
73
80
  const url = new URL(anchor.href);
74
81
  if (
@@ -84,10 +91,29 @@ export default function mountNavigate(window: Window): () => void {
84
91
  } else {
85
92
  globals.setLocationHash(window, url.hash);
86
93
  }
94
+ } else {
95
+ saveScrollPosition(window);
96
+ if (window.location.hash) {
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;
102
+ globals.setLocationHash(window, '');
103
+ // Native fragment clearing leaves a trailing #. Keep the authored
104
+ // URL and the current SPA history state on the new entry.
105
+ window.history.replaceState(state, '', url.href);
106
+ }
107
+ window.scrollTo({ top: 0 });
108
+ saveScrollPosition(window);
87
109
  }
88
110
  return;
89
111
  }
90
112
 
113
+ if (!anchor.hasAttribute('data-tada-page')) {
114
+ return;
115
+ }
116
+
91
117
  event.preventDefault();
92
118
 
93
119
  // Clear search and close header immediately, before the fetch
@@ -108,11 +134,30 @@ export default function mountNavigate(window: Window): () => void {
108
134
  }
109
135
 
110
136
  function handlePopState(event: PopStateEvent) {
137
+ if (clearingFragment) {
138
+ clearingFragment = false;
139
+ if (!window.location.hash) {
140
+ return;
141
+ }
142
+ }
143
+
111
144
  const newPath = window.location.pathname + window.location.search;
112
145
  if (newPath === getCurrentPath()) {
113
- const savedY = getSavedLocationScroll(newPath + window.location.hash);
146
+ const locationKey = newPath + window.location.hash;
147
+ const savedY = getSavedLocationScroll(locationKey);
114
148
  if (typeof savedY === 'number') {
115
- window.scrollTo({ top: savedY });
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);
116
161
  return;
117
162
  }
118
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
- cleanups = [];
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
  }
@@ -1,4 +1,9 @@
1
- import { mountPerPageComponents, teardownPerPageComponents } from './lifecycle';
1
+ import { getResponseValidators, type ResponseValidators } from '../validators';
2
+ import {
3
+ mountAppearancePickerForPage,
4
+ mountPerPageComponents,
5
+ teardownPerPageComponents,
6
+ } from './lifecycle';
2
7
  import { globals } from '../globals';
3
8
 
4
9
  export const NAVIGATION_EVENT = 'tada:navigation';
@@ -93,12 +98,22 @@ export function closeHeaderDetails(document: Document): void {
93
98
  }
94
99
  }
95
100
 
96
- function dispatchNavigationEvent(window: Window): void {
101
+ export interface NavigationEventDetail {
102
+ path: string;
103
+ validators: ResponseValidators;
104
+ }
105
+
106
+ function dispatchNavigationEvent(
107
+ window: Window,
108
+ validators: ResponseValidators,
109
+ ): void {
97
110
  const CustomEventCtor = (
98
111
  window as unknown as { CustomEvent: typeof CustomEvent }
99
112
  ).CustomEvent;
100
113
  window.dispatchEvent(
101
- new CustomEventCtor(NAVIGATION_EVENT, { detail: { path: currentPath } }),
114
+ new CustomEventCtor(NAVIGATION_EVENT, {
115
+ detail: { path: currentPath, validators },
116
+ }),
102
117
  );
103
118
  }
104
119
 
@@ -288,6 +303,7 @@ export async function navigateToUrl(
288
303
  teardownPerPageComponents();
289
304
  swapContent(document, newDoc);
290
305
  updateHead(document, newDoc);
306
+ mountAppearancePickerForPage(window);
291
307
  currentPath = parsed.pathname + parsed.search;
292
308
 
293
309
  if (pushHistory) {
@@ -396,7 +412,7 @@ export async function navigateToUrl(
396
412
  }
397
413
 
398
414
  currentAbortController = null;
399
- dispatchNavigationEvent(window);
415
+ dispatchNavigationEvent(window, getResponseValidators(response));
400
416
  }
401
417
 
402
418
  export async function refreshCurrentPage(window: Window): Promise<void> {
@@ -72,7 +72,9 @@ export function mountPageUpdate(
72
72
  const { container, toast, reloadButton, dismissButton } =
73
73
  createToast(document);
74
74
 
75
- let currentPath = window.location.pathname;
75
+ let currentPath = window.location.pathname + window.location.search;
76
+ let navigationGeneration = 0;
77
+ let disposed = false;
76
78
  let baseline = EMPTY_RESPONSE_VALIDATORS;
77
79
  let dismissedValidatorKey: string | null = null;
78
80
  let nextValidator: ResponseValidators | null = null;
@@ -98,7 +100,7 @@ export function mountPageUpdate(
98
100
 
99
101
  function scheduleNextCheck() {
100
102
  clearTimer();
101
- if (globals.isDocumentHidden(document)) {
103
+ if (disposed || globals.isDocumentHidden(document)) {
102
104
  return;
103
105
  }
104
106
  timer = window.setTimeout(() => {
@@ -145,11 +147,16 @@ export function mountPageUpdate(
145
147
  }
146
148
 
147
149
  const requestedPath = currentPath;
148
- let shouldRerun = false;
150
+ const requestedGeneration = navigationGeneration;
149
151
  checkInFlight = true;
150
152
  try {
151
153
  const validators = await fetchValidators(requestedPath);
152
- if (!validators || requestedPath !== currentPath) {
154
+ if (
155
+ !validators ||
156
+ disposed ||
157
+ requestedGeneration !== navigationGeneration ||
158
+ requestedPath !== currentPath
159
+ ) {
153
160
  return;
154
161
  }
155
162
 
@@ -175,30 +182,32 @@ export function mountPageUpdate(
175
182
  // best-effort
176
183
  } finally {
177
184
  checkInFlight = false;
178
- if (rerunAfterCurrentCheck && !globals.isDocumentHidden(document)) {
185
+ if (
186
+ rerunAfterCurrentCheck &&
187
+ !disposed &&
188
+ !globals.isDocumentHidden(document)
189
+ ) {
179
190
  rerunAfterCurrentCheck = false;
180
- shouldRerun = true;
181
- } else {
191
+ void checkForUpdate();
192
+ } else if (requestedGeneration === navigationGeneration) {
182
193
  scheduleNextCheck();
183
194
  }
184
195
  }
185
-
186
- if (shouldRerun) {
187
- void checkForUpdate();
188
- }
189
196
  }
190
197
 
191
- function resetState() {
192
- currentPath = window.location.pathname;
193
- baseline = EMPTY_RESPONSE_VALIDATORS;
198
+ function resetState(
199
+ validators: ResponseValidators = EMPTY_RESPONSE_VALIDATORS,
200
+ ) {
201
+ navigationGeneration++;
202
+ rerunAfterCurrentCheck = false;
203
+ currentPath = window.location.pathname + window.location.search;
204
+ baseline = validators;
194
205
  dismissedValidatorKey = null;
195
206
  nextValidator = null;
196
- hasBaseline = false;
207
+ hasBaseline = hasUsableResponseValidators(validators);
197
208
  hideToast();
198
209
  clearTimer();
199
- if (!globals.isDocumentHidden(document)) {
200
- void checkForUpdate();
201
- }
210
+ scheduleNextCheck();
202
211
  }
203
212
 
204
213
  function handleVisibilityChange() {
@@ -216,13 +225,16 @@ export function mountPageUpdate(
216
225
  void checkForUpdate();
217
226
  }
218
227
 
219
- function handleNavigation() {
228
+ function handleNavigation(event: Event) {
220
229
  if (__IS_DEV__) {
221
230
  console.log(
222
231
  `[page-update] navigation path=${window.location.pathname} visibility=${document.visibilityState} focus=${document.hasFocus()}`,
223
232
  );
224
233
  }
225
- resetState();
234
+ resetState(
235
+ (event as CustomEvent<{ validators?: ResponseValidators }>).detail
236
+ ?.validators,
237
+ );
226
238
  }
227
239
 
228
240
  function handleDismiss() {
@@ -272,6 +284,7 @@ export function mountPageUpdate(
272
284
  }
273
285
 
274
286
  return () => {
287
+ disposed = true;
275
288
  clearTimer();
276
289
  document.removeEventListener('visibilitychange', handleVisibilityChange);
277
290
  window.removeEventListener('focus', handleWindowFocus);
@@ -17,11 +17,13 @@
17
17
  }
18
18
 
19
19
  header,
20
- footer {
20
+ footer,
21
+ .page-bottom {
21
22
  display: none !important;
22
23
  }
23
24
 
24
25
  body > .container {
26
+ min-height: 0;
25
27
  margin-top: 0;
26
28
  }
27
29
 
@@ -141,6 +141,10 @@ function render(
141
141
  a.id = `result-${i}`;
142
142
  a.className = 'result';
143
143
  a.href = result.url;
144
+ const isGeneratedPage = Boolean(result.template);
145
+ if (isGeneratedPage) {
146
+ a.setAttribute('data-tada-page', '');
147
+ }
144
148
  a.tabIndex = 0;
145
149
 
146
150
  const titleEl = doc.createElement('div');
@@ -177,6 +181,9 @@ function render(
177
181
  for (const sub of subsToShow) {
178
182
  const subA = doc.createElement('a');
179
183
  subA.href = sub.url;
184
+ if (isGeneratedPage) {
185
+ subA.setAttribute('data-tada-page', '');
186
+ }
180
187
  subA.className = 'sub-result';
181
188
 
182
189
  const subTitle = doc.createElement('div');
@@ -281,6 +288,7 @@ export default (window: Window) => {
281
288
 
282
289
  let pagefindLoadPromise: Promise<boolean> | null = null;
283
290
  let pagefindLoadGeneration = 0;
291
+ let disposed = false;
284
292
 
285
293
  function pagefindModulePath(generation: number) {
286
294
  const path = applyBasePath('/pagefind/pagefind.js');
@@ -291,6 +299,9 @@ export default (window: Window) => {
291
299
  }
292
300
 
293
301
  async function loadPagefind(): Promise<boolean> {
302
+ if (disposed) {
303
+ return false;
304
+ }
294
305
  if (pagefind) {
295
306
  return true;
296
307
  }
@@ -317,7 +328,18 @@ export default (window: Window) => {
317
328
  }
318
329
  pagefind = loadedPagefind;
319
330
  return true;
320
- })();
331
+ })().catch(err => {
332
+ if (generation === pagefindLoadGeneration) {
333
+ // Failed dynamic imports are cached by URL. Retry only on a later
334
+ // interaction, with a fresh URL, sharing one pending load.
335
+ pagefindLoadGeneration += 1;
336
+ console.log(`failed to load Pagefind: ${err}`);
337
+ if (state.showResults) {
338
+ render(input!, resultsContainer, state, false);
339
+ }
340
+ }
341
+ return false;
342
+ });
321
343
 
322
344
  const promise = pagefindLoadPromise;
323
345
  try {
@@ -343,6 +365,9 @@ export default (window: Window) => {
343
365
  render(input!, resultsContainer, state, true);
344
366
  }
345
367
  const query = state.value;
368
+ if (!(await loadPagefind()) || updateId !== latestUpdateId) {
369
+ return;
370
+ }
346
371
  const nextState = await doSearch(query, window);
347
372
  if (updateId !== latestUpdateId) {
348
373
  return;
@@ -370,7 +395,7 @@ export default (window: Window) => {
370
395
  function loadPagefindAndUpdate() {
371
396
  loadPagefind()
372
397
  .then(loadedCurrentPagefind => {
373
- if (loadedCurrentPagefind && state.showResults) {
398
+ if (loadedCurrentPagefind && !disposed && state.showResults) {
374
399
  queueUpdate();
375
400
  }
376
401
  })
@@ -601,6 +626,9 @@ export default (window: Window) => {
601
626
  input.disabled = false;
602
627
 
603
628
  return () => {
629
+ disposed = true;
630
+ pagefindLoadGeneration += 1;
631
+ invalidateUpdates();
604
632
  window.removeEventListener(
605
633
  PAGE_UPDATE_REFRESH_EVENT,
606
634
  handlePageUpdateRefresh,