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.
Files changed (59) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -3
  3. package/assets/base.css +2 -0
  4. package/docs/reference/authoring.md +48 -7
  5. package/docs/site/content/appearance.md +1 -1
  6. package/docs/site/content/audience.md +36 -8
  7. package/docs/site/content/commands.md +4 -2
  8. package/docs/site/content/drawing.md +4 -4
  9. package/docs/site/content/getting-started.md +2 -32
  10. package/docs/site/content/install.md +3 -1
  11. package/docs/site/content/presenting.md +29 -8
  12. package/docs/site/content/writing.md +27 -0
  13. package/examples/showcase/slides.ar.md +5 -5
  14. package/examples/showcase/slides.de.md +5 -5
  15. package/examples/showcase/slides.de.results.json +27 -0
  16. package/examples/showcase/slides.en.md +5 -5
  17. package/package.json +1 -1
  18. package/skills/write-slides/SKILL.md +4 -1
  19. package/src/build/check.js +27 -6
  20. package/src/build/components.js +5 -3
  21. package/src/build/homePlugin.js +64 -14
  22. package/src/build/include.js +249 -0
  23. package/src/build/inkPlugin.js +12 -2
  24. package/src/build/relayAccess.js +3 -1
  25. package/src/build/renderCheck.js +23 -3
  26. package/src/build/slidesPlugin.js +64 -16
  27. package/src/build/timingPlugin.js +34 -18
  28. package/src/cli/args.js +3 -3
  29. package/src/cli/index.js +49 -10
  30. package/src/components/Bibliography.jsx +7 -2
  31. package/src/components/Poll.jsx +2 -2
  32. package/src/components/QrCodeTag.jsx +2 -2
  33. package/src/components/WordCloud.jsx +7 -6
  34. package/src/components/activity.jsx +11 -8
  35. package/src/components/poll.css +2 -0
  36. package/src/core/icons.js +6 -1
  37. package/src/core/labels.js +3 -0
  38. package/src/core/parseSlides.js +14 -4
  39. package/src/core/reactions.js +27 -0
  40. package/src/core/resultsReport.js +153 -0
  41. package/src/core/source.js +8 -1
  42. package/src/core/validateDeck.js +6 -2
  43. package/src/home/Home.jsx +38 -93
  44. package/src/home/home.css +5 -17
  45. package/src/layouts/renderSlide.jsx +13 -1
  46. package/src/live/answer/answer.css +12 -1
  47. package/src/live/answer/answer.js +32 -4
  48. package/src/live/client.js +23 -4
  49. package/src/live/rooms.js +29 -7
  50. package/src/live/server.js +15 -1
  51. package/src/runtime/KeyHelp.jsx +115 -0
  52. package/src/runtime/PresenterChrome.jsx +168 -0
  53. package/src/runtime/Reactions.jsx +56 -0
  54. package/src/runtime/deck-stage.js +322 -17
  55. package/src/runtime/helpKeys.js +75 -0
  56. package/src/runtime/ink/room.js +6 -6
  57. package/src/runtime/main.jsx +396 -303
  58. package/src/runtime/slideControls.js +60 -0
  59. 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
- useEffect(() => { if (slideId != null && slideId !== state.slide) update(s => ({ ...s, slide: slideId })) }, [slideId])
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)