mdeck 3.12.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.12.1 — 2026-10-12
4
+
5
+ - Fixed: on an iPhone the reaction buttons sat below the visible part of the phones' page, behind Safari's bar. The page now fits the visible height. Update your poll server for this one.
6
+ - `mdeck --version` (or `-v`) shows which mdeck this is.
7
+ - Fixed: with polls on your own server, the presenter view counted no phones: they join that server, while the presenter view's room stays on your computer. `mdeck run` now asks the server how many phones joined.
8
+ - Shorter status lines in the presenter view: "Poll server needs an update to 3.12.1.", "Phones follow another screen. Change slide to take them back."
9
+
3
10
  ## 3.12.0 — 2026-10-12
4
11
 
5
12
  - In a word cloud each phone counts with one word: a new one replaces its earlier word, and the phone says which it sent. Before, one phone could fill the cloud.
@@ -27,6 +27,7 @@ For how the commands, the views and the servers fit together, see [Views, comman
27
27
  | `mdeck list`, `mdeck starter` | Look up layouts, themes and palettes |
28
28
  | `mdeck docs` | Open this documentation |
29
29
  | `mdeck --help` | Show a short list of commands |
30
+ | `mdeck --version` | Show which version of mdeck you have |
30
31
 
31
32
  ## The launch page
32
33
 
@@ -2,54 +2,24 @@
2
2
  "version": 1,
3
3
  "rooms": {
4
4
  "tour": {
5
- "savedAt": "2026-10-12T21:17:14.623Z",
5
+ "savedAt": "2026-10-12T21:42:08.047Z",
6
6
  "closed": false,
7
7
  "answers": [
8
8
  {
9
9
  "phone": 1,
10
- "at": "2026-10-12T21:01:59.353Z",
11
- "value": "Umfragen"
10
+ "at": "2026-10-12T21:37:57.968Z",
11
+ "value": "Zeichnen"
12
12
  }
13
13
  ]
14
14
  },
15
15
  "mood": {
16
- "savedAt": "2026-10-12T21:18:16.405Z",
16
+ "savedAt": "2026-10-12T21:42:08.391Z",
17
17
  "closed": false,
18
18
  "answers": [
19
19
  {
20
20
  "phone": 1,
21
- "at": "2026-10-12T21:17:34.435Z",
22
- "value": "Bonj"
23
- },
24
- {
25
- "phone": 1,
26
- "at": "2026-10-12T21:17:38.153Z",
27
- "value": "Banjf"
28
- },
29
- {
30
- "phone": 1,
31
- "at": "2026-10-12T21:17:43.867Z",
32
- "value": "Hallo Herr Doktor Müller"
33
- },
34
- {
35
- "phone": 1,
36
- "at": "2026-10-12T21:17:49.791Z",
37
- "value": "Hallo Herr"
38
- },
39
- {
40
- "phone": 1,
41
- "at": "2026-10-12T21:17:57.260Z",
42
- "value": "Hallo Herr Doktor Müller"
43
- },
44
- {
45
- "phone": 1,
46
- "at": "2026-10-12T21:18:03.177Z",
47
- "value": "Ja 👍 und die sind"
48
- },
49
- {
50
- "phone": 1,
51
- "at": "2026-10-12T21:18:15.378Z",
52
- "value": "Hallo Herr Müller"
21
+ "at": "2026-10-12T21:38:02.812Z",
22
+ "value": "Gggghh"
53
23
  }
54
24
  ]
55
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdeck",
3
- "version": "3.12.0",
3
+ "version": "3.12.1",
4
4
  "type": "module",
5
5
  "description": "Write slides as a plain text file, present them in the browser, share them as one HTML file or PDF.",
6
6
  "license": "MIT",
package/src/cli/index.js CHANGED
@@ -391,6 +391,7 @@ const HELP = `
391
391
  through it (MDECK_SERVER_KEY); put a web server in front
392
392
 
393
393
  Commands that take ${c.bold}slides.md${c.reset} use the one in the current folder when you leave it out.
394
+ ${c.green}mdeck --version${c.reset} shows which mdeck this is.
394
395
 
395
396
  ${c.dim}Install the${c.reset} ${c.bold}mdeck${c.reset} ${c.dim}command globally:${c.reset}
396
397
  npm link
@@ -402,6 +403,11 @@ const HELP = `
402
403
  // ── Argument parsing ──────────────────────────────────────────────────────────
403
404
  const [command, ...argv] = process.argv.slice(2)
404
405
 
406
+ if (command === '--version' || command === '-v' || command === 'version') {
407
+ console.log(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version)
408
+ process.exit(0)
409
+ }
410
+
405
411
  if (!command || command === '--help' || command === '-h') {
406
412
  console.log(HELP)
407
413
  process.exit(0)
@@ -6,7 +6,9 @@
6
6
  }
7
7
  * { box-sizing: border-box; }
8
8
  html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--ink); }
9
- body { min-height: 100vh; display: flex; flex-direction: column; font: 18px/1.45 var(--font-body); -webkit-font-smoothing: antialiased; }
9
+ /* The visible height (dvh): on a phone, 100vh is taller than what shows beside
10
+ the browser's bars, and the reactions at the bottom slid out of sight. */
11
+ body { min-height: 100vh; min-height: 100dvh; padding-bottom: env(safe-area-inset-bottom); box-sizing: border-box; display: flex; flex-direction: column; font: 18px/1.45 var(--font-body); -webkit-font-smoothing: antialiased; }
10
12
  #answer { flex: 1; display: flex; flex-direction: column; }
11
13
  .answer { flex: 1; width: 100%; max-width: 520px; margin: 0 auto; padding: 32px 20px; }
12
14
  .answer h1 {
package/src/live/rooms.js CHANGED
@@ -43,13 +43,23 @@ export function createRooms({ maxMessages = 5000, maxInk = 5000, maxRooms = 500,
43
43
  const broadcast = (room, event) => { for (const listener of room.listeners) listener(event) }
44
44
  // A session is a room and the rooms named after it (`<code>.stage`, …).
45
45
  const sessionRooms = id => { const session = id.split('.')[0]; return [...rooms].filter(([name]) => name === session || name.startsWith(`${session}.`)).map(([, room]) => room) }
46
+ // Phones connected elsewhere, by session: mdeck run keeps the stage room
47
+ // here but the phones join a standalone server (src/live/server.js).
48
+ const awayPhones = new Map()
46
49
  const presence = id => {
47
50
  const present = sessionRooms(id).flatMap(room => [...room.present.values()])
48
51
  const views = present.filter(entry => entry.view !== 'phone').sort((a, b) => VIEWS.indexOf(a.view) - VIEWS.indexOf(b.view))
49
- return { views, phones: present.length - views.length }
52
+ return { views, phones: present.length - views.length + (awayPhones.get(id.split('.')[0]) ?? 0) }
50
53
  }
51
54
  const announcePresence = id => { const event = { type: 'presence', presence: presence(id) }; for (const room of sessionRooms(id)) broadcast(room, event) }
52
55
  return {
56
+ /** Phones of session `id` that joined another server; announced when the number changes. */
57
+ setAwayPhones(id, n) {
58
+ const session = id.split('.')[0]
59
+ if ((awayPhones.get(session) ?? 0) === n) return
60
+ awayPhones.set(session, n)
61
+ announcePresence(session)
62
+ },
53
63
  post(id, { from = null, data }) {
54
64
  const room = get(id)
55
65
  // Reactions (src/core/reactions.js) are passed on and forgotten.
@@ -14,11 +14,25 @@ export function livePlugin({ pairing = null, upstream = null, key = null, sessio
14
14
  return {
15
15
  name: 'vite-plugin-mdeck-live',
16
16
  configureServer(server) {
17
+ const rooms = createRooms()
17
18
  const handler = liveHandler({
18
- rooms: createRooms(),
19
+ rooms,
19
20
  canReset: (request, room) => isAllowedRequest(request) || (!!pairing?.allows(request) && (room == null || room === session() || room.startsWith(`${session()}.`))),
20
21
  info: () => ({ network: server.resolvedUrls?.network?.[0] ?? null }),
21
22
  })
23
+ // With polls on a standalone server, the phones join there: its count
24
+ // joins the local stage room's, so the presenter sees them.
25
+ const awayTimer = setInterval(async () => {
26
+ const address = typeof upstream === 'function' ? upstream() : upstream
27
+ const code = session()
28
+ if (!address || !code) return
29
+ try {
30
+ const response = await fetch(`${address.replace(/\/+$/, '')}/rooms/${encodeURIComponent(code)}.stage/presence`, { signal: AbortSignal.timeout(3000) })
31
+ if (response.ok) rooms.setAwayPhones(code, Number((await response.json()).phones) || 0)
32
+ } catch {}
33
+ }, 4000)
34
+ awayTimer.unref?.()
35
+ server.httpServer?.once('close', () => clearInterval(awayTimer))
22
36
  server.middlewares.use('/__mdeck/live', (request, response, next) => {
23
37
  const address = typeof upstream === 'function' ? upstream() : upstream
24
38
  // The stage room (position and live ink between the presenter's
@@ -99,9 +99,9 @@ const PAIR = {
99
99
  function Polls({ live, recheck }) {
100
100
  const [copied, setCopied] = useState(false)
101
101
  const copy = () => navigator.clipboard?.writeText(live.joinUrl).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500) }, () => {})
102
- const problem = live.server && !live.reachable ? `The poll server does not answer${live.error ? ` (${live.error})` : ''}.`
103
- : !live.joinUrl ? <>Phones cannot reach this computer: start with <code>--network</code>, or set <code>server</code> in the deck.</>
104
- : live.server && live.key && !live.keyAccepted ? 'The poll server does not accept this key: phones do not follow.'
102
+ const problem = live.server && !live.reachable ? 'Poll server not answering.'
103
+ : !live.joinUrl ? <>Phones can’t reach this computer: use <code>--network</code>.</>
104
+ : live.server && live.key && !live.keyAccepted ? 'Poll server refuses the key.'
105
105
  : null
106
106
  // A server on an older mdeck answers, but lacks what is new (reactions came with 3.12).
107
107
  const outdated = !problem && live.server && live.serverOutdated
@@ -111,8 +111,8 @@ function Polls({ live, recheck }) {
111
111
  <a href={live.joinUrl} target="_blank" rel="noopener" title="The join link for phones" style={{ color: '#ccc', textDecoration: 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{live.joinUrl.replace(/^https?:\/\//, '')}</a>
112
112
  <ToolButton class="presenter-copy-join" icon={copied ? 'check' : 'copy'} label={copied ? 'Copied' : 'Copy the join link'} size={15} style={{ minWidth: '30px', height: '30px', padding: 0, flexShrink: 0 }} onClick={copy} />
113
113
  </span>}
114
- {outdated && <span class="presenter-server-outdated" style={{ color: '#f5a524' }}>
115
- Your poll server runs {live.serverVersion ? `mdeck ${live.serverVersion}` : 'an older mdeck'}, this computer {live.version}: what is new, such as reactions, may not reach the phones. Update it with <code>npm install -g mdeck</code> and restart <code>mdeck server</code>.
114
+ {outdated && <span class="presenter-server-outdated" style={{ color: '#f5a524' }} title={`Server ${live.serverVersion ?? 'older'}, here ${live.version}: npm install -g mdeck there, then restart mdeck server`}>
115
+ Poll server needs an update to {live.version}.
116
116
  </span>}
117
117
  {problem && <span style={{ display: 'flex', gap: '8px', alignItems: 'center', color: '#f28b82' }}>
118
118
  <span style={{ flex: 1 }}>{problem}</span>
@@ -148,10 +148,10 @@ function announceSlide(deck, index, { initial = false } = {}) {
148
148
  // Whether this presenter screen moves the phones along, shown only for decks
149
149
  // with activities: a failure used to be silent.
150
150
  const STEERING = {
151
- 'no-code': 'Phones do not follow: this browser has no server key. Open the presenter view once with ?serverkey=… (the launch page has a link).',
152
- 'wrong-code': "Phones do not follow: the server does not accept this browser's presenter code.",
153
- unreachable: 'Phones do not follow: the server does not answer.',
154
- 'other-screen': 'Phones follow another screen, where this presentation was moved on more recently. Change the slide here to take them back.',
151
+ 'no-code': 'Phones don’t follow: no server key here.',
152
+ 'wrong-code': 'Phones don’t follow: key refused.',
153
+ unreachable: 'Phones don’t follow: server not answering.',
154
+ 'other-screen': 'Phones follow another screen. Change slide to take them back.',
155
155
  }
156
156
 
157
157
  function injectSpeakerNotes(slides) {