mdeck 3.6.0 → 3.6.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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.6.1 — 2026-10-09
4
+
5
+ - `mdeck snapshot --sheet` saves one picture of the slides in a grid, each under its number and heading (`my-talk-sheet.png`): a whole deck at a glance, for you or an assistant. It works with `--slide` and `--dark`.
6
+ - Fixed: pictures and PDFs could show a slide in its fallback font. mdeck now waits for the theme's font stylesheets and for every face the slides use before it takes a picture, renders a PDF or measures a slide, so `mdeck snapshot`, `mdeck pdf`, `mdeck check --render`, the theme check and the theme repository's previews show the theme's own fonts. Without a network it does not wait.
7
+
3
8
  ## 3.6.0 — 2026-10-09
4
9
 
5
10
  - A theme can say what it is for and how to write for it: a `[guide]` in its `extension.toml` with `suits` and `avoid` (short phrases) and `writing` (a few sentences). `mdeck list` and `mdeck themes search` show what each theme suits, search finds themes by it (`mdeck themes search photo`), and `mdeck list themes --json` and the theme repository's catalogue carry the whole guide. The slide-writing skill chooses a theme by its guide and follows its writing advice, as the theme author's advice and never as instructions. The six built-in themes have guides. A theme with a guide needs this version of mdeck.
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  <p align="center">
2
- <img src="assets/logo/logo-light.svg" alt="mdeck" width="280">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo/logo-dark.svg">
4
+ <img src="assets/logo/logo-light.svg" alt="mdeck" width="280">
5
+ </picture>
3
6
  </p>
4
7
 
5
8
  <p align="center"><b>Hand your slides, papers and notes to an AI assistant. Get a clean deck to present in the browser. Share it as one HTML file or a PDF.</b></p>
@@ -141,9 +141,10 @@ The check takes a few seconds and needs Chrome or Chromium; set `MDECK_CHROME` t
141
141
  ```sh
142
142
  mdeck snapshot my-talk.md
143
143
  mdeck snapshot my-talk.md --slide 3,5-7 --dark
144
+ mdeck snapshot my-talk.md --sheet
144
145
  ```
145
146
 
146
- This saves each slide, with all steps revealed, as a 1280×720 PNG file in `.mdeck-snapshots/` beside the deck (`-o` for another folder), named `my-talk-03.png` and so on, and prints the paths. `--slide` picks slides by number; `--dark` and `--light` show that appearance instead of the deck's own. An AI assistant that reads pictures can use them to look at what it wrote. If you keep your slides in Git, add `.mdeck-snapshots/` to your `.gitignore`.
147
+ This saves each slide, with all steps revealed, as a 1280×720 PNG file in `.mdeck-snapshots/` beside the deck (`-o` for another folder), named `my-talk-03.png` and so on, and prints the paths. `--slide` picks slides by number; `--dark` and `--light` show that appearance instead of the deck's own. `--sheet` saves one picture instead, `my-talk-sheet.png`: the slides in a grid, each under its number and heading, to see a whole deck at a glance. An AI assistant that reads pictures can use them to look at what it wrote. If you keep your slides in Git, add `.mdeck-snapshots/` to your `.gitignore`.
147
148
 
148
149
  ## Layouts, themes and palettes
149
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdeck",
3
- "version": "3.6.0",
3
+ "version": "3.6.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",
@@ -136,7 +136,7 @@ mdeck check <deck>.md --render
136
136
 
137
137
  Fix anything it reports. `--render` builds the deck and shows every slide in Chrome with all steps revealed; it names each slide whose content does not fit, whose text is cut off at the edge or hard to read, with the line the slide starts on. Shorten or split those slides, or pick another layout, and check again. If Chrome is missing, run the check without `--render` and tell the user.
138
138
 
139
- To see slides yourself, if you can read pictures, run `mdeck snapshot <deck>.md --slide 3,5-7`: it saves PNG files to `.mdeck-snapshots/` beside the deck and prints their paths (`--dark` shows the dark appearance). Look at slides you were unsure about, such as diagrams, pictures and columns. When you worked from source material, list what you left out or moved to notes, and anything you added that is not in the material. Finish by telling the user the two commands they will want next: `mdeck run <deck>.md` for the launch page (preview, presenting, editor, builds and checks), and `mdeck send <deck>.md` for a file to send around (a reader view with a PDF; notes are removed unless `--notes` is added).
139
+ To see slides yourself, if you can read pictures, run `mdeck snapshot <deck>.md --sheet`: it saves one picture of every slide in a grid, each under its number and heading, to `.mdeck-snapshots/` beside the deck and prints its path. For a closer look, `mdeck snapshot <deck>.md --slide 3,5-7` saves those slides as separate PNG files (`--dark` shows the dark appearance; `--sheet` works with `--slide` too). Look at slides you were unsure about, such as diagrams, pictures and columns. When you worked from source material, list what you left out or moved to notes, and anything you added that is not in the material. Finish by telling the user the two commands they will want next: `mdeck run <deck>.md` for the launch page (preview, presenting, editor, builds and checks), and `mdeck send <deck>.md` for a file to send around (a reader view with a PDF; notes are removed unless `--notes` is added).
140
140
 
141
141
  ## 5. A new look: themes and palettes
142
142
 
@@ -32,6 +32,32 @@ export function serveDirectory(root) {
32
32
  })
33
33
  }
34
34
 
35
+ // Runs in a deck page once it is up: the theme's fonts, loaded before any
36
+ // picture or measurement. document.fonts.status is already 'loaded' before
37
+ // a font stylesheet arrives (nothing is pending yet), and a face is only
38
+ // fetched once text uses it, so a slide could be captured, or measured, in
39
+ // its fallback font. This waits for every stylesheet to load or fail, then
40
+ // asks for each face that text on any slide (hidden ones too) uses, and
41
+ // waits for them and two frames. A font that cannot be fetched (offline)
42
+ // does not hold it up for longer than `limit` milliseconds.
43
+ export const FONTS_READY = `(async (limit = 8000) => {
44
+ const timeout = new Promise(done => setTimeout(done, limit))
45
+ // A stylesheet that already failed has no sheet and fires nothing more; its
46
+ // request is finished all the same.
47
+ const sheets = [...document.querySelectorAll('link[rel="stylesheet"]')].filter(link => !link.sheet && !performance.getEntriesByName(link.href).length)
48
+ .map(link => new Promise(done => { link.addEventListener('load', done, { once: true }); link.addEventListener('error', done, { once: true }) }))
49
+ await Promise.race([Promise.all(sheets), timeout])
50
+ const faces = new Set()
51
+ for (const element of document.querySelectorAll('deck-stage *')) {
52
+ if (![...element.childNodes].some(node => node.nodeType === 3 && node.textContent.trim())) continue
53
+ const style = getComputedStyle(element)
54
+ faces.add(style.fontStyle + ' ' + style.fontWeight + ' 32px ' + style.fontFamily)
55
+ }
56
+ await Promise.race([Promise.all([...faces].map(face => document.fonts.load(face).catch(() => null))).then(() => document.fonts.ready), timeout])
57
+ await new Promise(done => requestAnimationFrame(() => requestAnimationFrame(done)))
58
+ return true
59
+ })()`
60
+
35
61
  // Starts Chrome and a static server for `dir`. `open(path)` returns a page
36
62
  // with evaluate(), send() and waitFor(); call close() when done.
37
63
  export async function launchChrome({ dir, chrome = findChrome(), timeout = 90000 } = {}) {
@@ -110,8 +136,13 @@ export async function launchChrome({ dir, chrome = findChrome(), timeout = 90000
110
136
  }
111
137
  return false
112
138
  },
113
- // Waits until the deck stage exists, fonts are loaded and images decoded.
114
- waitForDeck: () => page.waitFor("document.querySelector('deck-stage')?.length > 0 && document.fonts.status === 'loaded' && [...document.images].every(image => image.complete)"),
139
+ // Waits until the deck stage exists, images are decoded and the
140
+ // theme's fonts are loaded (FONTS_READY).
141
+ async waitForDeck() {
142
+ const ready = await page.waitFor("document.querySelector('deck-stage')?.length > 0 && document.fonts.status === 'loaded' && [...document.images].every(image => image.complete)")
143
+ if (ready) await page.evaluate(FONTS_READY)
144
+ return ready
145
+ },
115
146
  async screenshot({ width = 1600, height = 900, scale = 1, format = 'png' } = {}) {
116
147
  await send('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: scale, mobile: false }, sessionId)
117
148
  await delay(150)
@@ -79,20 +79,51 @@ export function renderCheck(slidesPath) {
79
79
  }
80
80
 
81
81
  // Pictures of slides (numbers from 1; all when none are given) as PNG files
82
- // in `outDir`, named <deck>-<number>.png. Returns the files written.
83
- export async function snapshot(slidesPath, { slides = [], outDir, width = 1280, height = 720, appearance = '' } = {}) {
82
+ // in `outDir`, named <deck>-<number>.png. With `sheet`, one picture instead,
83
+ // <deck>-sheet.png: the slides in a grid, each under its number and heading,
84
+ // so a person or an assistant sees the whole deck at once. Returns the files
85
+ // written.
86
+ export async function snapshot(slidesPath, { slides = [], outDir, width = 1280, height = 720, appearance = '', sheet = false } = {}) {
84
87
  const name = basename(slidesPath).replace(/\.md$/i, '')
88
+ const labels = sheet ? slideLabels(readFileSync(slidesPath, 'utf8')) : []
85
89
  mkdirSync(outDir, { recursive: true })
86
90
  return withDeckPage(slidesPath, { appearance, width, height }, async ({ page, count, show }) => {
87
91
  const wanted = slides.length ? slides : Array.from({ length: count }, (_, i) => i + 1)
88
- const files = []
92
+ const files = [], shots = []
89
93
  for (const number of wanted) {
90
94
  if (number < 1 || number > count) throw new Error(`There is no slide ${number}; the deck has ${count}`)
91
95
  await show(number - 1)
96
+ const picture = await page.screenshot({ width, height })
97
+ if (sheet) { shots.push({ number, heading: labels[number - 1]?.heading ?? '', picture }); continue }
92
98
  const file = resolve(outDir, `${name}-${String(number).padStart(2, '0')}.png`)
93
- writeFileSync(file, await page.screenshot({ width, height }))
99
+ writeFileSync(file, picture)
94
100
  files.push(file)
95
101
  }
96
- return files
102
+ if (!sheet) return files
103
+ const file = resolve(outDir, `${name}-sheet.png`)
104
+ writeFileSync(file, await contactSheet(page, shots))
105
+ return [file]
97
106
  })
98
107
  }
108
+
109
+ // The slides' pictures in a grid on a dark page, each at half size under its
110
+ // number and heading; drawn by the page that took them, as its last task.
111
+ const CELL = { width: 640, height: 360, label: 30, gap: 24 }
112
+ async function contactSheet(page, shots) {
113
+ const columns = shots.length <= 1 ? 1 : shots.length <= 4 ? 2 : shots.length <= 9 ? 3 : 4
114
+ const rows = Math.ceil(shots.length / columns)
115
+ const width = columns * CELL.width + (columns + 1) * CELL.gap
116
+ const height = rows * (CELL.height + CELL.label) + (rows + 1) * CELL.gap
117
+ const escape = text => String(text).replace(/[&<>"]/g, ch => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[ch])
118
+ const cells = shots.map(({ number, heading, picture }) => `<figure><figcaption><b>${String(number).padStart(2, '0')}</b> ${escape(heading)}</figcaption><img src="data:image/png;base64,${picture.toString('base64')}"></figure>`).join('')
119
+ const html = `<!doctype html><meta charset="utf-8"><style>
120
+ html, body { margin: 0; background: #1c1c1c; }
121
+ main { display: grid; grid-template-columns: repeat(${columns}, ${CELL.width}px); gap: ${CELL.gap}px; padding: ${CELL.gap}px; }
122
+ figure { margin: 0; }
123
+ figcaption { height: ${CELL.label}px; font: 15px/1.2 system-ui, sans-serif; color: #c8c8c8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
124
+ figcaption b { color: #fff; margin-right: 6px; font-variant-numeric: tabular-nums; }
125
+ img { display: block; width: ${CELL.width}px; height: ${CELL.height}px; outline: 1px solid #3a3a3a; }
126
+ </style><main>${cells}</main>`
127
+ await page.evaluate(`document.open(); document.write(${JSON.stringify(html)}); document.close(); Promise.all([...document.images].map(image => image.decode())).then(() => true)`)
128
+ return page.screenshot({ width, height })
129
+ }
package/src/cli/args.js CHANGED
@@ -14,7 +14,7 @@ const commands = {
14
14
  pdf: { ...output, '--no-polls': flag, max: 1 },
15
15
  preview: { ...common, max: 1 },
16
16
  check: { '--strict': flag, '--render': flag, max: 1 },
17
- snapshot: { '--output': value, '-o': '--output', '--slide': value, '--dark': flag, '--light': flag, max: 1 },
17
+ snapshot: { '--output': value, '-o': '--output', '--slide': value, '--dark': flag, '--light': flag, '--sheet': flag, max: 1 },
18
18
  list: { '--json': flag, max: 2 },
19
19
  starter: { max: 2 },
20
20
  docs: { ...common, '--build': flag, max: 1 },
package/src/cli/index.js CHANGED
@@ -343,6 +343,7 @@ const HELP = `
343
343
  does not fit, text cut off or hard to read, errors on the page
344
344
  ${c.green}mdeck snapshot${c.reset} [slides.md] Pictures of slides as PNG files (default: .mdeck-snapshots/)
345
345
  --slide <numbers> only these slides: 3, 3,5 or 2-4
346
+ --sheet one picture of them all in a grid, each under its number and heading
346
347
  --dark | --light in this appearance instead of the deck's own -o <folder>
347
348
  ${c.green}mdeck results${c.reset} [slides.md] [-o answers.csv] The answers kept in the talk, as CSV (room, phone, time, answer)
348
349
  ${c.green}mdeck list${c.reset} [layouts|themes|palettes] [slides.md] [--json]
@@ -503,7 +504,7 @@ if (command === 'new') {
503
504
  const outDir = resolve(outputOption() ?? resolve(dirname(resolve(input)), '.mdeck-snapshots'))
504
505
  const appearance = hasFlag('--dark') ? 'dark' : hasFlag('--light') ? 'light' : ''
505
506
  const { snapshot } = await import('../build/renderCheck.js')
506
- for (const file of await snapshot(input, { slides, outDir, appearance })) console.log(relative(process.cwd(), file))
507
+ for (const file of await snapshot(input, { slides, outDir, appearance, sheet: hasFlag('--sheet') })) console.log(relative(process.cwd(), file))
507
508
  } catch (error) { err(error.message); process.exitCode = 1 }
508
509
 
509
510
  } else if (command === 'list') {