@abreen/tada 1.11.3 → 1.13.4

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 (85) hide show
  1. package/README.md +65 -64
  2. package/bin/tada.ts +61 -66
  3. package/build/build-manifest.ts +14 -3
  4. package/build/bundle.ts +0 -1
  5. package/build/config-files.ts +183 -0
  6. package/build/config-loader.ts +159 -0
  7. package/build/copy.ts +0 -83
  8. package/build/generate-content-assets.ts +75 -160
  9. package/build/globals.ts +48 -89
  10. package/build/log.ts +27 -18
  11. package/build/pagefind.ts +33 -26
  12. package/build/pipeline.ts +98 -79
  13. package/build/reachability.ts +51 -248
  14. package/build/serve.ts +35 -6
  15. package/build/site-variables.ts +20 -11
  16. package/build/source-model.ts +400 -0
  17. package/build/{watch/records.ts → source-records.ts} +111 -49
  18. package/build/template-globals.ts +91 -0
  19. package/build/templates.ts +50 -40
  20. package/build/test-helpers.ts +15 -0
  21. package/build/toc-plugin.ts +5 -1
  22. package/build/types.d.ts +9 -1
  23. package/build/util.ts +8 -0
  24. package/build/utils/code.ts +6 -16
  25. package/build/utils/content-files.ts +57 -126
  26. package/build/utils/final-html.ts +239 -0
  27. package/build/utils/markdown.ts +237 -17
  28. package/build/utils/paths.ts +2 -1
  29. package/build/utils/render.ts +108 -99
  30. package/build/utils/trace-core.ts +3 -7
  31. package/build/validate-config-links.ts +91 -14
  32. package/build/watch/assets.ts +0 -2
  33. package/build/watch/build-full.ts +14 -15
  34. package/build/watch/build-incremental.ts +15 -8
  35. package/build/watch/compiler-types.d.ts +6 -1
  36. package/build/watch/compiler.ts +27 -15
  37. package/build/watch/index.ts +2 -7
  38. package/build/watch/planner.ts +32 -16
  39. package/build/watch/reload.ts +4 -0
  40. package/build/watch/runtime.ts +29 -45
  41. package/build/watch/snapshot.ts +50 -402
  42. package/build/watch/validation.ts +6 -3
  43. package/build/watch-reachability-state.ts +16 -23
  44. package/build/watch-reload-client.ts +11 -4
  45. package/init/content/index.md +1 -1
  46. package/init/content/labs/02/index.md +61 -0
  47. package/init/content/labs/02/selection_sort.py +23 -0
  48. package/init/content/labs/index.md +4 -2
  49. package/init/content/lectures/01/index.md +1 -1
  50. package/init/content/markdown.md +1 -1
  51. package/package.json +3 -4
  52. package/src/_base.scss +67 -0
  53. package/src/anchor/README.md +9 -0
  54. package/src/anchor/index.ts +115 -16
  55. package/src/anchor/style.scss +70 -10
  56. package/src/code/index.ts +46 -18
  57. package/src/code.scss +2 -1
  58. package/src/globals.ts +47 -0
  59. package/src/index.ts +1 -0
  60. package/src/navigate/index.ts +2 -5
  61. package/src/navigate/lifecycle.ts +3 -1
  62. package/src/navigate/runtime.ts +6 -5
  63. package/src/page-update/index.ts +20 -16
  64. package/src/print/style.scss +0 -4
  65. package/src/search/index.ts +53 -25
  66. package/src/slides/index.ts +481 -0
  67. package/src/slides/style.scss +104 -0
  68. package/src/style.scss +8 -3
  69. package/src/timezone/index.ts +16 -20
  70. package/src/top/index.ts +2 -4
  71. package/src/trace/index.ts +116 -14
  72. package/src/trace/style.scss +1 -1
  73. package/src/util.ts +6 -2
  74. package/templates/_author.html +2 -2
  75. package/templates/_heading.html +1 -1
  76. package/templates/_nav.html +2 -2
  77. package/templates/_theme.scss +4 -3
  78. package/templates/_top.html +14 -14
  79. package/templates/default.html +9 -0
  80. package/types/dev.d.ts +1 -1
  81. package/types/site-variables.d.ts +4 -5
  82. package/watch/fs-commit.ts +63 -10
  83. package/build/apply-base-path-plugin.ts +0 -186
  84. package/build/content-watch.ts +0 -104
  85. package/build/validate-internal-links-plugin.ts +0 -207
@@ -0,0 +1,61 @@
1
+ ---
2
+ parent: ../index.html
3
+ parentLabel: Labs
4
+ title: Lab 2
5
+ author: alex
6
+ description: A page demonstrating slides mode.
7
+ slides: true
8
+ ---
9
+
10
+ ## Selection sort
11
+
12
+ Selection sort is a well-known comparison-based sorting algorithm.
13
+
14
+ - Divides the array into a sorted left part and an unsorted right part
15
+ - Repeatedly scans left-to-right, looking for the smallest element
16
+ - Swaps the smallest in the right part into correct position in the left part
17
+
18
+ ---
19
+
20
+ <%= renderTrace('selection_sort.py') %>
21
+
22
+ ---
23
+
24
+ ## Impressions
25
+
26
+ Selection sort's strength is its simplicity.
27
+
28
+ - `index_smallest()` always scans from `start` to the end
29
+ - It requires no additional memory (other than local variables)
30
+
31
+ However
32
+
33
+ - It has no `break` statements & can't return early
34
+ - No difference between best, average and worst cases
35
+
36
+ ---
37
+
38
+ ## Performance
39
+
40
+ - The outer loop always performs $n - 1$ iterations
41
+ - There are always $n - 1$ swaps
42
+ - At each value of `i`, `index_smallest()` iterates from `i + 1` to the end
43
+
44
+ ---
45
+
46
+ ## Comparisons
47
+
48
+ The innermost statement compares `nums[i]` and `nums[curr]`.
49
+
50
+ - when `i` is 0, the comparison is executed $n - 1$ times
51
+ - when `i` is 1, the innermost statement is executed $n - 2$ times
52
+ - $\dots$
53
+ - when `i` is $n - 3$, the innermost statement is executed two times
54
+ - when `i` is $n - 2$, the innermost statement is executed one time
55
+
56
+ Therefore the total number of comparisons is
57
+
58
+ $$
59
+ (n-1) + (n-2) + \dots + 1 = \sum_{i=1}^{n-1}i
60
+ $$
61
+
@@ -0,0 +1,23 @@
1
+ def swap(items, i, j):
2
+ temp = items[i]
3
+ items[i] = items[j]
4
+ items[j] = temp
5
+
6
+
7
+ def index_smallest(nums, start):
8
+ curr = start
9
+ for i in range(start + 1, len(nums)):
10
+ if nums[i] < nums[curr]:
11
+ curr = i
12
+
13
+ return curr
14
+
15
+
16
+ def selection(nums):
17
+ for i in range(len(nums) - 1):
18
+ swap(nums, i, index_smallest(nums, i))
19
+
20
+
21
+ input1 = [25, 31, 12, 9]
22
+ selection(input1)
23
+ print(input1)
@@ -8,6 +8,8 @@ In each one-hour lab, we'll dive deeper and write code together, exploring how
8
8
  to apply the concepts we learned in lecture to solve real-world problems and
9
9
  write full-length programs.
10
10
 
11
- 0. [`VowelCounter` and user input](/labs/00/index.html)
11
+ 0. [`VowelCounter` and user input](00/index.html)
12
12
 
13
- 1. [Memory diagrams](/labs/01/index.html)
13
+ 1. [Memory diagrams](01/index.html)
14
+
15
+ 2. [Slides mode demo](02/index.html)
@@ -10,7 +10,7 @@ Here is a sample description of the topics covered at the first lecture.
10
10
 
11
11
  The value of `vars.foobar` is <%= vars.foobar %>.
12
12
 
13
- You can add any variables you want to the `site.dev.json` and `site.prod.json`
13
+ You can add any variables you want to the `site.dev.yaml` and `site.prod.yaml`
14
14
  files under the `"vars"` property and access them using Lodash template syntax
15
15
  from Markdown files, HTML files, and source code. (Lodash template subtitution
16
16
  only works in source code files whose extension is listed in
@@ -126,7 +126,7 @@ The syntax for links is unchanged from Presto. However,
126
126
  - external links (links whose domain is not one of `site.internalDomains`) are
127
127
  automatically given `target="_blank"` and special styling
128
128
 
129
- See `config/site.dev.json` and `config/site.prod.json` to change `site.basePath`
129
+ See `site.dev.yaml` and `site.prod.yaml` to change `site.basePath`
130
130
  and `site.internalDomains`.
131
131
 
132
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.11.3",
3
+ "version": "1.13.4",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -38,6 +38,7 @@
38
38
  "lint:sass": "bun run scripts/lint-sass.ts",
39
39
  "typecheck": "tsc --noEmit",
40
40
  "format": "prettier --write \"**/*.{ts,js,scss,json}\"",
41
+ "test:unit": "bun run scripts/test.ts unit",
41
42
  "test:playwright": "bun run scripts/test.ts playwright",
42
43
  "test:functional": "bun run scripts/test.ts functional",
43
44
  "test:all": "bun run scripts/test.ts all",
@@ -79,8 +80,7 @@
79
80
  "sass": "^1.99.0",
80
81
  "sharp": "^0.34.5",
81
82
  "shiki": "^4.0.2",
82
- "string-strip-html": "^8.5.0",
83
- "ws": "^8.20.0"
83
+ "string-strip-html": "^8.5.0"
84
84
  },
85
85
  "lint-staged": {
86
86
  "*.{ts,js}": "prettier --write",
@@ -110,7 +110,6 @@
110
110
  "@types/istanbul-reports": "^3.0.4",
111
111
  "@types/lodash": "^4.17.24",
112
112
  "@types/markdown-it-container": "^4.0.0",
113
- "@types/ws": "^8.18.1",
114
113
  "eslint": "^10.2.1",
115
114
  "husky": "^9.1.7",
116
115
  "istanbul-lib-coverage": "^3.2.2",
package/src/_base.scss CHANGED
@@ -180,6 +180,73 @@ input[type='search'] {
180
180
  }
181
181
  }
182
182
 
183
+ label:has(> input[type='checkbox']) {
184
+ display: inline-flex;
185
+ gap: 0.45em;
186
+ align-items: center;
187
+ font-size: var(--font-size-small);
188
+ font-weight: 500;
189
+
190
+ @include user-select(none);
191
+ }
192
+
193
+ input[type='checkbox'] {
194
+ display: inline-grid;
195
+ flex: 0 0 auto;
196
+ place-content: center;
197
+ width: 1.5em;
198
+ height: 1.25em;
199
+ padding: 0;
200
+ margin: 0;
201
+ font: inherit;
202
+ color: var(--fg-color);
203
+ appearance: none;
204
+ cursor: pointer;
205
+ background: var(--bg-color);
206
+ border: var(--control-border-width) solid var(--fg2-color);
207
+ border-radius: 0.25em;
208
+
209
+ &::after {
210
+ display: block;
211
+ width: 1.1em;
212
+ height: 1.1em;
213
+ content: '';
214
+ background-color: currentcolor;
215
+ opacity: 0;
216
+
217
+ @include contained-mask(var(--icon-check));
218
+ }
219
+
220
+ &:checked {
221
+ &::after {
222
+ opacity: 1;
223
+ }
224
+ }
225
+
226
+ &:disabled {
227
+ cursor: not-allowed;
228
+ opacity: 0.4;
229
+ }
230
+
231
+ &:active:not(:disabled) {
232
+ color: var(--bg-color) !important;
233
+ background: var(--link-active-color) !important;
234
+ }
235
+
236
+ &:focus {
237
+ outline: 2px solid var(--theme-color);
238
+ outline-offset: 1px;
239
+ background-color: var(--bg2-color);
240
+ border-color: var(--fg-color);
241
+ }
242
+ }
243
+
244
+ @media (hover: hover) {
245
+ input[type='checkbox']:hover:not(:disabled) {
246
+ background: var(--bg2-color);
247
+ }
248
+ }
249
+
183
250
  figure {
184
251
  margin: 1rem 0;
185
252
 
@@ -4,3 +4,12 @@ Append a link to each heading having an `id` attribute.
4
4
 
5
5
  When the link is clicked, `window.location` is updated and the heading is
6
6
  highlighted using the same function as the `toc` component.
7
+
8
+ The heading link includes a real inline SVG hash icon, revealed on heading
9
+ hover or link focus.
10
+
11
+ For slide title headings (`div.slide > h2:first-child`), append a
12
+ `Present from this slide` icon button. Clicking the button dispatches a
13
+ bubbling `tada:slides-present` custom event with the slide index so the slides
14
+ component can open presentation mode from that slide using the current
15
+ `Full screen` checkbox state.
@@ -2,32 +2,131 @@ function getElements(parent: HTMLElement): HTMLHeadingElement[] {
2
2
  return Array.from(parent.querySelectorAll('h1, h2, h3, h4, h5, h6'));
3
3
  }
4
4
 
5
+ function createIcon(window: Window, type: 'hash' | 'present'): SVGSVGElement {
6
+ const svg = window.document.createElementNS(
7
+ 'http://www.w3.org/2000/svg',
8
+ 'svg',
9
+ );
10
+ svg.setAttribute('aria-hidden', 'true');
11
+ svg.setAttribute('viewBox', '0 0 24 24');
12
+ svg.setAttribute('width', '16');
13
+ svg.setAttribute('height', '16');
14
+ svg.setAttribute('fill', 'none');
15
+ svg.setAttribute('stroke', 'currentColor');
16
+ svg.setAttribute('stroke-width', '1.5');
17
+ svg.setAttribute('stroke-linecap', 'round');
18
+ svg.setAttribute('stroke-linejoin', 'round');
19
+
20
+ const paths =
21
+ type === 'hash'
22
+ ? ['M10 3 8 21', 'M16 3l-2 18', 'M4 9h17', 'M3 15h17']
23
+ : [
24
+ 'M5 5h14a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z',
25
+ 'M9 20h6',
26
+ ];
27
+
28
+ for (const d of paths) {
29
+ const path = window.document.createElementNS(
30
+ 'http://www.w3.org/2000/svg',
31
+ 'path',
32
+ );
33
+ path.setAttribute('d', d);
34
+ svg.appendChild(path);
35
+ }
36
+
37
+ return svg;
38
+ }
39
+
40
+ function getSlideIndex(slide: HTMLElement): number {
41
+ const rawIndex = slide.getAttribute('data-slide-index');
42
+ if (rawIndex != null) {
43
+ const parsed = Number.parseInt(rawIndex, 10);
44
+ if (Number.isFinite(parsed)) {
45
+ return parsed;
46
+ }
47
+ }
48
+
49
+ const root = slide.closest('[data-slides-root]');
50
+ if (!root) {
51
+ return 0;
52
+ }
53
+
54
+ return Array.from(root.querySelectorAll('.slide')).indexOf(slide);
55
+ }
56
+
57
+ function isSlideTitleHeading(
58
+ heading: HTMLHeadingElement,
59
+ ): heading is HTMLHeadingElement & { parentElement: HTMLElement } {
60
+ const slide = heading.parentElement;
61
+ return (
62
+ heading.tagName === 'H2' &&
63
+ slide?.tagName === 'DIV' &&
64
+ slide.classList.contains('slide') &&
65
+ slide.firstElementChild === heading
66
+ );
67
+ }
68
+
5
69
  export default (window: Window) => {
70
+ const view = window as Window & typeof globalThis;
6
71
  const elements = getElements(window.document.body);
7
- if (elements == null) {
8
- return;
9
- }
72
+ const cleanups: (() => void)[] = [];
10
73
 
11
74
  elements.forEach(el => {
12
- if (!el.id) {
75
+ if (el.id) {
76
+ const link = window.document.createElement('a');
77
+ link.className = 'heading-anchor';
78
+ link.href = `#${el.id}`;
79
+ link.title = 'Link to this heading';
80
+
81
+ // Move all existing child nodes into the link.
82
+ while (el.firstChild) {
83
+ link.appendChild(el.firstChild);
84
+ }
85
+ link.appendChild(createIcon(window, 'hash'));
86
+ el.appendChild(link);
87
+
88
+ const handleClick = () => {
89
+ el.focus();
90
+ };
91
+ link.addEventListener('click', handleClick);
92
+ cleanups.push(() => {
93
+ link.removeEventListener('click', handleClick);
94
+ });
95
+ }
96
+
97
+ if (!isSlideTitleHeading(el)) {
13
98
  return;
14
99
  }
15
100
 
16
- const link = window.document.createElement('a');
17
- link.className = 'heading-anchor';
18
- link.href = `#${el.id}`;
19
- link.title = 'Link to this heading';
101
+ const button = window.document.createElement('button');
102
+ button.type = 'button';
103
+ button.className = 'heading-present-button';
104
+ button.setAttribute('aria-label', 'Present from this slide');
105
+ button.title = 'Present from this slide';
106
+ button.appendChild(createIcon(window, 'present'));
107
+ el.appendChild(button);
20
108
 
21
- // Move all existing child nodes into the link
22
- while (el.firstChild) {
23
- link.appendChild(el.firstChild);
24
- }
25
- el.appendChild(link);
109
+ const slide = el.parentElement;
110
+ const slideIndex = getSlideIndex(slide);
26
111
 
27
- link.addEventListener('click', () => {
28
- el.focus();
112
+ const handlePresentClick = () => {
113
+ button.dispatchEvent(
114
+ new view.CustomEvent('tada:slides-present', {
115
+ bubbles: true,
116
+ detail: { slideIndex },
117
+ }),
118
+ );
119
+ };
120
+ button.addEventListener('click', handlePresentClick);
121
+
122
+ cleanups.push(() => {
123
+ button.removeEventListener('click', handlePresentClick);
29
124
  });
30
125
  });
31
126
 
32
- return () => {};
127
+ return () => {
128
+ for (const cleanup of cleanups) {
129
+ cleanup();
130
+ }
131
+ };
33
132
  };
@@ -7,29 +7,89 @@ a.heading-anchor {
7
7
  @include subdued-link;
8
8
  }
9
9
 
10
- a.heading-anchor::after {
10
+ .heading-anchor svg,
11
+ .heading-present-button svg {
11
12
  display: inline-block;
12
13
  width: 1em;
13
14
  height: 1em;
15
+ vertical-align: middle;
16
+ }
17
+
18
+ .heading-anchor svg {
14
19
  margin-left: 4px;
15
20
  vertical-align: middle;
16
- content: '';
17
21
  opacity: 0;
22
+ }
18
23
 
19
- @include contained-mask(var(--icon-anchor));
24
+ .heading-present-button {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ width: 1em;
29
+ height: 1em;
30
+ padding: 0;
31
+ margin-left: 4px;
32
+ font: inherit;
33
+ line-height: 1;
34
+ vertical-align: middle;
35
+ color: var(--fg-color);
36
+ background: transparent;
37
+ border: none;
38
+ border-radius: 0;
39
+ opacity: 0;
20
40
  }
21
41
 
22
- a.heading-anchor:focus-visible::after {
42
+ a.heading-anchor:focus-visible svg,
43
+ .heading-present-button:focus {
23
44
  opacity: 1;
24
45
  }
25
46
 
47
+ .heading-present-button:focus {
48
+ outline-offset: 2px;
49
+ background-color: transparent;
50
+ border-color: transparent;
51
+ }
52
+
53
+ button.heading-present-button:active:not(:disabled) {
54
+ color: var(--link-active-color) !important;
55
+ background: transparent !important;
56
+ }
57
+
58
+ body.is-presenting .heading-present-button {
59
+ display: none;
60
+ }
61
+
26
62
  @media (hover: hover) {
27
- h1:hover a.heading-anchor::after,
28
- h2:hover a.heading-anchor::after,
29
- h3:hover a.heading-anchor::after,
30
- h4:hover a.heading-anchor::after,
31
- h5:hover a.heading-anchor::after,
32
- h6:hover a.heading-anchor::after {
63
+ button.heading-present-button:hover:not(:disabled) {
64
+ color: var(--fg2-color);
65
+ background: transparent;
66
+ }
67
+
68
+ h1:hover a.heading-anchor svg,
69
+ h2:hover a.heading-anchor svg,
70
+ h3:hover a.heading-anchor svg,
71
+ h4:hover a.heading-anchor svg,
72
+ h5:hover a.heading-anchor svg,
73
+ h6:hover a.heading-anchor svg,
74
+ h1:hover .heading-present-button,
75
+ h2:hover .heading-present-button,
76
+ h3:hover .heading-present-button,
77
+ h4:hover .heading-present-button,
78
+ h5:hover .heading-present-button,
79
+ h6:hover .heading-present-button {
33
80
  opacity: 1;
34
81
  }
35
82
  }
83
+
84
+ @media (hover: none) {
85
+ .heading-present-button {
86
+ display: none;
87
+ }
88
+ }
89
+
90
+ @media print {
91
+ .heading-anchor svg,
92
+ .heading-present-button {
93
+ display: none;
94
+ }
95
+ }
package/src/code/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { globals } from '../globals';
2
+
1
3
  export default async function mount(
2
4
  window: Window,
3
5
  ): Promise<void | (() => void)> {
@@ -6,7 +8,17 @@ export default async function mount(
6
8
  return;
7
9
  }
8
10
 
9
- document.addEventListener('copy', (e: ClipboardEvent) => {
11
+ const codeBodyEl = document.querySelector<HTMLElement>('.code-body');
12
+ const scrollbarEl = document.querySelector<HTMLElement>('.code-scrollbar');
13
+ if (!codeBodyEl || !scrollbarEl) {
14
+ return;
15
+ }
16
+ const codeBody = codeBodyEl;
17
+ const scrollbar = scrollbarEl;
18
+
19
+ const cleanups: Array<() => void> = [];
20
+
21
+ const handleCopy = (e: ClipboardEvent) => {
10
22
  const selection = window.getSelection();
11
23
  if (!selection || selection.rangeCount === 0) {
12
24
  return;
@@ -54,19 +66,23 @@ export default async function mount(
54
66
 
55
67
  e.clipboardData!.setData('text/plain', lines.join('\n'));
56
68
  e.preventDefault();
69
+ };
70
+ document.addEventListener('copy', handleCopy);
71
+ cleanups.push(() => {
72
+ document.removeEventListener('copy', handleCopy);
57
73
  });
58
74
 
59
75
  const downloadLink = document.querySelector<HTMLAnchorElement>(
60
76
  '.file-header a[download]',
61
77
  );
62
78
  if (downloadLink && 'showSaveFilePicker' in window) {
63
- downloadLink.addEventListener('click', async (e: MouseEvent) => {
79
+ const handleDownloadClick = async (e: MouseEvent) => {
64
80
  e.preventDefault();
65
81
  try {
66
82
  const handle = await window.showSaveFilePicker!({
67
83
  suggestedName: downloadLink.download,
68
84
  });
69
- const response = await fetch(downloadLink.href);
85
+ const response = await globals.fetch(downloadLink.href);
70
86
  const writable = await handle.createWritable();
71
87
  await writable.write(await response.blob());
72
88
  await writable.close();
@@ -75,47 +91,59 @@ export default async function mount(
75
91
  throw err;
76
92
  }
77
93
  }
94
+ };
95
+ downloadLink.addEventListener('click', handleDownloadClick);
96
+ cleanups.push(() => {
97
+ downloadLink.removeEventListener('click', handleDownloadClick);
78
98
  });
79
99
  }
80
100
 
81
- const codeBody = document.querySelector<HTMLElement>('.code-body');
82
- const scrollbar = document.querySelector<HTMLElement>('.code-scrollbar');
83
- if (!codeBody || !scrollbar) {
84
- return;
85
- }
86
-
87
101
  const inner = scrollbar.firstElementChild as HTMLElement;
88
102
  let syncing = false;
89
103
 
90
104
  function updateScrollbar(): void {
91
- const hasOverflow = codeBody!.scrollWidth > codeBody!.clientWidth;
92
- scrollbar!.style.display = hasOverflow ? '' : 'none';
93
- inner.style.width = codeBody!.scrollWidth + 'px';
105
+ const hasOverflow = codeBody.scrollWidth > codeBody.clientWidth;
106
+ scrollbar.style.display = hasOverflow ? '' : 'none';
107
+ inner.style.width = codeBody.scrollWidth + 'px';
94
108
  }
95
109
 
96
- codeBody.addEventListener('scroll', () => {
110
+ const handleCodeBodyScroll = () => {
97
111
  if (syncing) {
98
112
  return;
99
113
  }
100
114
  syncing = true;
101
- scrollbar!.scrollLeft = codeBody!.scrollLeft;
115
+ scrollbar.scrollLeft = codeBody.scrollLeft;
102
116
  syncing = false;
117
+ };
118
+ codeBody.addEventListener('scroll', handleCodeBodyScroll);
119
+ cleanups.push(() => {
120
+ codeBody.removeEventListener('scroll', handleCodeBodyScroll);
103
121
  });
104
122
 
105
- scrollbar.addEventListener('scroll', () => {
123
+ const handleScrollbarScroll = () => {
106
124
  if (syncing) {
107
125
  return;
108
126
  }
109
127
  syncing = true;
110
- codeBody!.scrollLeft = scrollbar!.scrollLeft;
128
+ codeBody.scrollLeft = scrollbar.scrollLeft;
111
129
  syncing = false;
130
+ };
131
+ scrollbar.addEventListener('scroll', handleScrollbarScroll);
132
+ cleanups.push(() => {
133
+ scrollbar.removeEventListener('scroll', handleScrollbarScroll);
112
134
  });
113
135
 
114
- const resizeObserver = new ResizeObserver(updateScrollbar);
136
+ const resizeObserver = globals.createResizeObserver(updateScrollbar);
115
137
  resizeObserver.observe(codeBody);
116
138
  updateScrollbar();
139
+ cleanups.push(() => {
140
+ resizeObserver.disconnect();
141
+ });
117
142
 
118
143
  return () => {
119
- resizeObserver.disconnect();
144
+ cleanups
145
+ .splice(0)
146
+ .reverse()
147
+ .forEach(cleanup => cleanup());
120
148
  };
121
149
  }
package/src/code.scss CHANGED
@@ -4,7 +4,8 @@ body.code {
4
4
  --code-gutter-width: 4rem;
5
5
  }
6
6
 
7
- .file-header {
7
+ .file-header,
8
+ .slides-header {
8
9
  display: flex;
9
10
  gap: var(--gap);
10
11
  align-items: center;
package/src/globals.ts ADDED
@@ -0,0 +1,47 @@
1
+ type FetchFunction = (
2
+ input: RequestInfo | URL,
3
+ init?: RequestInit,
4
+ ) => Promise<Response>;
5
+
6
+ type ImportModuleFunction = (specifier: string) => Promise<unknown>;
7
+ type LocationMutationFunction = (window: Window, value: string) => void;
8
+
9
+ export interface Globals {
10
+ replaceLocation: LocationMutationFunction;
11
+ createResizeObserver: (
12
+ callback: ResizeObserverCallback,
13
+ ) => Pick<ResizeObserver, 'disconnect' | 'observe'>;
14
+ fetch: FetchFunction;
15
+ importModule: ImportModuleFunction;
16
+ isDocumentHidden: (document: Document) => boolean;
17
+ now: () => number;
18
+ setLocationHash: LocationMutationFunction;
19
+ setLocationHref: LocationMutationFunction;
20
+ }
21
+
22
+ export const globals: Globals = {
23
+ replaceLocation(window, value) {
24
+ window.location.replace(value);
25
+ },
26
+ createResizeObserver(callback) {
27
+ return new ResizeObserver(callback);
28
+ },
29
+ fetch(input: RequestInfo | URL, init?: RequestInit) {
30
+ return fetch(input, init);
31
+ },
32
+ importModule(specifier) {
33
+ return import(specifier);
34
+ },
35
+ isDocumentHidden(document) {
36
+ return document.hidden;
37
+ },
38
+ now() {
39
+ return Date.now();
40
+ },
41
+ setLocationHash(window, value) {
42
+ window.location.hash = value;
43
+ },
44
+ setLocationHref(window, value) {
45
+ window.location.href = value;
46
+ },
47
+ };
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import './header/style.scss';
8
8
  import './print/style.scss';
9
9
  import './question/style.scss';
10
10
  import './search/style.scss';
11
+ import './slides/style.scss';
11
12
  import './timezone/style.scss';
12
13
  import './toc/style.scss';
13
14
  import './trace/style.scss';