@abreen/tada 1.0.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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +290 -0
  3. package/bin/tada.js +361 -0
  4. package/config/authors.json +1 -0
  5. package/config/nav.json +28 -0
  6. package/content/index.md +19 -0
  7. package/content/lectures/01/Pair.java.md +296 -0
  8. package/content/lectures/01/Rectangle.java +80 -0
  9. package/content/lectures/01/demo.py +9 -0
  10. package/content/lectures/01/index.md +39 -0
  11. package/content/lectures/01/lecture1.pdf +0 -0
  12. package/content/lectures/index.md +25 -0
  13. package/content/markdown.md +379 -0
  14. package/content/problem_sets/index.md +6 -0
  15. package/fonts/google-sans-code/GoogleSansCodeVariable-Italic.ttf +0 -0
  16. package/fonts/google-sans-code/GoogleSansCodeVariable.ttf +0 -0
  17. package/fonts/google-sans-code/LICENSE.txt +93 -0
  18. package/fonts/inter/InterVariable-Italic.ttf +0 -0
  19. package/fonts/inter/InterVariable.ttf +0 -0
  20. package/fonts/inter/LICENSE.txt +92 -0
  21. package/package.json +70 -0
  22. package/public/avatars/alex.jpg +0 -0
  23. package/public/test.txt +1 -0
  24. package/src/_mixins.scss +4 -0
  25. package/src/anchor/README.md +6 -0
  26. package/src/anchor/index.ts +34 -0
  27. package/src/anchor/style.scss +48 -0
  28. package/src/code/README.md +5 -0
  29. package/src/code/index.ts +113 -0
  30. package/src/code/style.scss +101 -0
  31. package/src/code.scss +54 -0
  32. package/src/header/README.md +8 -0
  33. package/src/header/index.ts +43 -0
  34. package/src/header/style.scss +228 -0
  35. package/src/index.ts +73 -0
  36. package/src/layout.scss +144 -0
  37. package/src/literate/style.scss +60 -0
  38. package/src/print/README.md +4 -0
  39. package/src/print/index.ts +32 -0
  40. package/src/print/style.scss +82 -0
  41. package/src/question/README.md +3 -0
  42. package/src/question/index.ts +25 -0
  43. package/src/question/style.scss +116 -0
  44. package/src/search/README.md +6 -0
  45. package/src/search/index.ts +574 -0
  46. package/src/search/style.scss +217 -0
  47. package/src/style.scss +815 -0
  48. package/src/timezone/index.test.ts +100 -0
  49. package/src/timezone/index.ts +298 -0
  50. package/src/timezone/style.scss +16 -0
  51. package/src/timezone/timezones.json +58 -0
  52. package/src/toc/README.md +3 -0
  53. package/src/toc/index.ts +322 -0
  54. package/src/toc/style.scss +203 -0
  55. package/src/top/README.md +4 -0
  56. package/src/top/index.ts +75 -0
  57. package/src/util.ts +122 -0
  58. package/templates/_author.html +27 -0
  59. package/templates/_bottom.html +3 -0
  60. package/templates/_download.html +1 -0
  61. package/templates/_heading.html +19 -0
  62. package/templates/_nav.html +18 -0
  63. package/templates/_theme.scss +97 -0
  64. package/templates/_top.html +87 -0
  65. package/templates/authors.schema.json +13 -0
  66. package/templates/code.html +31 -0
  67. package/templates/default.html +13 -0
  68. package/templates/literate.html +16 -0
  69. package/templates/nav.schema.json +27 -0
  70. package/tsconfig.json +15 -0
  71. package/types/dev.ts +3 -0
  72. package/types/sass.d.ts +1 -0
  73. package/types/site-variables.d.ts +16 -0
  74. package/webpack/apply-base-path-plugin.js +78 -0
  75. package/webpack/build-state.js +97 -0
  76. package/webpack/code.test.js +162 -0
  77. package/webpack/colors.js +15 -0
  78. package/webpack/config.base.js +147 -0
  79. package/webpack/config.dev.js +23 -0
  80. package/webpack/config.prod.js +32 -0
  81. package/webpack/content-watch-plugin.js +153 -0
  82. package/webpack/deflist-id-plugin.js +62 -0
  83. package/webpack/external-links-plugin.js +37 -0
  84. package/webpack/features.js +5 -0
  85. package/webpack/flair.json +1 -0
  86. package/webpack/generate-content-assets-plugin.js +308 -0
  87. package/webpack/generate-favicon-plugin.js +198 -0
  88. package/webpack/generate-fonts-plugin.js +69 -0
  89. package/webpack/generate-manifest-plugin.js +116 -0
  90. package/webpack/globals.js +74 -0
  91. package/webpack/heading-subtitle-plugin.js +80 -0
  92. package/webpack/json-schema.js +19 -0
  93. package/webpack/log.js +143 -0
  94. package/webpack/markdown-plugins.test.js +203 -0
  95. package/webpack/pagefind-plugin.js +379 -0
  96. package/webpack/pagefind-plugin.test.js +131 -0
  97. package/webpack/pdf-text.js +163 -0
  98. package/webpack/print-flair-plugin.js +22 -0
  99. package/webpack/reachability.js +273 -0
  100. package/webpack/reachability.test.js +80 -0
  101. package/webpack/serve.js +104 -0
  102. package/webpack/site-variables.js +53 -0
  103. package/webpack/site.schema.json +67 -0
  104. package/webpack/templates.js +128 -0
  105. package/webpack/text-to-id.js +8 -0
  106. package/webpack/toc-plugin.js +167 -0
  107. package/webpack/util.js +49 -0
  108. package/webpack/utils/code.js +439 -0
  109. package/webpack/utils/content-files.js +147 -0
  110. package/webpack/utils/define-plugin.js +20 -0
  111. package/webpack/utils/file-types.js +26 -0
  112. package/webpack/utils/front-matter.js +57 -0
  113. package/webpack/utils/jdi-runner/LiterateRunner.class +0 -0
  114. package/webpack/utils/jdi-runner/LiterateRunner.java +241 -0
  115. package/webpack/utils/literate-java.js +153 -0
  116. package/webpack/utils/markdown.js +244 -0
  117. package/webpack/utils/parse-hsl.js +8 -0
  118. package/webpack/utils/paths.js +58 -0
  119. package/webpack/utils/render.js +466 -0
  120. package/webpack/utils/shiki-highlighter.js +26 -0
  121. package/webpack/validate-internal-links-plugin.js +155 -0
  122. package/webpack/watch-reachability-state.js +273 -0
  123. package/webpack/watch-reachability-state.test.js +198 -0
  124. package/webpack/watch-reload-client.js +54 -0
  125. package/webpack/watch.js +166 -0
@@ -0,0 +1,322 @@
1
+ import { debounce } from '../util';
2
+
3
+ const LATENCY_MS = 50;
4
+
5
+ type HeadingLevel = '1' | '2' | '3' | '4' | '5' | '6';
6
+ type AlertType = 'warning' | 'note';
7
+
8
+ type Alert = { type: AlertType; title: string };
9
+ type Heading = { level: HeadingLevel; innerHtml: string; id: string };
10
+ type Dinkus = { type: 'dinkus' };
11
+
12
+ function getContainer(parent: HTMLElement): HTMLElement | null {
13
+ return parent.querySelector('nav.toc');
14
+ }
15
+
16
+ function getCurrentListItem(parent: HTMLElement): HTMLLIElement | null {
17
+ return parent.querySelector('nav.toc .current');
18
+ }
19
+
20
+ function scrollIfNeeded(element: HTMLElement) {
21
+ const container = getContainer(document.body);
22
+ if (container == null) {
23
+ return;
24
+ }
25
+ const containerHasScrollbar = container.scrollHeight > container.clientHeight;
26
+ if (!containerHasScrollbar) {
27
+ return;
28
+ }
29
+
30
+ // Calculate element center relative to container's scroll space
31
+ const elementRect = element.getBoundingClientRect();
32
+ const containerRect = container.getBoundingClientRect();
33
+ const elementCenter =
34
+ elementRect.top -
35
+ containerRect.top +
36
+ container.scrollTop +
37
+ elementRect.height / 2;
38
+ const desiredScrollTop = elementCenter - container.clientHeight / 2;
39
+
40
+ container.scrollTop = desiredScrollTop;
41
+ }
42
+
43
+ function getHighlightIndexes(items: (Heading | Alert | Dinkus)[]) {
44
+ const indexes: (number | null)[] = [];
45
+ let currentHeadingIndex: number | null = null;
46
+ let tocIndex = 0;
47
+
48
+ items.forEach(item => {
49
+ if (!('level' in item) && item.type === 'dinkus') {
50
+ return;
51
+ }
52
+
53
+ if ('level' in item) {
54
+ currentHeadingIndex = tocIndex;
55
+ }
56
+
57
+ indexes.push(currentHeadingIndex ?? tocIndex);
58
+ tocIndex++;
59
+ });
60
+
61
+ return indexes;
62
+ }
63
+
64
+ function getHeadingsAndAlerts(
65
+ parent: HTMLElement,
66
+ ): (HTMLHeadingElement | HTMLDivElement)[] {
67
+ return Array.from(
68
+ parent.querySelectorAll(
69
+ '.body h1, .body h2, .body h3, .body h4, .body h5, .body h6, .body > div.alert, .body section > div.alert',
70
+ ),
71
+ );
72
+ }
73
+
74
+ function getTocElements(
75
+ parent: HTMLElement,
76
+ ): (HTMLHeadingElement | HTMLDivElement | HTMLHRElement)[] {
77
+ return Array.from(
78
+ parent.querySelectorAll(
79
+ '.body h1, .body h2, .body h3, .body h4, .body h5, .body h6, .body > div.alert, .body section > div.alert, .body > hr',
80
+ ),
81
+ );
82
+ }
83
+
84
+ /* Calculate how much to offset scroll calculations based on floating header */
85
+ function getHeaderOffset() {
86
+ const element = document.querySelector('header details summary');
87
+ if (!element) {
88
+ return 0;
89
+ }
90
+
91
+ return element.getBoundingClientRect().height;
92
+ }
93
+
94
+ function getViewportActivationPoint() {
95
+ const headerOffset = getHeaderOffset();
96
+
97
+ return headerOffset + (window.innerHeight - headerOffset) / 3;
98
+ }
99
+
100
+ function headingToTableItem(el: HTMLHeadingElement): Heading {
101
+ const level = el.tagName[1] as HeadingLevel;
102
+
103
+ const subtitle = el.querySelector('.heading-subtitle');
104
+ const subtitleText = subtitle?.textContent || '';
105
+ let mainText = el.textContent || '';
106
+
107
+ if (mainText.length > 0 && subtitleText.length > 0) {
108
+ mainText = mainText.replace(subtitleText, '').trim();
109
+ return {
110
+ level,
111
+ id: el.id,
112
+ innerHtml: `${mainText}: <span class="heading-subtitle">${subtitleText}</span>`,
113
+ };
114
+ }
115
+ return { level, id: el.id, innerHtml: el.innerHTML };
116
+ }
117
+
118
+ function alertToTableItem(el: HTMLElement): Alert | null {
119
+ const classes = el.className
120
+ .split(' ')
121
+ .map(cl => cl.trim())
122
+ .filter(cl => cl != 'alert');
123
+
124
+ const firstClass = classes[0];
125
+ if (firstClass === 'warning' || firstClass === 'note') {
126
+ let title = el.querySelector('.title')?.innerHTML;
127
+ if (!title) {
128
+ if (firstClass === 'warning') {
129
+ title = 'Warning';
130
+ } else {
131
+ title = 'Note';
132
+ }
133
+ }
134
+
135
+ return { type: firstClass, title };
136
+ }
137
+
138
+ return null;
139
+ }
140
+
141
+ function switchCurrent(
142
+ oldCurrent: HTMLElement | null,
143
+ newCurrent: HTMLElement,
144
+ ) {
145
+ if (oldCurrent) {
146
+ oldCurrent.classList.remove('current');
147
+ }
148
+ newCurrent.classList.add('current');
149
+ }
150
+
151
+ function wireAlertClickHandlers(
152
+ toc: HTMLElement,
153
+ headingsAndAlerts: (HTMLHeadingElement | HTMLDivElement)[],
154
+ items: (Heading | Alert | Dinkus)[],
155
+ ) {
156
+ const alertLinks = toc.querySelectorAll('li.alert-item a');
157
+ let alertIdx = 0;
158
+ let scrollIdx = 0;
159
+
160
+ items.forEach(item => {
161
+ if (!('level' in item) && (item as Alert | Dinkus).type === 'dinkus') {
162
+ return;
163
+ }
164
+
165
+ if (!('level' in item)) {
166
+ // This is an alert item
167
+ const scrollEl = headingsAndAlerts[scrollIdx];
168
+ const link = alertLinks[alertIdx] as HTMLAnchorElement | undefined;
169
+ if (link && scrollEl) {
170
+ link.onclick = (e: MouseEvent) => {
171
+ e.preventDefault();
172
+
173
+ const titleElement = scrollEl.querySelector('.title');
174
+ const titleId = titleElement?.id || null;
175
+
176
+ if (titleId) {
177
+ history.replaceState(
178
+ null,
179
+ document.title,
180
+ `${window.location.pathname}#${titleId}`,
181
+ );
182
+ }
183
+
184
+ scrollEl.scrollIntoView();
185
+ scrollEl.focus();
186
+ };
187
+ }
188
+ alertIdx++;
189
+ }
190
+
191
+ scrollIdx++;
192
+ });
193
+ }
194
+
195
+ export default (window: Window) => {
196
+ const toc = window.document.querySelector('nav.toc') as HTMLElement;
197
+ if (toc == null) {
198
+ return;
199
+ }
200
+
201
+ const isCodePage = window.document.body.classList.contains('code');
202
+
203
+ if (isCodePage) {
204
+ const elements: HTMLAnchorElement[] = Array.from(
205
+ toc.querySelectorAll('ol li a'),
206
+ );
207
+ if (elements.length === 0) {
208
+ return;
209
+ }
210
+
211
+ const codeLines: number[] = elements.map(a => {
212
+ const match = a.getAttribute('href')?.match(/^#L(\d+)$/);
213
+ return match ? parseInt(match[1], 10) : 0;
214
+ });
215
+
216
+ const parseHash = (hash: string): number | null => {
217
+ const single = hash.match(/^#L(\d+)$/);
218
+ if (single) {
219
+ return parseInt(single[1], 10);
220
+ }
221
+ const range = hash.match(/^#L(\d+)-L(\d+)$/);
222
+ if (range) {
223
+ return parseInt(range[1], 10);
224
+ }
225
+ return null;
226
+ };
227
+
228
+ const updateFromHash = () => {
229
+ const line = parseHash(window.location.hash);
230
+ const existingItem = getCurrentListItem(document.body);
231
+
232
+ if (line == null) {
233
+ if (existingItem) {
234
+ existingItem.classList.remove('current');
235
+ }
236
+ return;
237
+ }
238
+
239
+ let best = 0;
240
+ for (let i = 0; i < codeLines.length; i++) {
241
+ if (codeLines[i] <= line) {
242
+ best = i;
243
+ } else {
244
+ break;
245
+ }
246
+ }
247
+
248
+ const nextItem = elements[best]?.parentElement ?? null;
249
+ if (nextItem != null && nextItem !== existingItem) {
250
+ switchCurrent(existingItem, nextItem);
251
+ scrollIfNeeded(nextItem);
252
+ }
253
+ };
254
+
255
+ window.addEventListener('hashchange', updateFromHash);
256
+ updateFromHash();
257
+
258
+ return () => {
259
+ window.removeEventListener('hashchange', updateFromHash);
260
+ };
261
+ }
262
+
263
+ // Regular page: TOC is already rendered in the DOM
264
+ const elements: HTMLAnchorElement[] = Array.from(
265
+ toc.querySelectorAll('ol li a'),
266
+ );
267
+ if (elements.length === 0) {
268
+ return;
269
+ }
270
+
271
+ const headingsAndAlerts = getHeadingsAndAlerts(window.document.body);
272
+ const items = getTocElements(window.document.body)
273
+ .map(el => {
274
+ const tag = el.tagName.toLowerCase();
275
+ if (tag === 'hr') {
276
+ return { type: 'dinkus' } as Dinkus;
277
+ } else if (tag === 'div') {
278
+ return alertToTableItem(el as HTMLElement);
279
+ } else {
280
+ return headingToTableItem(el as HTMLHeadingElement);
281
+ }
282
+ })
283
+ .filter(obj => obj != null);
284
+
285
+ const highlightIndexes = getHighlightIndexes(items);
286
+
287
+ // Wire up click handlers for alert items
288
+ wireAlertClickHandlers(toc, headingsAndAlerts, items);
289
+
290
+ function handleScroll() {
291
+ const viewportActivationPoint = getViewportActivationPoint();
292
+
293
+ let i = 0;
294
+ for (let idx = 0; idx < headingsAndAlerts.length; idx++) {
295
+ const top = headingsAndAlerts[idx].getBoundingClientRect().top;
296
+ if (top <= viewportActivationPoint + 1) {
297
+ i = idx;
298
+ } else {
299
+ break;
300
+ }
301
+ }
302
+
303
+ const existingItem = getCurrentListItem(document.body);
304
+ const highlightIndex = highlightIndexes[i];
305
+ const nextItem =
306
+ highlightIndex == null ? null : elements[highlightIndex]?.parentElement;
307
+
308
+ if (nextItem != null && nextItem !== existingItem) {
309
+ switchCurrent(existingItem, nextItem);
310
+ scrollIfNeeded(nextItem);
311
+ }
312
+ }
313
+ const debounced = debounce(handleScroll, LATENCY_MS);
314
+ window.addEventListener('scroll', debounced, { passive: true });
315
+
316
+ // Call after load to set current item
317
+ handleScroll();
318
+
319
+ return () => {
320
+ window.removeEventListener('scroll', debounced);
321
+ };
322
+ };
@@ -0,0 +1,203 @@
1
+ @import '../_mixins.scss';
2
+
3
+ :root {
4
+ --highlight-hue: 59deg;
5
+ --highlight-color: hsl(var(--highlight-hue) 100% 50% / 50%);
6
+ // For animations
7
+ --highlight-color-transparent: hsl(var(--highlight-hue) 50% 90% / 0%);
8
+ }
9
+
10
+ .toc {
11
+ @include user-select(none);
12
+
13
+ line-height: 1.4;
14
+ }
15
+
16
+ body.code .toc ol li:has(a) {
17
+ font-family: var(--mono-font);
18
+ font-size: var(--mono-font-size);
19
+ }
20
+
21
+ body.code .toc ol a {
22
+ white-space: nowrap;
23
+ overflow: hidden;
24
+ text-overflow: ellipsis;
25
+ }
26
+
27
+ .toc ol::before {
28
+ content: 'On this page';
29
+ display: block;
30
+ font-family: var(--mono-font);
31
+ font-style: italic;
32
+ font-size: var(--font-size-small);
33
+ text-align: center;
34
+ }
35
+
36
+ .toc ol {
37
+ margin: 0;
38
+ padding: 0;
39
+ }
40
+
41
+ .toc ol li {
42
+ list-style-type: none;
43
+ overflow-wrap: break-word;
44
+ margin-bottom: 0;
45
+ }
46
+
47
+ .toc ol a {
48
+ text-decoration: none;
49
+ border: none;
50
+ display: block;
51
+ padding: 4px 6px;
52
+ color: var(--fg-color);
53
+
54
+ &:focus-visible {
55
+ outline-offset: -2px;
56
+ }
57
+ }
58
+
59
+ @media (hover: hover) {
60
+ .toc ol li:has(a):hover,
61
+ .toc ol li:has(a):hover a {
62
+ color: var(--fg2-color);
63
+ }
64
+
65
+ .toc ol li a:active {
66
+ color: var(--link-active-color);
67
+ }
68
+ }
69
+
70
+ @media (min-width: 900px) {
71
+ .toc ol li.current a {
72
+ background: var(--bg2-color);
73
+ }
74
+
75
+ .toc ol li:last-child {
76
+ margin-bottom: var(--gap);
77
+ }
78
+
79
+ .toc ol::before {
80
+ display: none;
81
+ content: '';
82
+ }
83
+ }
84
+
85
+ .toc ol li.level1 {
86
+ font-size: var(--font-size-smaller);
87
+ }
88
+
89
+ .toc ol li.level2 {
90
+ font-size: var(--font-size-smaller);
91
+ }
92
+
93
+ .toc ol li.level3 {
94
+ font-size: var(--font-size-smaller);
95
+ margin-left: 10px;
96
+ }
97
+
98
+ .toc ol li.level4 {
99
+ font-size: var(--font-size-small);
100
+ font-weight: 450;
101
+ margin-left: 18px;
102
+ }
103
+
104
+ .toc ol li.level5 {
105
+ font-size: var(--font-size-small);
106
+ font-weight: 450;
107
+ margin-left: 24px;
108
+ }
109
+
110
+ .toc ol li.level6 {
111
+ font-size: var(--font-size-small);
112
+ font-weight: 450;
113
+ margin-left: 32px;
114
+ }
115
+
116
+ .toc ol li.dinkus-item {
117
+ height: 8px;
118
+ margin: 4px 0;
119
+ pointer-events: none;
120
+ background-color: var(--fg2-color);
121
+ mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 12'><rect x='0' y='5.5' width='96' height='1' fill='black'/><path d='M 64,6 C 71,6 80,4.5 80,1 C 80,4.5 89,6 96,6 C 89,6 80,7.5 80,11 C 80,7.5 71,6 64,6 Z' fill='black'/><rect x='64' y='5.5' width='96' height='1' fill='black'/></svg>");
122
+ mask-size: 100% 100%;
123
+ mask-repeat: no-repeat;
124
+ }
125
+
126
+ .toc ol li.alert-item {
127
+ position: relative;
128
+ }
129
+
130
+ .toc ol li.alert-item.warning a::before,
131
+ .toc ol li.alert-item.note a::before {
132
+ display: inline-block;
133
+ height: 20px;
134
+ vertical-align: bottom;
135
+ margin-right: 4px;
136
+ }
137
+
138
+ .toc ol li.alert-item.warning a::before {
139
+ // fg-color
140
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='hsl(20deg 8% 8%)' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='hsl(20deg 8% 8%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='hsl(20deg 8% 8%)' stroke-width='2' stroke-linecap='round'/></svg>");
141
+ }
142
+
143
+ @media (prefers-color-scheme: dark) {
144
+ .toc ol li.alert-item.warning a::before {
145
+ // fg2-color
146
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='hsl(20deg 20% 90%)' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='hsl(20deg 20% 90%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='hsl(20deg 20% 90%)' stroke-width='2' stroke-linecap='round'/></svg>");
147
+ }
148
+ }
149
+
150
+ @media (hover: hover) {
151
+ .toc ol li.alert-item.warning:hover a::before,
152
+ .toc ol li.alert-item.warning a:hover::before {
153
+ // fg2-color
154
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='hsl(20deg 6% 60%)' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='hsl(20deg 6% 60%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='hsl(20deg 6% 60%)' stroke-width='2' stroke-linecap='round'/></svg>");
155
+ }
156
+ }
157
+
158
+ @media (hover: hover) and (prefers-color-scheme: dark) {
159
+ .toc ol li.alert-item.warning:hover a::before,
160
+ .toc ol li.alert-item.warning a:hover::before {
161
+ // fg2-color
162
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><polygon points='12,2 22,20 2,20' fill='none' stroke='hsl(20deg 6% 55%)' stroke-width='2' stroke-linejoin='round'/><line x1='12' y1='9' x2='12' y2='13' stroke='hsl(20deg 6% 55%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='16' x2='12' y2='16' stroke='hsl(20deg 6% 55%)' stroke-width='2' stroke-linecap='round'/></svg>");
163
+ }
164
+ }
165
+
166
+ .toc ol li.alert-item.note a::before {
167
+ // fg-color
168
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='hsl(20deg 8% 8%)' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='hsl(20deg 8% 8%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='hsl(20deg 8% 8%)' stroke-width='2' stroke-linecap='round'/></svg>");
169
+ }
170
+
171
+ @media (prefers-color-scheme: dark) {
172
+ .toc ol li.alert-item.note a::before {
173
+ // fg-color
174
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='hsl(20deg 20% 90%)' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='hsl(20deg 20% 90%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='hsl(20deg 20% 90%)' stroke-width='2' stroke-linecap='round'/></svg>");
175
+ }
176
+ }
177
+
178
+ @media (hover: hover) {
179
+ .toc ol li.alert-item.note:hover a::before,
180
+ .toc ol li.alert-item.note a:hover::before {
181
+ // fg2-color
182
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='hsl(20deg 6% 60%)' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='hsl(20deg 6% 60%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='hsl(20deg 6% 60%)' stroke-width='2' stroke-linecap='round'/></svg>");
183
+ }
184
+ }
185
+
186
+ @media (hover: hover) and (prefers-color-scheme: dark) {
187
+ .toc ol li.alert-item.note:hover a::before,
188
+ .toc ol li.alert-item.note a:hover::before {
189
+ // fg2-color
190
+ content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'><circle cx='12' cy='12' r='10' fill='none' stroke='hsl(20deg 6% 55%)' stroke-width='2'/><line x1='12' y1='10' x2='12' y2='16' stroke='hsl(20deg 6% 55%)' stroke-width='2' stroke-linecap='round'/><line x1='12' y1='7' x2='12' y2='7' stroke='hsl(20deg 6% 55%)' stroke-width='2' stroke-linecap='round'/></svg>");
191
+ }
192
+ }
193
+
194
+ .toc ol li.label {
195
+ font-size: var(--font-size-smaller);
196
+ color: var(--fg2-color);
197
+ }
198
+
199
+ @media print {
200
+ .toc {
201
+ display: none;
202
+ }
203
+ }
@@ -0,0 +1,4 @@
1
+ # The `top` component
2
+
3
+ Show a floating "back to top" button once the page is scrolled more than 250px,
4
+ and keep it visible until the page is back near the top.
@@ -0,0 +1,75 @@
1
+ import { debounce, removeClass } from '../util';
2
+
3
+ /** Show the "Back to top" button once the user is past this scroll position */
4
+ const SHOW_THRESHOLD_PX = 250;
5
+
6
+ /** Debounce time (maximum amount of time to wait before updates) */
7
+ const LATENCY_MS = 50;
8
+
9
+ export default (window: Window) => {
10
+ const mountParent =
11
+ (window.document.getElementById(
12
+ 'to-top-container',
13
+ ) as HTMLElement | null) ?? document.body;
14
+
15
+ function createLink(parent: HTMLElement): HTMLAnchorElement {
16
+ const link = window.document.createElement('a');
17
+ link.href = '#';
18
+ link.className = 'button';
19
+ link.tabIndex = -1;
20
+ link.innerText = 'Back to top';
21
+ parent.appendChild(link);
22
+ return link;
23
+ }
24
+
25
+ const link = createLink(mountParent);
26
+
27
+ let isShowing = false;
28
+
29
+ link.onclick = e => {
30
+ e.preventDefault();
31
+ const cleanUrl = window.location.pathname + window.location.search;
32
+ if (window.location.hash) {
33
+ history.pushState(null, '', cleanUrl);
34
+ } else {
35
+ history.replaceState(null, '', cleanUrl);
36
+ }
37
+ window.scrollTo({ top: 0 });
38
+ const toc = window.document.querySelector('nav.toc') as HTMLElement | null;
39
+ if (toc) {
40
+ toc.scrollTop = 0;
41
+ }
42
+ };
43
+
44
+ function show(link: HTMLAnchorElement) {
45
+ if (!isShowing) {
46
+ link.classList.add('is-visible');
47
+ link.tabIndex = 0;
48
+ }
49
+ isShowing = true;
50
+ }
51
+
52
+ function hide(link: HTMLAnchorElement) {
53
+ if (isShowing) {
54
+ removeClass(link, 'is-visible');
55
+ link.tabIndex = -1;
56
+ }
57
+ isShowing = false;
58
+ }
59
+
60
+ function updateVisibility() {
61
+ if (window.scrollY > SHOW_THRESHOLD_PX) {
62
+ show(link);
63
+ } else {
64
+ hide(link);
65
+ }
66
+ }
67
+
68
+ const debounced = debounce(updateVisibility, LATENCY_MS);
69
+ window.addEventListener('scroll', debounced, { passive: true });
70
+ updateVisibility();
71
+
72
+ return () => {
73
+ window.removeEventListener('scroll', debounced);
74
+ };
75
+ };