@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.
- package/README.md +65 -64
- package/bin/tada.ts +61 -66
- package/build/build-manifest.ts +14 -3
- package/build/bundle.ts +0 -1
- package/build/config-files.ts +183 -0
- package/build/config-loader.ts +159 -0
- package/build/copy.ts +0 -83
- package/build/generate-content-assets.ts +75 -160
- package/build/globals.ts +48 -89
- package/build/log.ts +27 -18
- package/build/pagefind.ts +33 -26
- package/build/pipeline.ts +98 -79
- package/build/reachability.ts +51 -248
- package/build/serve.ts +35 -6
- package/build/site-variables.ts +20 -11
- package/build/source-model.ts +400 -0
- package/build/{watch/records.ts → source-records.ts} +111 -49
- package/build/template-globals.ts +91 -0
- package/build/templates.ts +50 -40
- package/build/test-helpers.ts +15 -0
- package/build/toc-plugin.ts +5 -1
- package/build/types.d.ts +9 -1
- package/build/util.ts +8 -0
- package/build/utils/code.ts +6 -16
- package/build/utils/content-files.ts +57 -126
- package/build/utils/final-html.ts +239 -0
- package/build/utils/markdown.ts +237 -17
- package/build/utils/paths.ts +2 -1
- package/build/utils/render.ts +108 -99
- package/build/utils/trace-core.ts +3 -7
- package/build/validate-config-links.ts +91 -14
- package/build/watch/assets.ts +0 -2
- package/build/watch/build-full.ts +14 -15
- package/build/watch/build-incremental.ts +15 -8
- package/build/watch/compiler-types.d.ts +6 -1
- package/build/watch/compiler.ts +27 -15
- package/build/watch/index.ts +2 -7
- package/build/watch/planner.ts +32 -16
- package/build/watch/reload.ts +4 -0
- package/build/watch/runtime.ts +29 -45
- package/build/watch/snapshot.ts +50 -402
- package/build/watch/validation.ts +6 -3
- package/build/watch-reachability-state.ts +16 -23
- package/build/watch-reload-client.ts +11 -4
- package/init/content/index.md +1 -1
- package/init/content/labs/02/index.md +61 -0
- package/init/content/labs/02/selection_sort.py +23 -0
- package/init/content/labs/index.md +4 -2
- package/init/content/lectures/01/index.md +1 -1
- package/init/content/markdown.md +1 -1
- package/package.json +3 -4
- package/src/_base.scss +67 -0
- package/src/anchor/README.md +9 -0
- package/src/anchor/index.ts +115 -16
- package/src/anchor/style.scss +70 -10
- package/src/code/index.ts +46 -18
- package/src/code.scss +2 -1
- package/src/globals.ts +47 -0
- package/src/index.ts +1 -0
- package/src/navigate/index.ts +2 -5
- package/src/navigate/lifecycle.ts +3 -1
- package/src/navigate/runtime.ts +6 -5
- package/src/page-update/index.ts +20 -16
- package/src/print/style.scss +0 -4
- package/src/search/index.ts +53 -25
- package/src/slides/index.ts +481 -0
- package/src/slides/style.scss +104 -0
- package/src/style.scss +8 -3
- package/src/timezone/index.ts +16 -20
- package/src/top/index.ts +2 -4
- package/src/trace/index.ts +116 -14
- package/src/trace/style.scss +1 -1
- package/src/util.ts +6 -2
- package/templates/_author.html +2 -2
- package/templates/_heading.html +1 -1
- package/templates/_nav.html +2 -2
- package/templates/_theme.scss +4 -3
- package/templates/_top.html +14 -14
- package/templates/default.html +9 -0
- package/types/dev.d.ts +1 -1
- package/types/site-variables.d.ts +4 -5
- package/watch/fs-commit.ts +63 -10
- package/build/apply-base-path-plugin.ts +0 -186
- package/build/content-watch.ts +0 -104
- package/build/validate-internal-links-plugin.ts +0 -207
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
interface TraceToolbarState {
|
|
2
|
+
hidden: boolean;
|
|
3
|
+
ariaHidden: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type PresentationMode = 'normal' | 'fullscreen';
|
|
7
|
+
|
|
8
|
+
interface SlidesPresentDetail {
|
|
9
|
+
slideIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isInteractive(
|
|
13
|
+
view: Window & typeof globalThis,
|
|
14
|
+
target: EventTarget | null,
|
|
15
|
+
): boolean {
|
|
16
|
+
if (!(target instanceof view.Node)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const element =
|
|
21
|
+
target instanceof view.Element ? target : target.parentElement;
|
|
22
|
+
return Boolean(
|
|
23
|
+
element?.closest(
|
|
24
|
+
'a, button, input, select, textarea, summary, label, [role="button"], [contenteditable=""], [contenteditable="true"], .question-a-body',
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function updateActiveSlide(
|
|
30
|
+
slides: HTMLElement[],
|
|
31
|
+
activeIndex: number,
|
|
32
|
+
isPresenting: boolean,
|
|
33
|
+
): void {
|
|
34
|
+
for (const [index, slide] of slides.entries()) {
|
|
35
|
+
slide.classList.toggle('is-active', isPresenting && index === activeIndex);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isTraceReady(widget: HTMLElement): boolean {
|
|
40
|
+
return (
|
|
41
|
+
widget.querySelector('.trace-line-active') !== null ||
|
|
42
|
+
widget.querySelector('.trace-diagram > *') !== null
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function mountSlides(window: Window): void | (() => void) {
|
|
47
|
+
const { document } = window;
|
|
48
|
+
const view = document.defaultView;
|
|
49
|
+
const presentButton = document.querySelector(
|
|
50
|
+
'[data-slides-present]',
|
|
51
|
+
) as HTMLButtonElement | null;
|
|
52
|
+
const fullscreenCheckbox = document.querySelector(
|
|
53
|
+
'[data-slides-fullscreen]',
|
|
54
|
+
) as HTMLInputElement | null;
|
|
55
|
+
const rootScopeButton = presentButton ?? fullscreenCheckbox;
|
|
56
|
+
const scopedRoot = rootScopeButton
|
|
57
|
+
?.closest('main')
|
|
58
|
+
?.querySelector('[data-slides-root]');
|
|
59
|
+
const root = (scopedRoot ??
|
|
60
|
+
document.querySelector('[data-slides-root]')) as HTMLElement | null;
|
|
61
|
+
|
|
62
|
+
if (!view || !root) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const domView = view;
|
|
67
|
+
const domDocument = document as Document & {
|
|
68
|
+
fullscreenElement?: Element | null;
|
|
69
|
+
exitFullscreen?: () => Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
const fullscreenTarget = document.documentElement as HTMLElement & {
|
|
72
|
+
requestFullscreen?: () => Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
const slidesRoot = root;
|
|
75
|
+
const slides = Array.from(
|
|
76
|
+
slidesRoot.querySelectorAll('[data-slide-index]'),
|
|
77
|
+
) as HTMLElement[];
|
|
78
|
+
if (slides.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (presentButton) {
|
|
83
|
+
presentButton.disabled = false;
|
|
84
|
+
}
|
|
85
|
+
if (fullscreenCheckbox) {
|
|
86
|
+
fullscreenCheckbox.disabled = false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const overlay = document.createElement('div');
|
|
90
|
+
overlay.className = 'slides-toolbar';
|
|
91
|
+
overlay.hidden = true;
|
|
92
|
+
overlay.dataset.slidesOverlay = '';
|
|
93
|
+
|
|
94
|
+
const closeButton = document.createElement('button');
|
|
95
|
+
closeButton.type = 'button';
|
|
96
|
+
closeButton.textContent = 'Close';
|
|
97
|
+
closeButton.dataset.slidesClose = '';
|
|
98
|
+
|
|
99
|
+
overlay.append(closeButton);
|
|
100
|
+
document.body.appendChild(overlay);
|
|
101
|
+
|
|
102
|
+
let isPresenting = false;
|
|
103
|
+
let activeIndex = -1;
|
|
104
|
+
let presentationMode: PresentationMode | null = null;
|
|
105
|
+
let isToolbarPinnedVisible = false;
|
|
106
|
+
|
|
107
|
+
const toolbarStates = new WeakMap<HTMLElement, TraceToolbarState>();
|
|
108
|
+
const slideTabIndexes = new WeakMap<HTMLElement, string | null>();
|
|
109
|
+
|
|
110
|
+
function hidePresentationCursor(): void {
|
|
111
|
+
document.body.classList.add('is-presentation-cursor-hidden');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function showPresentationCursor(): void {
|
|
115
|
+
document.body.classList.remove('is-presentation-cursor-hidden');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function hidePresentationControls(resetPinned = true): void {
|
|
119
|
+
if (resetPinned) {
|
|
120
|
+
isToolbarPinnedVisible = false;
|
|
121
|
+
}
|
|
122
|
+
overlay.hidden = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function showPresentationControls(): void {
|
|
126
|
+
if (!isPresenting || presentationMode === 'fullscreen') {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
overlay.hidden = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pinPresentationControlsVisible(): void {
|
|
134
|
+
if (!isPresenting || presentationMode === 'fullscreen') {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
isToolbarPinnedVisible = true;
|
|
139
|
+
overlay.hidden = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getToolbarRevealBottom(): number {
|
|
143
|
+
const wasHidden = overlay.hidden;
|
|
144
|
+
if (wasHidden) {
|
|
145
|
+
overlay.hidden = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const bottom = closeButton.getBoundingClientRect().bottom;
|
|
149
|
+
|
|
150
|
+
if (wasHidden && !isToolbarPinnedVisible) {
|
|
151
|
+
overlay.hidden = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return bottom;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function setTraceToolbarVisibility(hidden: boolean): void {
|
|
158
|
+
const toolbars = slidesRoot.querySelectorAll('.trace-toolbar');
|
|
159
|
+
for (const toolbar of toolbars) {
|
|
160
|
+
const element = toolbar as HTMLElement;
|
|
161
|
+
|
|
162
|
+
if (hidden) {
|
|
163
|
+
if (!toolbarStates.has(element)) {
|
|
164
|
+
toolbarStates.set(element, {
|
|
165
|
+
hidden: element.hidden,
|
|
166
|
+
ariaHidden: element.getAttribute('aria-hidden'),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
element.hidden = true;
|
|
170
|
+
element.setAttribute('aria-hidden', 'true');
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const previous = toolbarStates.get(element);
|
|
175
|
+
element.hidden = previous?.hidden ?? false;
|
|
176
|
+
if (previous?.ariaHidden == null) {
|
|
177
|
+
element.removeAttribute('aria-hidden');
|
|
178
|
+
} else {
|
|
179
|
+
element.setAttribute('aria-hidden', previous.ariaHidden);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function setActiveSlide(index: number): void {
|
|
185
|
+
activeIndex = Math.max(0, Math.min(index, slides.length - 1));
|
|
186
|
+
updateActiveSlide(slides, activeIndex, isPresenting);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function focusSlide(slide: HTMLElement | undefined): void {
|
|
190
|
+
if (!slide) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!slideTabIndexes.has(slide)) {
|
|
195
|
+
slideTabIndexes.set(slide, slide.getAttribute('tabindex'));
|
|
196
|
+
}
|
|
197
|
+
slide.tabIndex = -1;
|
|
198
|
+
slide.focus();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function restoreSlideFocusability(): void {
|
|
202
|
+
for (const slide of slides) {
|
|
203
|
+
const prior = slideTabIndexes.get(slide);
|
|
204
|
+
if (prior === undefined) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (prior == null) {
|
|
208
|
+
slide.removeAttribute('tabindex');
|
|
209
|
+
} else {
|
|
210
|
+
slide.setAttribute('tabindex', prior);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function exitPresentation(): void {
|
|
216
|
+
if (!isPresenting) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const shouldExitFullscreen =
|
|
221
|
+
presentationMode === 'fullscreen' &&
|
|
222
|
+
domDocument.fullscreenElement != null &&
|
|
223
|
+
typeof domDocument.exitFullscreen === 'function';
|
|
224
|
+
|
|
225
|
+
isPresenting = false;
|
|
226
|
+
activeIndex = -1;
|
|
227
|
+
presentationMode = null;
|
|
228
|
+
hidePresentationControls();
|
|
229
|
+
showPresentationCursor();
|
|
230
|
+
document.body.classList.remove('is-presenting');
|
|
231
|
+
updateActiveSlide(slides, activeIndex, isPresenting);
|
|
232
|
+
restoreSlideFocusability();
|
|
233
|
+
setTraceToolbarVisibility(false);
|
|
234
|
+
|
|
235
|
+
if (shouldExitFullscreen) {
|
|
236
|
+
void domDocument.exitFullscreen?.();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function resetReadyTraces(): void {
|
|
241
|
+
const widgets = slidesRoot.querySelectorAll(
|
|
242
|
+
'.trace-widget:not(.trace-disabled)',
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
for (const widget of widgets) {
|
|
246
|
+
const widgetEl = widget as HTMLElement;
|
|
247
|
+
if (!isTraceReady(widgetEl)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const firstButton = widgetEl.querySelector(
|
|
252
|
+
'.trace-first:not([disabled])',
|
|
253
|
+
) as HTMLButtonElement | null;
|
|
254
|
+
|
|
255
|
+
firstButton?.click();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function enterPresentation(mode: PresentationMode, slideIndex = 0): void {
|
|
260
|
+
isPresenting = true;
|
|
261
|
+
presentationMode = mode;
|
|
262
|
+
hidePresentationControls();
|
|
263
|
+
showPresentationCursor();
|
|
264
|
+
document.body.classList.add('is-presenting');
|
|
265
|
+
setTraceToolbarVisibility(true);
|
|
266
|
+
resetReadyTraces();
|
|
267
|
+
setActiveSlide(slideIndex);
|
|
268
|
+
focusSlide(slides[activeIndex]);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function driveTrace(delta: 1 | -1): boolean {
|
|
272
|
+
if (activeIndex < 0) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const slide = slides[activeIndex];
|
|
277
|
+
const selector =
|
|
278
|
+
delta === 1
|
|
279
|
+
? '.trace-next:not([disabled])'
|
|
280
|
+
: '.trace-prev:not([disabled])';
|
|
281
|
+
const widgets = slide.querySelectorAll(
|
|
282
|
+
'.trace-widget:not(.trace-disabled)',
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
for (const widget of widgets) {
|
|
286
|
+
const widgetEl = widget as HTMLElement;
|
|
287
|
+
if (!isTraceReady(widgetEl)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const button = widgetEl.querySelector(
|
|
292
|
+
selector,
|
|
293
|
+
) as HTMLButtonElement | null;
|
|
294
|
+
if (!button) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
button.click();
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function move(delta: 1 | -1): void {
|
|
306
|
+
if (!isPresenting) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
hidePresentationCursor();
|
|
311
|
+
|
|
312
|
+
if (delta === -1) {
|
|
313
|
+
hidePresentationControls();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (driveTrace(delta)) {
|
|
317
|
+
if (delta === 1) {
|
|
318
|
+
hidePresentationControls();
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (delta === 1 && activeIndex >= slides.length - 1) {
|
|
324
|
+
pinPresentationControlsVisible();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (delta === 1) {
|
|
329
|
+
hidePresentationControls();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
setActiveSlide(activeIndex + delta);
|
|
333
|
+
focusSlide(slides[activeIndex]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handlePresentFullscreenClick(slideIndex = 0): Promise<void> {
|
|
337
|
+
enterPresentation('fullscreen', slideIndex);
|
|
338
|
+
|
|
339
|
+
if (typeof fullscreenTarget.requestFullscreen !== 'function') {
|
|
340
|
+
presentationMode = 'normal';
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
await fullscreenTarget.requestFullscreen();
|
|
346
|
+
} catch {
|
|
347
|
+
presentationMode = 'normal';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function startPresentation(slideIndex = 0): void {
|
|
352
|
+
if (fullscreenCheckbox?.checked === true) {
|
|
353
|
+
void handlePresentFullscreenClick(slideIndex);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
enterPresentation('normal', slideIndex);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handlePresentClick(): void {
|
|
361
|
+
startPresentation(0);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function handleCloseClick(): void {
|
|
365
|
+
exitPresentation();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function handleFullscreenChange(): void {
|
|
369
|
+
if (
|
|
370
|
+
isPresenting &&
|
|
371
|
+
presentationMode === 'fullscreen' &&
|
|
372
|
+
domDocument.fullscreenElement == null
|
|
373
|
+
) {
|
|
374
|
+
exitPresentation();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
379
|
+
if (!isPresenting) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
(event.key === 'ArrowRight' ||
|
|
385
|
+
event.key === 'ArrowLeft' ||
|
|
386
|
+
event.key === ' ' ||
|
|
387
|
+
event.key === 'Spacebar') &&
|
|
388
|
+
isInteractive(domView, document.activeElement)
|
|
389
|
+
) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (event.key === 'ArrowRight') {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
move(1);
|
|
396
|
+
} else if (event.key === 'ArrowLeft') {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
move(-1);
|
|
399
|
+
} else if (event.key === ' ' || event.key === 'Spacebar') {
|
|
400
|
+
event.preventDefault();
|
|
401
|
+
move(1);
|
|
402
|
+
} else if (event.key === 'Escape') {
|
|
403
|
+
event.preventDefault();
|
|
404
|
+
exitPresentation();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function handleSlideClick(event: MouseEvent): void {
|
|
409
|
+
if (!isPresenting || isInteractive(domView, event.target)) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const activeSlide = slides[activeIndex];
|
|
414
|
+
if (!activeSlide) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (
|
|
419
|
+
!(event.target instanceof domView.Node) ||
|
|
420
|
+
!activeSlide.contains(event.target)
|
|
421
|
+
) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
move(1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function handleMouseMove(event: MouseEvent): void {
|
|
429
|
+
if (!isPresenting) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
showPresentationCursor();
|
|
434
|
+
|
|
435
|
+
if (presentationMode === 'fullscreen') {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (isToolbarPinnedVisible) {
|
|
440
|
+
showPresentationControls();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (event.clientY <= getToolbarRevealBottom()) {
|
|
445
|
+
showPresentationControls();
|
|
446
|
+
} else {
|
|
447
|
+
hidePresentationControls(false);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function handleSlidesPresent(event: Event): void {
|
|
452
|
+
const detail = (event as CustomEvent<SlidesPresentDetail>).detail ?? {};
|
|
453
|
+
const slideIndex =
|
|
454
|
+
typeof detail.slideIndex === 'number' &&
|
|
455
|
+
Number.isFinite(detail.slideIndex)
|
|
456
|
+
? detail.slideIndex
|
|
457
|
+
: 0;
|
|
458
|
+
|
|
459
|
+
startPresentation(slideIndex);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
presentButton?.addEventListener('click', handlePresentClick);
|
|
463
|
+
closeButton.addEventListener('click', handleCloseClick);
|
|
464
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
465
|
+
window.addEventListener('keydown', handleKeydown);
|
|
466
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
467
|
+
slidesRoot.addEventListener('click', handleSlideClick);
|
|
468
|
+
slidesRoot.addEventListener('tada:slides-present', handleSlidesPresent);
|
|
469
|
+
|
|
470
|
+
return () => {
|
|
471
|
+
exitPresentation();
|
|
472
|
+
presentButton?.removeEventListener('click', handlePresentClick);
|
|
473
|
+
closeButton.removeEventListener('click', handleCloseClick);
|
|
474
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
475
|
+
window.removeEventListener('keydown', handleKeydown);
|
|
476
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
477
|
+
slidesRoot.removeEventListener('click', handleSlideClick);
|
|
478
|
+
slidesRoot.removeEventListener('tada:slides-present', handleSlidesPresent);
|
|
479
|
+
overlay.remove();
|
|
480
|
+
};
|
|
481
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
@use '../mixins' as *;
|
|
2
|
+
|
|
3
|
+
.slide-deck,
|
|
4
|
+
.slide {
|
|
5
|
+
display: block;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body.is-presenting {
|
|
9
|
+
--slides-scale: clamp(150%, 3.6vmin, 250%);
|
|
10
|
+
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body.is-presenting.is-presentation-cursor-hidden,
|
|
15
|
+
body.is-presenting.is-presentation-cursor-hidden * {
|
|
16
|
+
cursor: none !important;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body.is-presenting header,
|
|
20
|
+
body.is-presenting .title-and-info,
|
|
21
|
+
body.is-presenting nav.toc,
|
|
22
|
+
body.is-presenting footer,
|
|
23
|
+
body.is-presenting .slides-header,
|
|
24
|
+
body.is-presenting .trace-toolbar {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (hover: none) {
|
|
29
|
+
.slides-header {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
body.is-presenting [data-slides-close] {
|
|
35
|
+
position: fixed;
|
|
36
|
+
top: var(--gap);
|
|
37
|
+
right: var(--gap);
|
|
38
|
+
z-index: 1001;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media print {
|
|
42
|
+
[data-slides-close] {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
body.is-presenting .slide-deck {
|
|
48
|
+
position: fixed;
|
|
49
|
+
inset: 0;
|
|
50
|
+
z-index: 999;
|
|
51
|
+
background: var(--bg-color);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
body.is-presenting .slide {
|
|
55
|
+
@include user-select(none);
|
|
56
|
+
|
|
57
|
+
position: fixed;
|
|
58
|
+
top: 50%;
|
|
59
|
+
left: 50%;
|
|
60
|
+
z-index: 1000;
|
|
61
|
+
box-sizing: border-box;
|
|
62
|
+
display: none;
|
|
63
|
+
width: min(100vw, calc(100vh * 4 / 3));
|
|
64
|
+
height: min(100vh, calc(100vw * 3 / 4));
|
|
65
|
+
padding: calc(3 * var(--gap));
|
|
66
|
+
overflow: auto;
|
|
67
|
+
font-size: var(--slides-scale);
|
|
68
|
+
letter-spacing: 0.2px;
|
|
69
|
+
outline: none;
|
|
70
|
+
background: var(--bg-color);
|
|
71
|
+
transform: translate(-50%, -50%);
|
|
72
|
+
|
|
73
|
+
> :first-child {
|
|
74
|
+
margin-top: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
> h2:first-child {
|
|
78
|
+
text-align: center;
|
|
79
|
+
|
|
80
|
+
a.heading-anchor {
|
|
81
|
+
color: var(--theme-color-text);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
body.is-presenting .slide.is-active {
|
|
87
|
+
display: block;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
body.is-presenting .slide.is-active:has(> .trace-widget) {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
body.is-presenting .slide:has(> .trace-widget) > .trace-widget {
|
|
96
|
+
flex: 1 1 auto;
|
|
97
|
+
min-height: 0;
|
|
98
|
+
font-size: 66.6%;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
body.is-presenting .slide:has(> .trace-widget) > .trace-widget > .trace-body {
|
|
102
|
+
height: 100%;
|
|
103
|
+
min-height: 0;
|
|
104
|
+
}
|
package/src/style.scss
CHANGED
|
@@ -235,12 +235,17 @@ ul.styled-list {
|
|
|
235
235
|
|
|
236
236
|
ul.styled-list {
|
|
237
237
|
// Make bullet points slightly bigger
|
|
238
|
-
font-size: 1.
|
|
238
|
+
font-size: 1.2em;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
ol li .styled-list-item {
|
|
242
|
+
font-size: 1em;
|
|
243
|
+
font-weight: var(--font-weight);
|
|
244
|
+
color: var(--fg-color);
|
|
239
245
|
}
|
|
240
246
|
|
|
241
|
-
ol li .styled-list-item,
|
|
242
247
|
ul li .styled-list-item {
|
|
243
|
-
font-size:
|
|
248
|
+
font-size: 0.8333em;
|
|
244
249
|
font-weight: var(--font-weight);
|
|
245
250
|
color: var(--fg-color);
|
|
246
251
|
}
|
package/src/timezone/index.ts
CHANGED
|
@@ -45,6 +45,11 @@ export function normalizeHM(totalMinutes: number) {
|
|
|
45
45
|
return [h, m] as const;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function getDayShift(totalMinutes: number) {
|
|
49
|
+
// Negative partial days like -30 minutes should still count as the previous day.
|
|
50
|
+
return Math.floor(totalMinutes / 1440);
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
function parseHHMM(hhmm: string) {
|
|
49
54
|
const [h, m] = hhmm.split(':');
|
|
50
55
|
if (h == null || m == null) {
|
|
@@ -85,26 +90,16 @@ function getOffsetMinutes(tz: string, date: Date): number {
|
|
|
85
90
|
return (utcTs - date.getTime()) / 60000;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
// Read lazily so tests can set __SITE_TIMEZONES__ before the first call.
|
|
89
|
-
// Cache the result so mutations (like computeOffsets adding offsetMinutes)
|
|
90
|
-
// persist across calls. The bundler inlines __SITE_TIMEZONES__ as a JSON
|
|
91
|
-
// literal, so each raw access would create a fresh array.
|
|
92
|
-
let _timezones: TimeZone[] | null = null;
|
|
93
93
|
function getTimezones(): TimeZone[] {
|
|
94
|
-
|
|
95
|
-
_timezones =
|
|
96
|
-
typeof __SITE_TIMEZONES__ !== 'undefined' ? __SITE_TIMEZONES__ : [];
|
|
97
|
-
}
|
|
98
|
-
return _timezones;
|
|
94
|
+
return __SITE_TIMEZONES__.map(tz => ({ ...tz }));
|
|
99
95
|
}
|
|
100
96
|
|
|
101
|
-
function getDefaultTimezone() {
|
|
102
|
-
|
|
103
|
-
return getTimezones().find(tz => tz.value === value)!;
|
|
97
|
+
function getDefaultTimezone(timezones: TimeZone[]): TimeZone {
|
|
98
|
+
return timezones.find(tz => tz.value === __SITE_DEFAULT_TIMEZONE__)!;
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
function computeOffsets(baseDate: Date) {
|
|
107
|
-
|
|
101
|
+
function computeOffsets(timezones: TimeZone[], baseDate: Date) {
|
|
102
|
+
timezones.forEach(tz => {
|
|
108
103
|
tz.offsetMinutes = getOffsetMinutes(tz.value, baseDate);
|
|
109
104
|
});
|
|
110
105
|
}
|
|
@@ -143,14 +138,15 @@ function dominantPeriodStyle(
|
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
export default (window: Window) => {
|
|
146
|
-
const
|
|
141
|
+
const timezones = getTimezones();
|
|
142
|
+
const defaultTz = getDefaultTimezone(timezones);
|
|
147
143
|
const resetTitle = `Reset time zone to ${defaultTz.abbreviation} (default)`;
|
|
148
144
|
|
|
149
145
|
// determine initial time zone (from storage or default)
|
|
150
146
|
let initialTz = defaultTz.value;
|
|
151
147
|
try {
|
|
152
148
|
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
153
|
-
if (stored &&
|
|
149
|
+
if (stored && timezones.some(t => t.value === stored)) {
|
|
154
150
|
initialTz = stored;
|
|
155
151
|
}
|
|
156
152
|
} catch {
|
|
@@ -158,7 +154,7 @@ export default (window: Window) => {
|
|
|
158
154
|
}
|
|
159
155
|
|
|
160
156
|
const now = new Date();
|
|
161
|
-
computeOffsets(now);
|
|
157
|
+
computeOffsets(timezones, now);
|
|
162
158
|
const defaultOffset = defaultTz.offsetMinutes ?? 0;
|
|
163
159
|
|
|
164
160
|
// Snapshot the original AM/PM style from each <time> element's text
|
|
@@ -173,7 +169,7 @@ export default (window: Window) => {
|
|
|
173
169
|
const pagePeriodStyle = dominantPeriodStyle(periodStyles);
|
|
174
170
|
|
|
175
171
|
function updateTimes(targetTz: string) {
|
|
176
|
-
const target =
|
|
172
|
+
const target = timezones.find(t => t.value === targetTz) || defaultTz;
|
|
177
173
|
const targetOffset = target.offsetMinutes ?? 0;
|
|
178
174
|
const deltaMinutes = targetOffset - defaultOffset;
|
|
179
175
|
|
|
@@ -196,7 +192,7 @@ export default (window: Window) => {
|
|
|
196
192
|
|
|
197
193
|
// raw (can be < 0 or > 1439)
|
|
198
194
|
const rawMinutes = baseMinutes + deltaMinutes;
|
|
199
|
-
const dayShift =
|
|
195
|
+
const dayShift = getDayShift(rawMinutes);
|
|
200
196
|
// Normalize after computing dayShift
|
|
201
197
|
const [h, m] = normalizeHM(rawMinutes);
|
|
202
198
|
|
package/src/top/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { debounce, removeClass } from '../util';
|
|
2
|
+
import { globals } from '../globals';
|
|
2
3
|
|
|
3
4
|
/** Show the "Back to top" button once the user is past this scroll position */
|
|
4
5
|
const SHOW_THRESHOLD_PX = 250;
|
|
@@ -30,10 +31,7 @@ export default (window: Window) => {
|
|
|
30
31
|
e.preventDefault();
|
|
31
32
|
const cleanUrl = window.location.pathname + window.location.search;
|
|
32
33
|
if (window.location.hash) {
|
|
33
|
-
|
|
34
|
-
// hashchange fires. location.hash = '' may leave a trailing '#';
|
|
35
|
-
// strip it with replaceState.
|
|
36
|
-
window.location.hash = '';
|
|
34
|
+
globals.setLocationHash(window, '');
|
|
37
35
|
window.history.replaceState(null, '', cleanUrl);
|
|
38
36
|
} else {
|
|
39
37
|
window.history.replaceState(null, '', cleanUrl);
|