mdeck 3.2.0 → 3.4.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.
- package/CHANGELOG.md +27 -0
- package/assets/base.css +23 -2
- package/assets/extensions/themes/academic/styles.css +33 -5
- package/assets/extensions/themes/aurora/styles.css +32 -6
- package/assets/extensions/themes/minimal/styles.css +16 -4
- package/assets/extensions/themes/neue/styles.css +17 -4
- package/docs/reference/authoring.md +89 -6
- package/docs/site/content/audience.md +41 -4
- package/docs/site/content/commands.md +30 -4
- package/docs/site/content/drawing.md +1 -1
- package/docs/site/content/more-content.md +22 -0
- package/docs/site/content/sharing.md +1 -0
- package/docs/site/content/troubleshooting.md +1 -1
- package/docs/site/content/views.md +3 -1
- package/examples/citations/refs.bib +30 -0
- package/examples/citations/refs.yml +21 -0
- package/examples/citations/slides.md +43 -0
- package/examples/citations/slides.pdf +0 -0
- package/package.json +5 -1
- package/skills/write-slides/SKILL.md +7 -3
- package/src/build/bibliography.js +307 -0
- package/src/build/check.js +12 -0
- package/src/build/renderCheck.js +98 -0
- package/src/build/resultsPlugin.js +77 -0
- package/src/build/slidesPlugin.js +70 -7
- package/src/build/themeCheck.js +8 -5
- package/src/cli/args.js +22 -5
- package/src/cli/index.js +54 -12
- package/src/components/Bibliography.jsx +23 -0
- package/src/components/Numeric.jsx +59 -0
- package/src/components/Poll.jsx +59 -33
- package/src/components/QrCodeTag.jsx +11 -2
- package/src/components/Question.jsx +10 -6
- package/src/components/Scale.jsx +13 -8
- package/src/components/VideoPlayer.jsx +16 -0
- package/src/components/WordCloud.jsx +10 -6
- package/src/components/activity.jsx +90 -9
- package/src/components/citation.css +28 -0
- package/src/components/inlineText.js +40 -0
- package/src/components/numbers.js +39 -0
- package/src/components/poll.css +27 -7
- package/src/core/citations.js +108 -0
- package/src/core/editDeck.js +43 -0
- package/src/core/icons.js +2 -0
- package/src/core/ink.js +16 -0
- package/src/core/labels.js +48 -0
- package/src/core/parseSlides.js +1 -1
- package/src/core/results.js +64 -0
- package/src/core/urls.js +2 -2
- package/src/core/validateDeck.js +7 -1
- package/src/editor/sampleDeck.js +34 -8
- package/src/layouts/layoutApi.jsx +14 -3
- package/src/live/answer/answer.css +20 -0
- package/src/live/answer/answer.js +149 -24
- package/src/live/client.js +75 -7
- package/src/live/roomTag.js +6 -2
- package/src/live/rooms.js +6 -1
- package/src/runtime/ReaderView.jsx +2 -1
- package/src/runtime/citations.js +70 -0
- package/src/runtime/deck-stage.js +5 -2
- package/src/runtime/editorBridge.js +7 -1
- package/src/runtime/follow.js +89 -0
- package/src/runtime/ink/bus.js +7 -6
- package/src/runtime/ink/room.js +8 -0
- package/src/runtime/ink/select.js +4 -2
- package/src/runtime/main.jsx +22 -8
- package/src/runtime/registry.jsx +4 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The follow view (?view=follow): a phone or laptop in the room shows the
|
|
2
|
+
// presenter's slide and ink, read from the stage room, and sends nothing.
|
|
3
|
+
// A follower may page back on their own and return with "Back to live";
|
|
4
|
+
// slides and steps the presenter has not shown yet stay hidden.
|
|
5
|
+
|
|
6
|
+
/** Where a follower may be: no further than the furthest the presenter has shown. */
|
|
7
|
+
export function clampToShown({ index, step }, furthest) {
|
|
8
|
+
if (!furthest) return { index: 0, step: -1 }
|
|
9
|
+
if (index > furthest.index) return { index: furthest.index, step: furthest.step }
|
|
10
|
+
if (index === furthest.index && step > furthest.step) return { index, step: furthest.step }
|
|
11
|
+
return { index, step }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The furthest of two positions, slide first, then step.
|
|
15
|
+
const further = (a, b) => !a ? b : (b.index > a.index || (b.index === a.index && b.step > a.step)) ? b : a
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The follower's state: the presenter's position, the furthest shown, and
|
|
19
|
+
* whether this device is with the presenter (`live`).
|
|
20
|
+
* presenter(position) the presenter moved: returns where to go, or null
|
|
21
|
+
* moved(position) the follower moved: returns where to go back to if
|
|
22
|
+
* they went past what was shown, else null
|
|
23
|
+
* backToLive() returns the presenter's position
|
|
24
|
+
*/
|
|
25
|
+
export function createFollower() {
|
|
26
|
+
const state = { presenter: null, furthest: null, live: true }
|
|
27
|
+
const same = (a, b) => !!a && !!b && a.index === b.index && a.step === b.step
|
|
28
|
+
return {
|
|
29
|
+
state,
|
|
30
|
+
presenter(position) {
|
|
31
|
+
state.presenter = position
|
|
32
|
+
state.furthest = further(state.furthest, position)
|
|
33
|
+
return state.live ? position : null
|
|
34
|
+
},
|
|
35
|
+
moved(position) {
|
|
36
|
+
const allowed = clampToShown(position, state.furthest)
|
|
37
|
+
state.live = state.presenter ? same(allowed, state.presenter) : true
|
|
38
|
+
return same(allowed, position) ? null : allowed
|
|
39
|
+
},
|
|
40
|
+
backToLive() {
|
|
41
|
+
state.live = true
|
|
42
|
+
return state.presenter
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const BAR_CSS = `
|
|
48
|
+
.follow-bar { position: fixed; left: 50%; top: max(14px, env(safe-area-inset-top)); transform: translateX(-50%); z-index: 2147483001;
|
|
49
|
+
display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; border: 1px solid #3a3a3a; border-radius: 999px;
|
|
50
|
+
background: rgba(22, 22, 22, 0.92); color: #e6e6e6; font: 14px/1.2 system-ui, -apple-system, "Segoe UI", sans-serif; white-space: nowrap; }
|
|
51
|
+
button.follow-bar { cursor: pointer; }
|
|
52
|
+
.follow-dot { width: 8px; height: 8px; border-radius: 50%; background: #e11d48; box-shadow: 0 0 0 3px rgba(225, 29, 72, 0.25); }
|
|
53
|
+
`
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Follow the presenter on `stage`. `onStagePosition` hears the positions the
|
|
57
|
+
* stage room carries; it must be called before the room is listened to.
|
|
58
|
+
* `words` are the labels: live, back, waiting.
|
|
59
|
+
*/
|
|
60
|
+
export function startFollowing(stage, { onStagePosition, words }) {
|
|
61
|
+
const follower = createFollower()
|
|
62
|
+
const style = document.createElement('style')
|
|
63
|
+
style.textContent = BAR_CSS
|
|
64
|
+
document.head.appendChild(style)
|
|
65
|
+
let bar = null
|
|
66
|
+
|
|
67
|
+
function update() {
|
|
68
|
+
const { presenter, live } = follower.state
|
|
69
|
+
const next = !presenter ? document.createElement('div') : live ? document.createElement('div') : document.createElement('button')
|
|
70
|
+
next.className = 'follow-bar'
|
|
71
|
+
if (!presenter) next.textContent = words.waiting
|
|
72
|
+
else if (live) { const dot = document.createElement('span'); dot.className = 'follow-dot'; next.append(dot, words.live) }
|
|
73
|
+
else { next.textContent = words.back; next.addEventListener('click', () => { go(follower.backToLive()); update() }) }
|
|
74
|
+
bar ? bar.replaceWith(next) : document.body.appendChild(next)
|
|
75
|
+
bar = next
|
|
76
|
+
}
|
|
77
|
+
// Positions set from here arrive back as 'sync' and are not the follower's own.
|
|
78
|
+
const go = position => { if (position) stage.setState({ index: position.index, step: position.step }) }
|
|
79
|
+
|
|
80
|
+
onStagePosition(position => { go(follower.presenter(position)); update() })
|
|
81
|
+
stage.addEventListener('statechange', event => {
|
|
82
|
+
if (['init', 'sync'].includes(event.detail.reason)) return
|
|
83
|
+
go(follower.moved({ index: event.detail.index, step: event.detail.step }))
|
|
84
|
+
update()
|
|
85
|
+
})
|
|
86
|
+
// Until the presenter's position arrives, the title slide.
|
|
87
|
+
go(follower.moved(stage.state))
|
|
88
|
+
update()
|
|
89
|
+
}
|
package/src/runtime/ink/bus.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// a hovering pen moves, at most every ~50 ms
|
|
13
13
|
// { type: 'zoom', key, slideId, scale, x, y } the part of the slide this
|
|
14
14
|
// window shows (stage zoom), at most every ~50 ms
|
|
15
|
-
// { type: '
|
|
15
|
+
// { type: 'controls', key, room, controls } an activity closed or opened,
|
|
16
|
+
// its right answer or results shown or hidden (components/activity.jsx)
|
|
16
17
|
// { type: 'op', op } a change of the saved ink (applyOp)
|
|
17
18
|
// Keys start with the sending window's id, so strokes never mix.
|
|
18
19
|
import { changeInk, renameSlide } from './store.js'
|
|
@@ -49,8 +50,8 @@ export function createInkBus(stage, transports) {
|
|
|
49
50
|
if (message.from <= known.points.length) known.points = [...known.points.slice(0, message.from), ...message.points]
|
|
50
51
|
incoming.set(message.key, known)
|
|
51
52
|
stage.liveStroke(message.key, known)
|
|
52
|
-
} else if (message.type === '
|
|
53
|
-
stage.dispatchEvent(new CustomEvent('
|
|
53
|
+
} else if (message.type === 'controls') {
|
|
54
|
+
stage.dispatchEvent(new CustomEvent('pollcontrols', { detail: { room: message.room, controls: message.controls } }))
|
|
54
55
|
} else if (message.type === 'zoom') {
|
|
55
56
|
if (message.slideId && message.slideId === stage._inkSlideId()) stage.setZoom(message.scale > 1 ? message : null, 'remote', { announce: false })
|
|
56
57
|
} else if (message.type === 'dot') {
|
|
@@ -99,9 +100,9 @@ export function createInkBus(stage, transports) {
|
|
|
99
100
|
dotTimer ??= setTimeout(sendDot, DOT_MS)
|
|
100
101
|
})
|
|
101
102
|
|
|
102
|
-
//
|
|
103
|
-
stage.addEventListener('
|
|
104
|
-
if (detail?.local) send({ type: '
|
|
103
|
+
// An activity's controls changed here: the others follow.
|
|
104
|
+
stage.addEventListener('pollcontrols', ({ detail }) => {
|
|
105
|
+
if (detail?.local) send({ type: 'controls', key: `${me}:controls`, room: detail.room, controls: detail.controls })
|
|
105
106
|
})
|
|
106
107
|
|
|
107
108
|
// The zoom: the latest view, not every step of a pinch.
|
package/src/runtime/ink/room.js
CHANGED
|
@@ -77,6 +77,14 @@ export function followStageRoom(stage) {
|
|
|
77
77
|
})
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Hear the position other devices announce (the follow view decides what to do with it). */
|
|
81
|
+
export function onStagePosition(listener) {
|
|
82
|
+
stateListeners.add(state => {
|
|
83
|
+
if (!state || state.screen === stageRoom().screen || !Number.isInteger(state.index)) return
|
|
84
|
+
listener({ index: state.index, step: state.step ?? -1, slideId: state.slideId })
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
/** Publish navigation made here; received positions never echo back. */
|
|
81
89
|
export function announceStagePosition(stage) {
|
|
82
90
|
const room = stageRoom()
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// that the pen moves one at a time. Changes show at once in this window and
|
|
8
8
|
// become one change (undo puts the strokes back) when the pen lifts.
|
|
9
9
|
import { slideStrokes, changeInk, onInkChange } from './store.js'
|
|
10
|
-
import { strokesInLasso, strokesBox, translateStroke, hitTest } from '../../core/ink.js'
|
|
10
|
+
import { strokesInLasso, strokesBox, translateStroke, hitTest, snapAngle } from '../../core/ink.js'
|
|
11
11
|
|
|
12
12
|
const PAD = 14 // design pixels around a selection
|
|
13
13
|
const TAP = 10 // a lasso smaller than this is a tap
|
|
@@ -119,6 +119,8 @@ export function createInkSelection(stage, controller) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// The stroke with one end point moved to `point` (its pressure kept).
|
|
122
|
-
|
|
122
|
+
// The other end stays put, and the moved one snaps to steps of 15° around it.
|
|
123
|
+
function moveEnd(stroke, index, point) {
|
|
124
|
+
const [x, y] = snapAngle(stroke.points[1 - index], point)
|
|
123
125
|
return { ...stroke, points: stroke.points.map((p, i) => i === index ? [x, y, p[2]] : p) }
|
|
124
126
|
}
|
package/src/runtime/main.jsx
CHANGED
|
@@ -2,6 +2,7 @@ import { validatePageUrl } from '../core/urls.js'
|
|
|
2
2
|
import { h, render } from 'preact'
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
4
4
|
import { marked } from 'marked'
|
|
5
|
+
import { renderCitations, onBibliography } from './citations.js'
|
|
5
6
|
import deckFileContent from 'virtual:slides'
|
|
6
7
|
import { parseSlides } from '../core/parseSlides'
|
|
7
8
|
import { validateDeck } from '../core/validateDeck'
|
|
@@ -12,16 +13,18 @@ import { S, PaletteSwatches } from './chrome.jsx'
|
|
|
12
13
|
import { SlideErrorBoundary } from './SlideErrorBoundary.jsx'
|
|
13
14
|
import { createEditorBridge } from './editorBridge.js'
|
|
14
15
|
import { ReaderView } from './ReaderView.jsx'
|
|
15
|
-
import { configureLive, announce, setLookSource, actAsPresenter, useSteering } from '../live/client.js'
|
|
16
|
+
import { configureLive, announce, setLookSource, actAsPresenter, useSteering, setSavedResults } from '../live/client.js'
|
|
16
17
|
import { registry } from './registry'
|
|
17
18
|
import { followActiveRooms } from '../live/follow.js'
|
|
18
19
|
import { createOrder } from './syncOrder.js'
|
|
19
20
|
import { fitDeck } from './fit.js'
|
|
20
21
|
import { attachInk, watchInk } from './ink/attach.js'
|
|
21
|
-
import { roomTransport, followStageRoom, announceStagePosition } from './ink/room.js'
|
|
22
|
+
import { roomTransport, followStageRoom, announceStagePosition, onStagePosition } from './ink/room.js'
|
|
23
|
+
import { startFollowing } from './follow.js'
|
|
22
24
|
import { claimPairing } from '../live/pairing.js'
|
|
23
25
|
import { strokePath } from '../core/ink.js'
|
|
24
26
|
import { inkFileName } from 'virtual:deck-ink'
|
|
27
|
+
import savedResults from 'virtual:deck-results'
|
|
25
28
|
import { roomsIn, roomsOnSlide, findRoomTag, slideTitleFor } from '../live/roomTag.js'
|
|
26
29
|
import './reader.css'
|
|
27
30
|
import { SlideRenderer, manifests } from '../layouts/renderSlide'
|
|
@@ -33,7 +36,7 @@ import './deck-stage.js'
|
|
|
33
36
|
|
|
34
37
|
const PresenterIcon = ({ name, size = 18 }) => <Icon name={name} size={size} style={{ display: 'block' }} />
|
|
35
38
|
|
|
36
|
-
// One address parameter selects the view: ?view=deck, reader, presenter or
|
|
39
|
+
// One address parameter selects the view: ?view=deck, reader, presenter, audience or follow.
|
|
37
40
|
export function requestedView(url, fallback = 'deck') {
|
|
38
41
|
return url.searchParams.get('view') ?? fallback
|
|
39
42
|
}
|
|
@@ -486,7 +489,7 @@ function PresenterView({ deckConfig, slides }) {
|
|
|
486
489
|
}}
|
|
487
490
|
dangerouslySetInnerHTML={{
|
|
488
491
|
__html: note
|
|
489
|
-
? marked.parse(note)
|
|
492
|
+
? marked.parse(renderCitations(note).markdown)
|
|
490
493
|
: '<p>No notes — add a <code>:::notes</code> block to the slide.</p>',
|
|
491
494
|
}}
|
|
492
495
|
/>
|
|
@@ -691,14 +694,17 @@ async function init() {
|
|
|
691
694
|
setDeckLanguage(deckConfig)
|
|
692
695
|
document.documentElement.lang = deckLanguage()
|
|
693
696
|
configureLive(deckConfig)
|
|
697
|
+
setSavedResults(savedResults)
|
|
694
698
|
const { slides } = parsed
|
|
695
699
|
const presenterMode = view === 'presenter'
|
|
696
700
|
const audienceMode = view === 'audience'
|
|
701
|
+
// A phone or laptop in the room following the talk: shows, never sends.
|
|
702
|
+
const followMode = view === 'follow'
|
|
697
703
|
const embedded = url.searchParams.get('embedded') === '1'
|
|
698
704
|
const readerMode = view === 'reader' && !editorMode && !embedded
|
|
699
705
|
// The deck window and the presenter's main frame draw; the audience window
|
|
700
706
|
// and the next-slide preview show what is drawn.
|
|
701
|
-
const drawHere = !audienceMode && (!embedded || url.searchParams.get('draw') === '1')
|
|
707
|
+
const drawHere = !audienceMode && !followMode && (!embedded || url.searchParams.get('draw') === '1')
|
|
702
708
|
const inkStorageKey = `mdeck-ink:${inkFileName}:${location.pathname}`
|
|
703
709
|
|
|
704
710
|
injectSpeakerNotes(slides)
|
|
@@ -748,6 +754,8 @@ async function init() {
|
|
|
748
754
|
if (event.source !== window.parent || event.origin !== window.location.origin) return
|
|
749
755
|
if (!bridge.handleMessage(event.data)) handleDeckControl(event.data?.deckControl)
|
|
750
756
|
})
|
|
757
|
+
// Saving the deck or its reference file reformats the citations.
|
|
758
|
+
onBibliography(() => bridge.refresh())
|
|
751
759
|
await bridge.render(slidesContent)
|
|
752
760
|
post({ deckEditorReady: true })
|
|
753
761
|
return
|
|
@@ -762,7 +770,7 @@ async function init() {
|
|
|
762
770
|
|
|
763
771
|
await loadTheme(deckConfig)
|
|
764
772
|
mountDeck({ deck: parsed, deckConfig })
|
|
765
|
-
if (!embedded && !audienceMode) {
|
|
773
|
+
if (!embedded && !audienceMode && !followMode) {
|
|
766
774
|
// A quiet way into the reader view for anyone who opened the file directly.
|
|
767
775
|
const entry = document.createElement('a')
|
|
768
776
|
entry.className = 'reader-entry'
|
|
@@ -801,14 +809,20 @@ async function init() {
|
|
|
801
809
|
}
|
|
802
810
|
window.addEventListener('pointerdown', onPen, true)
|
|
803
811
|
}
|
|
812
|
+
} else if (inkStage && followMode) {
|
|
813
|
+
// The presenter's slide and ink, read from the stage room; counted as a phone.
|
|
814
|
+
inkStage.setAttribute('data-follower', '')
|
|
815
|
+
startFollowing(inkStage, { onStagePosition, words: { live: t('follow.live'), back: t('follow.back'), waiting: t('follow.waiting') } })
|
|
816
|
+
watchInk(inkStage, { storageKey: inkStorageKey, transports: [roomTransport({ receive: true, view: 'phone' })] })
|
|
804
817
|
} else if (inkStage && audienceMode) {
|
|
805
818
|
// The audience window shows the presenter's zoom and a poll's answer, without their controls.
|
|
806
819
|
inkStage.setAttribute('data-follower', '')
|
|
807
820
|
watchInk(inkStage, { storageKey: inkStorageKey, transports: [roomTransport({ receive: true, view: 'audience' })] })
|
|
808
821
|
} else if (inkStage) watchInk(inkStage, { storageKey: inkStorageKey })
|
|
809
822
|
|
|
810
|
-
// A full deck or audience window is a presenter's screen; previews are
|
|
811
|
-
|
|
823
|
+
// A full deck or audience window is a presenter's screen; previews are
|
|
824
|
+
// embedded, and a follower only watches.
|
|
825
|
+
if (!embedded && !followMode) {
|
|
812
826
|
const stage = document.querySelector('deck-stage')
|
|
813
827
|
stage?.addEventListener('slidechange', event => announceSlide(parsed, event.detail.index, { initial: event.detail.reason === 'init' }))
|
|
814
828
|
if (stage) announceSlide(parsed, stage.index, { initial: true })
|
package/src/runtime/registry.jsx
CHANGED
|
@@ -5,6 +5,8 @@ import Poll from '../components/Poll.jsx'
|
|
|
5
5
|
import Question from '../components/Question.jsx'
|
|
6
6
|
import WordCloud from '../components/WordCloud.jsx'
|
|
7
7
|
import Scale from '../components/Scale.jsx'
|
|
8
|
+
import Numeric from '../components/Numeric.jsx'
|
|
9
|
+
import Bibliography from '../components/Bibliography.jsx'
|
|
8
10
|
import deckComponents from 'virtual:deck-components'
|
|
9
11
|
|
|
10
12
|
// Maps lowercase HTML tag names to Preact components.
|
|
@@ -22,5 +24,7 @@ export const registry = {
|
|
|
22
24
|
question: Question,
|
|
23
25
|
wordcloud: WordCloud,
|
|
24
26
|
scale: Scale,
|
|
27
|
+
numeric: Numeric,
|
|
28
|
+
bibliography: Bibliography,
|
|
25
29
|
...deckComponents,
|
|
26
30
|
}
|