mdeck 3.11.0 → 3.12.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.
- package/CHANGELOG.md +28 -0
- package/README.md +3 -3
- package/assets/base.css +2 -0
- package/docs/reference/authoring.md +48 -7
- package/docs/site/content/appearance.md +1 -1
- package/docs/site/content/audience.md +36 -8
- package/docs/site/content/commands.md +4 -2
- package/docs/site/content/drawing.md +4 -4
- package/docs/site/content/getting-started.md +2 -32
- package/docs/site/content/install.md +3 -1
- package/docs/site/content/presenting.md +29 -8
- package/docs/site/content/writing.md +27 -0
- package/examples/showcase/slides.ar.md +5 -5
- package/examples/showcase/slides.de.md +5 -5
- package/examples/showcase/slides.de.results.json +27 -0
- package/examples/showcase/slides.en.md +5 -5
- package/package.json +1 -1
- package/skills/write-slides/SKILL.md +4 -1
- package/src/build/check.js +27 -6
- package/src/build/components.js +5 -3
- package/src/build/homePlugin.js +64 -14
- package/src/build/include.js +249 -0
- package/src/build/inkPlugin.js +12 -2
- package/src/build/relayAccess.js +3 -1
- package/src/build/renderCheck.js +23 -3
- package/src/build/slidesPlugin.js +64 -16
- package/src/build/timingPlugin.js +34 -18
- package/src/cli/args.js +3 -3
- package/src/cli/index.js +49 -10
- package/src/components/Bibliography.jsx +7 -2
- package/src/components/Poll.jsx +2 -2
- package/src/components/QrCodeTag.jsx +2 -2
- package/src/components/WordCloud.jsx +7 -6
- package/src/components/activity.jsx +11 -8
- package/src/components/poll.css +2 -0
- package/src/core/icons.js +6 -1
- package/src/core/labels.js +3 -0
- package/src/core/parseSlides.js +14 -4
- package/src/core/reactions.js +27 -0
- package/src/core/resultsReport.js +153 -0
- package/src/core/source.js +8 -1
- package/src/core/validateDeck.js +6 -2
- package/src/home/Home.jsx +38 -93
- package/src/home/home.css +5 -17
- package/src/layouts/renderSlide.jsx +13 -1
- package/src/live/answer/answer.css +12 -1
- package/src/live/answer/answer.js +32 -4
- package/src/live/client.js +23 -4
- package/src/live/rooms.js +29 -7
- package/src/live/server.js +15 -1
- package/src/runtime/KeyHelp.jsx +115 -0
- package/src/runtime/PresenterChrome.jsx +168 -0
- package/src/runtime/Reactions.jsx +56 -0
- package/src/runtime/deck-stage.js +322 -17
- package/src/runtime/helpKeys.js +75 -0
- package/src/runtime/ink/room.js +6 -6
- package/src/runtime/main.jsx +396 -303
- package/src/runtime/slideControls.js +60 -0
- package/src/runtime/useTalkTimer.js +4 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// What the presenter can do on the slide shown, for the phone clicker
|
|
2
|
+
// (main.jsx): the buttons components mark with `data-presenter-control` (a
|
|
3
|
+
// poll's lock, eye, tick and Reset), the slide's videos (play or pause), and
|
|
4
|
+
// its hotspots (zoom to a part, back to the whole slide). The clicker's slide
|
|
5
|
+
// frame reports them; a tap on the clicker presses the original here, so the
|
|
6
|
+
// change reaches the other screens the way a click on the slide does.
|
|
7
|
+
|
|
8
|
+
const activeSlide = stage => stage.querySelector(':scope > [data-deck-active]')
|
|
9
|
+
|
|
10
|
+
function controlElements(stage) {
|
|
11
|
+
const slide = activeSlide(stage)
|
|
12
|
+
if (!slide) return []
|
|
13
|
+
return [...slide.querySelectorAll('[data-presenter-control], video')].filter(element => !element.closest('[data-deck-static]'))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const labelOf = element => element.getAttribute('data-presenter-control') || element.getAttribute('aria-label') || element.getAttribute('title') || element.textContent.trim()
|
|
17
|
+
|
|
18
|
+
/** { items: [{ label, icon, pressed, disabled }], hotspots: n, hotspot } for the slide shown. `icon` names one of mdeck's icons (`data-presenter-icon`). */
|
|
19
|
+
export function slideControls(stage) {
|
|
20
|
+
const items = controlElements(stage).map(element => element.localName === 'video'
|
|
21
|
+
? { label: element.paused ? 'Play the video' : 'Pause the video', icon: element.paused ? 'play' : 'pause', pressed: !element.paused, disabled: false }
|
|
22
|
+
: { label: labelOf(element), icon: element.getAttribute('data-presenter-icon') || null, pressed: element.getAttribute('aria-pressed') === 'true', disabled: !!element.disabled })
|
|
23
|
+
const hotspots = stage.hotspotMode === 'off' ? 0 : Math.min(9, stage.hotspots.length)
|
|
24
|
+
return { items, hotspots, hotspot: hotspots ? stage.hotspot ?? null : null }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Presses control `index` of slideControls(stage). */
|
|
28
|
+
export function pressSlideControl(stage, index) {
|
|
29
|
+
const element = controlElements(stage)[index]
|
|
30
|
+
if (!element || element.disabled) return false
|
|
31
|
+
if (element.localName === 'video') {
|
|
32
|
+
// Pressed from the toolbar or a phone, not in this frame: the browser may
|
|
33
|
+
// refuse sound here. The audience window plays it; this copy goes silent.
|
|
34
|
+
if (element.paused) element.play().catch(error => {
|
|
35
|
+
if (error?.name !== 'NotAllowedError') return
|
|
36
|
+
element.muted = true
|
|
37
|
+
return element.play()
|
|
38
|
+
}).catch(() => {})
|
|
39
|
+
else element.pause()
|
|
40
|
+
} else element.click()
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Calls `report(slideControls(stage))` whenever they may have changed, a few times a second at most. */
|
|
45
|
+
export function watchSlideControls(stage, report) {
|
|
46
|
+
// A timer, not an animation frame: a presenter view in a background tab
|
|
47
|
+
// gets no frames, but its clicker still wants to know.
|
|
48
|
+
let timer = 0
|
|
49
|
+
const send = () => {
|
|
50
|
+
if (timer) return
|
|
51
|
+
timer = setTimeout(() => { timer = 0; report(slideControls(stage)) }, 60)
|
|
52
|
+
}
|
|
53
|
+
for (const type of ['slidechange', 'statechange', 'zoomchange', 'hotspotschange']) stage.addEventListener(type, send)
|
|
54
|
+
// Videos say when they play or pause; their events do not bubble.
|
|
55
|
+
for (const type of ['play', 'pause']) stage.addEventListener(type, send, true)
|
|
56
|
+
const observer = new MutationObserver(send)
|
|
57
|
+
observer.observe(stage, { subtree: true, childList: true, attributes: true, attributeFilter: ['aria-pressed', 'disabled', 'data-deck-active', 'data-presenter-control', 'data-presenter-icon'] })
|
|
58
|
+
send()
|
|
59
|
+
return () => observer.disconnect()
|
|
60
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// A rehearsal is the same clock, started from nothing: the time each slide
|
|
7
7
|
// got is what the rehearsal summary offers to save (core/timing.js).
|
|
8
|
-
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
8
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'
|
|
9
9
|
|
|
10
10
|
const fresh = () => ({ running: false, rehearsing: false, mark: 0, total: 0, spent: {}, slide: null })
|
|
11
11
|
|
|
@@ -37,7 +37,9 @@ export function useTalkTimer(key, slideId, { rehearse = false } = {}) {
|
|
|
37
37
|
const update = change => setState(current => change(settle(current, Date.now())))
|
|
38
38
|
|
|
39
39
|
useEffect(() => { try { sessionStorage.setItem(key, JSON.stringify(state)) } catch {} }, [state])
|
|
40
|
-
|
|
40
|
+
// At once, not after the next paint: in a background tab that comes late,
|
|
41
|
+
// and the time in between went to the slide before.
|
|
42
|
+
useLayoutEffect(() => { if (slideId != null && slideId !== state.slide) update(s => ({ ...s, slide: slideId })) }, [slideId])
|
|
41
43
|
useEffect(() => {
|
|
42
44
|
if (!state.running) return
|
|
43
45
|
const timer = setInterval(() => setTick(n => n + 1), 500)
|