@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.
Files changed (111) hide show
  1. package/README.md +38 -7
  2. package/assets/material-symbols/chevron_right_24px.svg +1 -0
  3. package/bin/tada.ts +16 -7
  4. package/bin/validators.ts +33 -0
  5. package/build/branding.ts +41 -0
  6. package/build/build-manifest.ts +9 -4
  7. package/build/{watch/compiler-types.d.ts → build-types.d.ts} +8 -5
  8. package/build/build-validation.ts +176 -0
  9. package/build/bundle.ts +75 -65
  10. package/build/config-loader.ts +3 -3
  11. package/build/custom-fonts.ts +8 -5
  12. package/build/generate-favicon.ts +7 -6
  13. package/build/generate-fonts.ts +8 -15
  14. package/build/generate-katex-assets.ts +19 -17
  15. package/build/generate-web-app-manifest.ts +3 -11
  16. package/build/lodash-template.ts +14 -0
  17. package/build/material-symbols.ts +7 -4
  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 -244
  25. package/build/source-records.ts +69 -81
  26. package/build/template-globals.ts +3 -0
  27. package/build/templates.ts +5 -2
  28. package/build/toc-plugin.ts +21 -21
  29. package/build/types.d.ts +44 -38
  30. package/build/util.ts +0 -4
  31. package/build/utils/code.ts +38 -101
  32. package/build/utils/final-html.ts +171 -47
  33. package/build/utils/literate-java.ts +11 -7
  34. package/build/utils/markdown-partials.ts +4 -3
  35. package/build/utils/markdown.ts +6 -17
  36. package/build/utils/paths.ts +3 -0
  37. package/build/utils/plain-text.ts +18 -0
  38. package/build/utils/render.ts +227 -165
  39. package/build/utils/shiki-highlighter.ts +11 -0
  40. package/build/utils/source-template.ts +2 -2
  41. package/build/utils/trace-core.ts +8 -83
  42. package/build/utils/trace.ts +38 -95
  43. package/build/validate-config-links.ts +60 -20
  44. package/build/watch/build-incremental.ts +46 -103
  45. package/build/watch/compiler.ts +158 -37
  46. package/build/watch/engine.ts +207 -176
  47. package/build/watch/index.ts +9 -2
  48. package/build/watch/planner.ts +88 -144
  49. package/build/watch/runtime.ts +21 -34
  50. package/build/watch/types.d.ts +32 -64
  51. package/init/content/labs/00/VowelCounter.java.md +5 -2
  52. package/init/content/labs/00/index.md +3 -2
  53. package/init/content/labs/01/index.md +3 -2
  54. package/init/content/labs/02/index.md +3 -2
  55. package/init/content/lectures/01/index.md +3 -2
  56. package/init/content/lectures/02/index.md +3 -2
  57. package/package.json +4 -4
  58. package/schema/site.schema.json +18 -2
  59. package/src/_content.scss +463 -0
  60. package/src/_fonts.scss +90 -0
  61. package/src/_layout.scss +50 -35
  62. package/src/{material-symbols.scss → _material-symbols.scss} +7 -0
  63. package/src/appearance-picker/{style.scss → _index.scss} +1 -1
  64. package/src/code/{style.scss → _index.scss} +61 -0
  65. package/src/header/{_base.scss → _index.scss} +98 -6
  66. package/src/index.ts +1 -17
  67. package/src/navigate/{style.scss → _index.scss} +29 -3
  68. package/src/navigate/breadcrumbs.ts +129 -0
  69. package/src/navigate/eligible.ts +0 -40
  70. package/src/navigate/index.ts +38 -4
  71. package/src/navigate/lifecycle.ts +19 -2
  72. package/src/navigate/runtime.ts +57 -22
  73. package/src/page-update/index.ts +33 -20
  74. package/src/print/{style.scss → _index.scss} +3 -1
  75. package/src/search/index.ts +7 -0
  76. package/src/slides/{style.scss → _index.scss} +5 -0
  77. package/src/style.scss +24 -536
  78. package/src/toc/index.ts +2 -2
  79. package/templates/_author.html +2 -2
  80. package/templates/_download.html +1 -1
  81. package/templates/_footer.html +1 -1
  82. package/templates/_heading.html +17 -4
  83. package/templates/_nav.html +2 -2
  84. package/templates/_page-bottom.html +6 -0
  85. package/templates/_top.html +18 -14
  86. package/templates/code.html +22 -21
  87. package/templates/default.html +12 -11
  88. package/templates/literate.html +6 -5
  89. package/assets/material-symbols/south_east_20px.svg +0 -1
  90. package/build/copy.ts +0 -82
  91. package/build/generate-content-assets.ts +0 -121
  92. package/build/utils/content-files.ts +0 -170
  93. package/build/watch/assets.ts +0 -103
  94. package/build/watch/build-full.ts +0 -97
  95. package/build/watch/build-helpers.ts +0 -66
  96. package/build/watch/build-result.ts +0 -19
  97. package/build/watch/fs-commit.ts +0 -218
  98. package/build/watch/mutations.ts +0 -73
  99. package/build/watch/snapshot.ts +0 -201
  100. package/build/watch/validation.ts +0 -57
  101. package/src/code.scss +0 -60
  102. package/src/critical.scss +0 -5
  103. package/src/header/style.scss +0 -91
  104. /package/src/{literate.scss → _literate.scss} +0 -0
  105. /package/src/anchor/{style.scss → _index.scss} +0 -0
  106. /package/src/page-update/{style.scss → _index.scss} +0 -0
  107. /package/src/question/{style.scss → _index.scss} +0 -0
  108. /package/src/search/{style.scss → _index.scss} +0 -0
  109. /package/src/timezone/{style.scss → _index.scss} +0 -0
  110. /package/src/toc/{style.scss → _index.scss} +0 -0
  111. /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
+ }
@@ -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 || 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 savedY = getSavedLocationScroll(newPath + window.location.hash);
146
+ const locationKey = newPath + window.location.hash;
147
+ const savedY = getSavedLocationScroll(locationKey);
125
148
  if (typeof savedY === 'number') {
126
- 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);
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
- 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
  }