mdeck 2.3.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.
Files changed (268) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/LICENSE +21 -0
  3. package/README.md +156 -0
  4. package/assets/base.css +196 -0
  5. package/assets/extensions/layouts/chapter/extension.toml +26 -0
  6. package/assets/extensions/layouts/chapter/layout.jsx +21 -0
  7. package/assets/extensions/layouts/chapter/starter.md +8 -0
  8. package/assets/extensions/layouts/focus/extension.toml +8 -0
  9. package/assets/extensions/layouts/focus/layout.jsx +10 -0
  10. package/assets/extensions/layouts/focus/starter.md +4 -0
  11. package/assets/extensions/layouts/full-bleed-image/extension.toml +31 -0
  12. package/assets/extensions/layouts/full-bleed-image/layout.jsx +19 -0
  13. package/assets/extensions/layouts/full-bleed-image/starter.md +6 -0
  14. package/assets/extensions/layouts/generic/extension.toml +8 -0
  15. package/assets/extensions/layouts/generic/layout.jsx +8 -0
  16. package/assets/extensions/layouts/generic/starter.md +4 -0
  17. package/assets/extensions/layouts/image-text/extension.toml +25 -0
  18. package/assets/extensions/layouts/image-text/layout.jsx +17 -0
  19. package/assets/extensions/layouts/image-text/starter.md +6 -0
  20. package/assets/extensions/layouts/split/extension.toml +25 -0
  21. package/assets/extensions/layouts/split/layout.jsx +19 -0
  22. package/assets/extensions/layouts/split/starter.md +14 -0
  23. package/assets/extensions/layouts/title/extension.toml +26 -0
  24. package/assets/extensions/layouts/title/layout.jsx +14 -0
  25. package/assets/extensions/layouts/title/starter.md +5 -0
  26. package/assets/extensions/palettes/brand/extension.toml +29 -0
  27. package/assets/extensions/palettes/cobalt/extension.toml +27 -0
  28. package/assets/extensions/palettes/ember/extension.toml +27 -0
  29. package/assets/extensions/palettes/forest/extension.toml +27 -0
  30. package/assets/extensions/palettes/graphite/extension.toml +27 -0
  31. package/assets/extensions/palettes/lagoon/extension.toml +27 -0
  32. package/assets/extensions/palettes/neon/extension.toml +27 -0
  33. package/assets/extensions/palettes/nordic/extension.toml +27 -0
  34. package/assets/extensions/palettes/pastel/extension.toml +30 -0
  35. package/assets/extensions/palettes/phosphor/extension.toml +27 -0
  36. package/assets/extensions/palettes/swiss/extension.toml +27 -0
  37. package/assets/extensions/palettes/terra/extension.toml +27 -0
  38. package/assets/extensions/themes/academic/extension.toml +53 -0
  39. package/assets/extensions/themes/academic/styles.css +631 -0
  40. package/assets/extensions/themes/aurora/extension.toml +37 -0
  41. package/assets/extensions/themes/aurora/styles.css +644 -0
  42. package/assets/extensions/themes/duet/extension.toml +46 -0
  43. package/assets/extensions/themes/duet/styles.css +573 -0
  44. package/assets/extensions/themes/editorial/extension.toml +46 -0
  45. package/assets/extensions/themes/editorial/styles.css +574 -0
  46. package/assets/extensions/themes/fhnw/extension.toml +61 -0
  47. package/assets/extensions/themes/fhnw/styles.css +604 -0
  48. package/assets/extensions/themes/neue/extension.toml +46 -0
  49. package/assets/extensions/themes/neue/styles.css +489 -0
  50. package/assets/extensions/themes/sketch/extension.toml +48 -0
  51. package/assets/extensions/themes/sketch/styles.css +633 -0
  52. package/assets/extensions/themes/terminal/extension.toml +38 -0
  53. package/assets/extensions/themes/terminal/styles.css +550 -0
  54. package/assets/logo/avatar.png +0 -0
  55. package/assets/logo/logo-dark.svg +10 -0
  56. package/assets/logo/logo-light.svg +10 -0
  57. package/assets/logo/mark.svg +15 -0
  58. package/bin/mdeck.js +2 -0
  59. package/docs/README.md +8 -0
  60. package/docs/reference/authoring.md +795 -0
  61. package/docs/reference/claude-skill.md +96 -0
  62. package/docs/reference/extensions.md +250 -0
  63. package/docs/reference/layouts.md +153 -0
  64. package/docs/reference/palettes.md +106 -0
  65. package/docs/reference/structured-slides.md +120 -0
  66. package/docs/reference/themes.md +133 -0
  67. package/docs/site/assets/favicon.svg +15 -0
  68. package/docs/site/assets/github-logo.svg +1 -0
  69. package/docs/site/assets/site.css +162 -0
  70. package/docs/site/assets/site.js +123 -0
  71. package/docs/site/build.js +148 -0
  72. package/docs/site/content/appearance.md +203 -0
  73. package/docs/site/content/audience.md +178 -0
  74. package/docs/site/content/commands.md +153 -0
  75. package/docs/site/content/components.md +134 -0
  76. package/docs/site/content/drawing.md +72 -0
  77. package/docs/site/content/editing.md +59 -0
  78. package/docs/site/content/getting-started.md +103 -0
  79. package/docs/site/content/layouts.md +126 -0
  80. package/docs/site/content/media.md +107 -0
  81. package/docs/site/content/more-content.md +130 -0
  82. package/docs/site/content/presenting.md +93 -0
  83. package/docs/site/content/reusable-layouts.md +74 -0
  84. package/docs/site/content/sharing.md +88 -0
  85. package/docs/site/content/theme-authoring.md +144 -0
  86. package/docs/site/content/theme-repository.md +76 -0
  87. package/docs/site/content/troubleshooting.md +92 -0
  88. package/docs/site/content/views.md +56 -0
  89. package/docs/site/content/welcome.md +71 -0
  90. package/docs/site/content/writing.md +121 -0
  91. package/docs/site/examples/first-talk.md +88 -0
  92. package/docs/site/examples/notebook.svg +58 -0
  93. package/docs/site/images/themes/academic-dark.webp +0 -0
  94. package/docs/site/images/themes/academic-light.webp +0 -0
  95. package/docs/site/images/themes/academic.webp +0 -0
  96. package/docs/site/images/themes/aurora-dark.webp +0 -0
  97. package/docs/site/images/themes/aurora-light.webp +0 -0
  98. package/docs/site/images/themes/aurora.webp +0 -0
  99. package/docs/site/images/themes/duet-dark.webp +0 -0
  100. package/docs/site/images/themes/duet-light.webp +0 -0
  101. package/docs/site/images/themes/duet.webp +0 -0
  102. package/docs/site/images/themes/editorial-dark.webp +0 -0
  103. package/docs/site/images/themes/editorial-light.webp +0 -0
  104. package/docs/site/images/themes/editorial.webp +0 -0
  105. package/docs/site/images/themes/fhnw.webp +0 -0
  106. package/docs/site/images/themes/neue-dark.webp +0 -0
  107. package/docs/site/images/themes/neue-light.webp +0 -0
  108. package/docs/site/images/themes/neue.webp +0 -0
  109. package/docs/site/images/themes/sketch-dark.webp +0 -0
  110. package/docs/site/images/themes/sketch-light.webp +0 -0
  111. package/docs/site/images/themes/sketch.webp +0 -0
  112. package/docs/site/images/themes/terminal-dark.webp +0 -0
  113. package/docs/site/images/themes/terminal-light.webp +0 -0
  114. package/docs/site/images/themes/terminal.webp +0 -0
  115. package/docs/site/package.json +9 -0
  116. package/docs/site/pages.js +27 -0
  117. package/docs/site/server.js +58 -0
  118. package/examples/custom-layouts/extensions/comparison/extension.toml +35 -0
  119. package/examples/custom-layouts/extensions/comparison/layout.jsx +15 -0
  120. package/examples/custom-layouts/extensions/comparison/starter.md +17 -0
  121. package/examples/custom-layouts/extensions/comparison/styles.css +4 -0
  122. package/examples/custom-layouts/slides.md +60 -0
  123. package/examples/poll/slides.md +127 -0
  124. package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +19 -0
  125. package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +6 -0
  126. package/examples/python/slides.drawings.json +6 -0
  127. package/examples/python/slides.md +493 -0
  128. package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +10 -0
  129. package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +13 -0
  130. package/examples/showcase/img/image.jpg +0 -0
  131. package/examples/showcase/img/mdeck.svg +66 -0
  132. package/examples/showcase/slides.drawings.json +11 -0
  133. package/examples/showcase/slides.md +427 -0
  134. package/package.json +75 -0
  135. package/skills/write-slides/SKILL.md +144 -0
  136. package/src/build/check.js +49 -0
  137. package/src/build/chrome.js +130 -0
  138. package/src/build/components.js +44 -0
  139. package/src/build/config.js +61 -0
  140. package/src/build/drawings.js +14 -0
  141. package/src/build/editorPlugin.js +272 -0
  142. package/src/build/editorServer.js +36 -0
  143. package/src/build/fonts.js +84 -0
  144. package/src/build/homePlugin.js +208 -0
  145. package/src/build/inkPlugin.js +192 -0
  146. package/src/build/ownWrites.js +18 -0
  147. package/src/build/pairing.js +81 -0
  148. package/src/build/pdf.js +34 -0
  149. package/src/build/relayAccess.js +85 -0
  150. package/src/build/slidesPlugin.js +371 -0
  151. package/src/build/themeCheck.js +151 -0
  152. package/src/build/tunnelClient.js +184 -0
  153. package/src/cli/args.js +76 -0
  154. package/src/cli/index.js +758 -0
  155. package/src/cli/migrate.js +123 -0
  156. package/src/cli/skill.js +40 -0
  157. package/src/cli/themes.js +140 -0
  158. package/src/components/CodeBlock.jsx +168 -0
  159. package/src/components/ConnectedViews.jsx +25 -0
  160. package/src/components/Icon.jsx +8 -0
  161. package/src/components/Poll.jsx +67 -0
  162. package/src/components/QrCode.jsx +29 -0
  163. package/src/components/QrCodeTag.jsx +27 -0
  164. package/src/components/Question.jsx +28 -0
  165. package/src/components/Scale.jsx +47 -0
  166. package/src/components/VideoPlayer.jsx +108 -0
  167. package/src/components/WordCloud.jsx +106 -0
  168. package/src/components/activity.jsx +35 -0
  169. package/src/components/callout.css +78 -0
  170. package/src/components/code-block.css +123 -0
  171. package/src/components/columns.css +11 -0
  172. package/src/components/poll.css +81 -0
  173. package/src/components/video-player.css +14 -0
  174. package/src/core/devPath.js +4 -0
  175. package/src/core/editDeck.js +230 -0
  176. package/src/core/icons.js +69 -0
  177. package/src/core/ink.js +221 -0
  178. package/src/core/labels.js +144 -0
  179. package/src/core/outline.js +33 -0
  180. package/src/core/parseSlides.js +152 -0
  181. package/src/core/source.js +137 -0
  182. package/src/core/urls.js +34 -0
  183. package/src/core/validateDeck.js +139 -0
  184. package/src/editor/api.js +67 -0
  185. package/src/editor/components/App.jsx +75 -0
  186. package/src/editor/components/ConflictBanner.jsx +10 -0
  187. package/src/editor/components/DeckSettings.jsx +108 -0
  188. package/src/editor/components/DesignApp.jsx +252 -0
  189. package/src/editor/components/Diagnostics.jsx +10 -0
  190. package/src/editor/components/ExtensionEditor.jsx +66 -0
  191. package/src/editor/components/ExtensionForms.jsx +134 -0
  192. package/src/editor/components/ExtensionsPanel.jsx +114 -0
  193. package/src/editor/components/Field.jsx +24 -0
  194. package/src/editor/components/Inspector.jsx +36 -0
  195. package/src/editor/components/LayoutPicker.jsx +23 -0
  196. package/src/editor/components/LayoutSelect.jsx +25 -0
  197. package/src/editor/components/LookThumbnail.jsx +8 -0
  198. package/src/editor/components/MetaFields.jsx +24 -0
  199. package/src/editor/components/Outline.jsx +28 -0
  200. package/src/editor/components/PalettePicker.jsx +32 -0
  201. package/src/editor/components/Preview.jsx +63 -0
  202. package/src/editor/components/PropsForm.jsx +46 -0
  203. package/src/editor/components/RawSlideEditor.jsx +9 -0
  204. package/src/editor/components/RegionEditor.jsx +15 -0
  205. package/src/editor/components/Resizer.jsx +18 -0
  206. package/src/editor/designLink.js +19 -0
  207. package/src/editor/editor.css +139 -0
  208. package/src/editor/extensions.js +86 -0
  209. package/src/editor/keyboard.js +21 -0
  210. package/src/editor/model.js +38 -0
  211. package/src/editor/sampleDeck.js +165 -0
  212. package/src/editor/saveQueue.js +47 -0
  213. package/src/editor/schema.js +36 -0
  214. package/src/editor/state.js +92 -0
  215. package/src/editor/useDeckDocument.js +67 -0
  216. package/src/extensions/appearance.js +79 -0
  217. package/src/extensions/contrast.js +57 -0
  218. package/src/extensions/discover.js +126 -0
  219. package/src/extensions/manifest.js +209 -0
  220. package/src/extensions/packs.js +268 -0
  221. package/src/extensions/tokens.js +25 -0
  222. package/src/home/Home.jsx +362 -0
  223. package/src/home/home.css +148 -0
  224. package/src/layouts/layoutApi.jsx +205 -0
  225. package/src/layouts/layoutProps.js +30 -0
  226. package/src/layouts/renderSlide.jsx +17 -0
  227. package/src/live/answer/answer.css +34 -0
  228. package/src/live/answer/answer.html +14 -0
  229. package/src/live/answer/answer.js +130 -0
  230. package/src/live/client.js +290 -0
  231. package/src/live/code.js +19 -0
  232. package/src/live/follow.js +17 -0
  233. package/src/live/pairing.js +34 -0
  234. package/src/live/presence.js +33 -0
  235. package/src/live/roomTag.js +38 -0
  236. package/src/live/rooms.js +249 -0
  237. package/src/live/server.js +82 -0
  238. package/src/live/tunnel.js +180 -0
  239. package/src/paths.js +9 -0
  240. package/src/runtime/ReaderView.jsx +193 -0
  241. package/src/runtime/SlideErrorBoundary.jsx +28 -0
  242. package/src/runtime/chrome.jsx +55 -0
  243. package/src/runtime/deck-stage.js +1533 -0
  244. package/src/runtime/design.html +12 -0
  245. package/src/runtime/design.jsx +5 -0
  246. package/src/runtime/editor.html +12 -0
  247. package/src/runtime/editor.jsx +5 -0
  248. package/src/runtime/editorBridge.js +58 -0
  249. package/src/runtime/fit.js +149 -0
  250. package/src/runtime/home.html +13 -0
  251. package/src/runtime/home.jsx +5 -0
  252. package/src/runtime/index.html +15 -0
  253. package/src/runtime/ink/InkLayer.jsx +17 -0
  254. package/src/runtime/ink/Toolbar.jsx +145 -0
  255. package/src/runtime/ink/attach.js +69 -0
  256. package/src/runtime/ink/bus.js +126 -0
  257. package/src/runtime/ink/controller.js +132 -0
  258. package/src/runtime/ink/room.js +101 -0
  259. package/src/runtime/ink/select.js +124 -0
  260. package/src/runtime/ink/server.js +75 -0
  261. package/src/runtime/ink/store.js +55 -0
  262. package/src/runtime/ink/toolbar.css +79 -0
  263. package/src/runtime/main.jsx +863 -0
  264. package/src/runtime/markedSetup.js +147 -0
  265. package/src/runtime/reader.css +59 -0
  266. package/src/runtime/registry.jsx +26 -0
  267. package/src/runtime/syncOrder.js +32 -0
  268. package/src/runtime/themeLoader.js +68 -0
@@ -0,0 +1,148 @@
1
+ import { readFile, writeFile, mkdir, copyFile, cp } from 'node:fs/promises'
2
+ import { resolve, dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { execFile } from 'node:child_process'
5
+ import { promisify } from 'node:util'
6
+ import { Marked, Renderer } from 'marked'
7
+ import Prism from 'prismjs'
8
+ import 'prismjs/components/prism-markdown.js'
9
+ import 'prismjs/components/prism-yaml.js'
10
+ import 'prismjs/components/prism-jsx.js'
11
+ import 'prismjs/components/prism-bash.js'
12
+ import { pages } from './pages.js'
13
+ import { frameworkRoot as projectRoot } from '../../src/paths.js'
14
+ import { loadRegistry, manifestsOf } from '../../src/extensions/discover.js'
15
+ import { palettesFor } from '../../src/extensions/tokens.js'
16
+ import { createRequire } from 'node:module'
17
+
18
+ const REPO = 'https://github.com/tilman-schieber/mdeck'
19
+ const LINKS = [['Repository', REPO], ['Releases', `${REPO}/releases`], ['Package', 'https://www.npmjs.com/package/mdeck'], ['Issues', `${REPO}/issues`]]
20
+ const version = createRequire(import.meta.url)('../../package.json').version
21
+ const projectLinks = () => LINKS.map(([label, href]) => `<a href="${href}" rel="noopener">${label}</a>`).join('')
22
+ import { parseSlides } from '../../src/core/parseSlides.js'
23
+ import { readFileSync, existsSync } from 'node:fs'
24
+
25
+ export const docsRoot = dirname(fileURLToPath(import.meta.url))
26
+ const exec = promisify(execFile)
27
+ export const escape = value => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
28
+ const plain = html => html.replace(/<[^>]*>/g, '').replace(/&(?:amp|lt|gt|quot|#39);/g, value => ({ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" })[value])
29
+
30
+ function navigation(current) {
31
+ const groups = [...new Set(pages.map(p => p.group))]
32
+ return groups.map(group => {
33
+ const links = pages.filter(p => p.group === group).map(p => `<a href="${p.slug}.html"${p.slug === current.slug ? ' aria-current="page"' : ''}>${escape(p.title)}</a>`).join('')
34
+ if (group === 'Advanced customization') return `<details class="advanced-nav"${current.group === group ? ' open' : ''}><summary>${group}</summary><div>${links}</div></details>`
35
+ return `<div class="nav-group"><p>${group}</p>${links}</div>`
36
+ }).join('')
37
+ }
38
+
39
+ const exampleFiles = { 'first-talk': resolve(docsRoot, 'examples/first-talk.md'), 'custom-layouts': resolve(projectRoot, 'examples/custom-layouts/slides.md') }
40
+
41
+ // The theme picker lists every registered theme and starts on the example's own design.
42
+ function themeOptions(exampleFile) {
43
+ const current = parseSlides(readFileSync(exampleFile, 'utf8')).deckConfig.theme ?? 'neue'
44
+ const themes = loadRegistry(exampleFile).themes
45
+ return Object.values(themes).map(theme => `<option value="${theme.id}"${theme.id === current ? ' selected' : ''}>${escape(theme.title)}</option>`).join('')
46
+ }
47
+
48
+ function preview(page) {
49
+ if (!page.preview) return ''
50
+ const custom = page.preview === 'custom-layouts'
51
+ return `<figure class="slide-example" data-preview>
52
+ <figcaption><span>${custom ? 'A reusable comparison design' : 'A presentation you can try'}</span><a href="examples/${page.preview}.html?palette=&amp;appearance=" target="_blank" rel="noopener">Open slides</a></figcaption>
53
+ <iframe title="${custom ? 'Comparison slide example' : 'Example presentation'}" src="examples/${page.preview}.html?embedded=1&amp;palette=&amp;appearance=" loading="lazy"></iframe>
54
+ <div class="preview-controls"><div><button type="button" data-control="prev" aria-label="Previous slide or point">Previous</button><button type="button" data-control="next" aria-label="Next slide or point">Next</button><output aria-live="polite">Slide 1</output></div><label>Look <select aria-label="Example theme">${themeOptions(exampleFiles[page.preview])}</select></label></div>
55
+ </figure>`
56
+ }
57
+
58
+ // <!-- theme-sheets light --> (or dark) becomes one tab per theme with a
59
+ // contact sheet of its chapter slide in every palette (tools/theme-images.mjs).
60
+ function themeSheets(appearance) {
61
+ const registry = loadRegistry(exampleFiles['first-talk'])
62
+ const palettes = manifestsOf(registry, 'palette')
63
+ const themes = Object.values(manifestsOf(registry, 'theme'))
64
+ .filter(theme => palettesFor(theme, palettes).length > 1 && existsSync(resolve(docsRoot, `images/themes/${theme.id}-${appearance}.webp`)))
65
+ const id = theme => `sheet-${appearance}-${theme.id}`
66
+ const tabs = themes.map((theme, i) => `<button type="button" role="tab" id="${id(theme)}-tab" aria-controls="${id(theme)}" aria-selected="${i === 0}">${escape(theme.title)}</button>`).join('')
67
+ const panels = themes.map(theme => {
68
+ const src = `images/themes/${theme.id}-${appearance}.webp`
69
+ return `<div class="theme-sheet" role="tabpanel" id="${id(theme)}" aria-labelledby="${id(theme)}-tab"><a href="${src}"><img src="${src}" alt="The ${escape(theme.title)} theme's chapter slide in every palette, ${appearance}" loading="lazy"></a></div>`
70
+ }).join('')
71
+ return `<div class="theme-sheets" data-tabs><div class="theme-tabs" role="tablist" aria-label="Theme, ${appearance}">${tabs}</div>${panels}</div>`
72
+ }
73
+
74
+ export function renderPage(page, markdown) {
75
+ const headings = [], used = new Map()
76
+ let title = page.title
77
+ const firstHeading = markdown.match(/^# (.+)\r?$/m)
78
+ if (firstHeading) { title = firstHeading[1]; markdown = markdown.replace(firstHeading[0], '') }
79
+ const renderer = {
80
+ heading({ tokens, depth: level }) {
81
+ const text = this.parser.parseInline(tokens)
82
+ const base = plain(text).toLowerCase().replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-') || 'section'
83
+ const count = used.get(base) ?? 0
84
+ used.set(base, count + 1)
85
+ const id = base + (count ? `-${count + 1}` : '')
86
+ if (level === 2) headings.push({ id, text: plain(text) })
87
+ return `<h${level} id="${id}">${text}<a class="heading-link" href="#${id}" aria-label="Link to ${escape(plain(text))}">#</a></h${level}>\n`
88
+ },
89
+ code({ text: code, lang: info }) {
90
+ const language = (info ?? '').split(/\s/)[0]
91
+ const grammar = Prism.languages[{ sh: 'bash', html: 'markup', text: 'plain' }[language] ?? language]
92
+ const html = grammar ? Prism.highlight(code, grammar, language) : escape(code)
93
+ return `<figure class="code-example"><figcaption><span>${{ sh: 'In your terminal', prompt: 'Ask your assistant' }[language] ?? 'Example'}</span><button type="button" data-copy aria-label="Copy example">Copy</button></figcaption><pre><code>${html}</code></pre></figure>\n`
94
+ },
95
+ table(token) { return `<div class="table-scroll" tabindex="0" role="region" aria-label="Reference table">${Renderer.prototype.table.call(this, token)}</div>` },
96
+ link({ href, title: linkTitle, tokens }) {
97
+ const text = this.parser.parseInline(tokens)
98
+ const aliases = { 'layouts.md': 'custom-layouts.html', 'extensions.md': 'extensions.html', 'themes.md': 'theme-authoring.html', 'palettes.md': 'theme-authoring.html', '../../examples/custom-layouts/slides.md': 'downloads/comparison.md' }
99
+ href = href.replace(/^extensions\.md#/, 'extensions.html#')
100
+ href = aliases[href] ?? href
101
+ return `<a href="${escape(href)}"${linkTitle ? ` title="${escape(linkTitle)}"` : ''}>${text}</a>`
102
+ },
103
+ }
104
+ const parser = new Marked({ renderer })
105
+ const article = parser.parse(markdown).replace('<!-- preview -->', preview(page))
106
+ .replace(/<!-- theme-sheets (light|dark) -->/g, (_, appearance) => themeSheets(appearance))
107
+ const index = pages.indexOf(page)
108
+ const previous = pages[index - 1], next = pages[index + 1]
109
+ const footerLinks = `${previous ? `<a href="${previous.slug}.html"><small>Previous guide</small>${escape(previous.title)}</a>` : '<span></span>'}${next ? `<a href="${next.slug}.html"><small>Next guide</small>${escape(next.title)}</a>` : ''}`
110
+ const html = `<!doctype html>
111
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="description" content="${escape(page.description)}"><title>${escape(page.title)} · mdeck guide</title><link rel="icon" href="assets/favicon.svg" type="image/svg+xml"><link rel="stylesheet" href="assets/site.css?v=${escape(version)}"><script src="assets/site.js?v=${escape(version)}" defer></script></head>
112
+ <body${page.slug === 'index' ? ' class="welcome"' : ''}>
113
+ <a class="skip-link" href="#main">Skip to the guide</a>
114
+ <header class="site-header"><a class="brand" href="index.html" aria-label="mdeck documentation home"><img src="assets/favicon.svg" alt="" width="26" height="26"><span>mdeck</span><span class="brand-guide">Guide</span></a>
115
+ <div class="header-tools"><div class="search-box" role="search"><label for="search" class="sr-only">Search the guides</label><input id="search" type="search" placeholder="Search the guides" autocomplete="off" aria-controls="search-results" aria-expanded="false"><kbd aria-hidden="true">/</kbd><div id="search-results" hidden></div><span id="search-status" class="sr-only" aria-live="polite"></span></div>
116
+ <a class="repo-link" href="${REPO}" rel="noopener"><img src="assets/github-logo.svg" alt="" width="20" height="20">GitHub</a></div></header>
117
+ <div class="site-layout"><details class="nav-shell" open><summary>Browse the guides</summary><nav aria-label="Documentation">${navigation(page)}<a class="nav-help" href="commands.html">Open these docs: <code>mdeck docs</code></a><div class="nav-group nav-project"><p>mdeck ${escape(version)} on GitHub</p>${projectLinks()}</div></nav></details>
118
+ <main id="main" tabindex="-1"><article><header class="article-header"><p class="breadcrumb">${escape(page.group)}</p><h1>${escape(title)}</h1>${page.group === 'Advanced customization' ? '<p class="advanced-notice">This section is for people comfortable with code. You can make and present slides without it.</p>' : ''}</header>${article}</article><nav class="page-navigation" aria-label="Previous and next guide">${footerLinks}</nav><footer class="article-footer"><span>mdeck ${escape(version)}</span><span class="footer-links">${projectLinks()}</span></footer></main>
119
+ <aside class="page-outline" aria-label="On this page"><p>On this page</p>${headings.map(h => `<a href="#${h.id}">${escape(h.text)}</a>`).join('')}</aside></div></body></html>`
120
+ return { html, headings, search: { title: page.title, href: `${page.slug}.html`, description: page.description, text: plain(article).replace(/\s+/g, ' ').trim() } }
121
+ }
122
+
123
+ export async function buildDocs({ outDir = resolve(docsRoot, 'dist'), examples = true } = {}) {
124
+ await mkdir(resolve(outDir, 'assets'), { recursive: true })
125
+ await mkdir(resolve(outDir, 'downloads'), { recursive: true })
126
+ const search = []
127
+ for (const page of pages) {
128
+ const file = page.source ? resolve(docsRoot, page.source) : resolve(docsRoot, 'content', page.file)
129
+ const { html, search: entry } = renderPage(page, await readFile(file, 'utf8'))
130
+ await writeFile(resolve(outDir, `${page.slug}.html`), html)
131
+ search.push(entry)
132
+ }
133
+ for (const file of ['site.css', 'site.js', 'favicon.svg', 'github-logo.svg']) await copyFile(resolve(docsRoot, 'assets', file), resolve(outDir, 'assets', file))
134
+ await cp(resolve(docsRoot, 'images'), resolve(outDir, 'images'), { recursive: true })
135
+ await writeFile(resolve(outDir, 'search.json'), JSON.stringify(search))
136
+ await copyFile(resolve(projectRoot, 'examples/custom-layouts/slides.md'), resolve(outDir, 'downloads/comparison.md'))
137
+ await copyFile(resolve(docsRoot, 'examples/first-talk.md'), resolve(outDir, 'downloads/first-talk.md'))
138
+ if (examples) {
139
+ for (const [name, file] of Object.entries(exampleFiles)) {
140
+ await exec(process.execPath, [resolve(projectRoot, 'bin/mdeck.js'), 'build', file, '--single-file', '-o', resolve(outDir, 'examples', `${name}.html`)], { cwd: projectRoot, maxBuffer: 4 * 1024 * 1024 })
141
+ }
142
+ }
143
+ return outDir
144
+ }
145
+
146
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
147
+ console.log(`Documentation built: ${await buildDocs()}`)
148
+ }
@@ -0,0 +1,203 @@
1
+ # Change the look
2
+
3
+ A **theme** chooses the fonts and the style of the slides. A **palette** chooses the colours, in a light and a dark version. You can change either without rewriting your slides.
4
+
5
+ Ask your assistant by name or by description:
6
+
7
+ ```prompt
8
+ Use the aurora theme, dark. Or suggest a look that suits a talk for
9
+ a design conference.
10
+ ```
11
+
12
+ The settings it changes are the ones described below. To try looks before deciding, use the presenter view (see “Try a look before saving it”).
13
+
14
+ ## Choose a theme
15
+
16
+ Put the theme name under `theme` in the settings at the very top of your file:
17
+
18
+ ```yaml
19
+ ---
20
+ theme: neue
21
+ ---
22
+ ```
23
+
24
+ If you already have a settings block there, change the existing lines rather than adding a second block.
25
+
26
+ | Name to use | What it looks like |
27
+ |---|---|
28
+ | `neue` | Swiss style: large type, clear lines, red on white |
29
+ | `aurora` | Northern lights: curtains of light, night-sky chapters and glass |
30
+ | `duet` | Two voices: two accent colours that take turns, slab headings |
31
+ | `editorial` | A magazine spread: serif headlines, drop caps, pull quotes |
32
+ | `fhnw` | The FHNW visual identity |
33
+ | `terminal` | A terminal session: headings typed at a prompt, a blinking cursor, a status bar |
34
+ | `academic` | A lecture: compact slides, theorem and definition boxes, tables like in a paper |
35
+ | `sketch` | A sketchbook: handwriting, marker scribbles, sticky notes; a chalkboard in the dark |
36
+
37
+ Each theme comes with colours that suit it, so you do not have to choose any.
38
+
39
+ ## See the themes
40
+
41
+ Each theme in its own colours, on the title slide of the tour example. Click a picture to see it larger.
42
+
43
+ ### Neue
44
+
45
+ [![The tour's title slide in the Neue theme](images/themes/neue.webp)](images/themes/neue.webp)
46
+
47
+ Default colours: `swiss`.
48
+
49
+ ### Aurora
50
+
51
+ [![The tour's title slide in the Aurora theme](images/themes/aurora.webp)](images/themes/aurora.webp)
52
+
53
+ Default colours: `neon`.
54
+
55
+ ### Duet
56
+
57
+ [![The tour's title slide in the Duet theme](images/themes/duet.webp)](images/themes/duet.webp)
58
+
59
+ Default colours: `cobalt`.
60
+
61
+ ### Editorial
62
+
63
+ [![The tour's title slide in the Editorial theme](images/themes/editorial.webp)](images/themes/editorial.webp)
64
+
65
+ Default colours: `terra`.
66
+
67
+ ### FHNW
68
+
69
+ [![The tour's title slide in the FHNW theme](images/themes/fhnw.webp)](images/themes/fhnw.webp)
70
+
71
+ Default colours: its own FHNW colours.
72
+
73
+ ### Terminal
74
+
75
+ [![The tour's title slide in the Terminal theme](images/themes/terminal.webp)](images/themes/terminal.webp)
76
+
77
+ Default colours: `phosphor`, dark.
78
+
79
+ ### Academic
80
+
81
+ [![The tour's title slide in the Academic theme](images/themes/academic.webp)](images/themes/academic.webp)
82
+
83
+ Default colours: `nordic`.
84
+
85
+ ### Sketch
86
+
87
+ [![The tour's title slide in the Sketch theme](images/themes/sketch.webp)](images/themes/sketch.webp)
88
+
89
+ Default colours: `pastel`.
90
+
91
+ ## Choose colours
92
+
93
+ To use other colours, set `palette` to one of these:
94
+
95
+ | Name to use | Colours |
96
+ |---|---|
97
+ | `lagoon` | Teal on warm stone |
98
+ | `swiss` | White, black and signal red (neue's own) |
99
+ | `cobalt` | Strong ultramarine on white |
100
+ | `nordic` | Ice grey, deep navy and a cool steel blue |
101
+ | `graphite` | Black and white with a steel-blue accent |
102
+ | `terra` | Cream, deep navy and brick red |
103
+ | `forest` | Deep green with moss and ochre |
104
+ | `ember` | Sand and espresso with orange |
105
+ | `neon` | Violet and magenta, loud |
106
+ | `phosphor` | Green on black, like an old terminal screen |
107
+ | `pastel` | Soft pastels, mauve and peach (in the style of Catppuccin) |
108
+
109
+ ```yaml
110
+ ---
111
+ theme: neue
112
+ palette: terra
113
+ ---
114
+ ```
115
+
116
+ The FHNW theme always uses the FHNW colours.
117
+
118
+ ## Light or dark
119
+
120
+ Every palette has a light and a dark version. Choose dark for a dark lecture hall, light for a bright room:
121
+
122
+ ```yaml
123
+ ---
124
+ theme: neue
125
+ palette: terra
126
+ appearance: dark
127
+ ---
128
+ ```
129
+
130
+ Some slides use the other version on purpose: neue's statement slides are dark in a light talk and light in a dark one.
131
+
132
+ ## Every theme in every palette
133
+
134
+ The first chapter slide of the tour in each palette a theme offers. Choose a theme above the pictures; click a sheet to see it larger. The FHNW theme has only its own colours, so it is not shown here.
135
+
136
+ Some themes turn their chapter slides around on purpose: aurora's are a night sky in a light talk, and light in a dark one.
137
+
138
+ ### Light
139
+
140
+ <!-- theme-sheets light -->
141
+
142
+ ### Dark
143
+
144
+ <!-- theme-sheets dark -->
145
+
146
+ ## Try a look before saving it
147
+
148
+ Open the launch page and choose **Presenter view**:
149
+
150
+ ```sh
151
+ mdeck run my-talk.md
152
+ ```
153
+
154
+ Its theme, palette and light or dark controls, under the notes, let you try other looks (fold them away with the arrow beside them). These changes are a preview: they do not save back to your text file. Once you find a look you like, put its settings in the file and save.
155
+
156
+ ## More themes from other people
157
+
158
+ The theme repository has themes and palettes others made. `mdeck themes search` lists them, and `mdeck themes install <pack> my-talk.md` puts one beside your slides; see [Install or share a theme](theme-repository.html).
159
+
160
+ ## Add your name and organization
161
+
162
+ ```yaml
163
+ ---
164
+ theme: neue
165
+ meta:
166
+ title: "A place to grow"
167
+ author: "Alex Morgan"
168
+ organization: "The garden team"
169
+ date: "12 June 2026"
170
+ ---
171
+ ```
172
+
173
+ The text on the title slide still comes from the heading you write on that slide. The settings above supply information for the slide's header and footer.
174
+
175
+ You can also add `logo: ./img/logo.png` inside the `meta` group.
176
+
177
+ ## Control headers and page numbers
178
+
179
+ These settings go together under `show`, in the deck settings:
180
+
181
+ ```yaml
182
+ show:
183
+ organization: none
184
+ numbers: all
185
+ ```
186
+
187
+ | Setting | Choices | Usual behavior |
188
+ |---|---|---|
189
+ | `show.organization` | `title`, `all`, `none` | Organization appears on the title slide |
190
+ | `show.author` | `title`, `all`, `none` | Author and date appear on the title slide |
191
+ | `show.numbers` | `slides`, `all`, `none` | Numbers appear after the title slide |
192
+ | `show.sections` | `all`, `none` | Section labels appear when provided |
193
+
194
+ ## Change the slide shape
195
+
196
+ The usual shape is widescreen, with a width of 1920 and height of 1080. For a classic 4:3 shape, set:
197
+
198
+ ```yaml
199
+ width: 1920
200
+ height: 1440
201
+ ```
202
+
203
+ Check your slides after changing their shape. A layout that fit before may need fewer words or a different picture crop.
@@ -0,0 +1,178 @@
1
+ # Ask your audience
2
+
3
+ A slide can ask a question that people answer on their phones, with the results appearing on the slide as they come in.
4
+
5
+ Your assistant can add these for you. Say what you want to find out and where in the talk:
6
+
7
+ ```prompt
8
+ Before the chapter on methods, ask which of the three methods people
9
+ have used, and end with a slide for open questions. Show the QR code
10
+ once at the start.
11
+ ```
12
+
13
+ The rest of this page shows what it writes, and how to run the questions in a real room.
14
+
15
+ ## Add a poll
16
+
17
+ ```markdown
18
+ ---
19
+ # Where do we eat?
20
+
21
+ <poll room="lunch" options="Mensa|Thai|Pizza|Salad" />
22
+ ```
23
+
24
+ The slide shows one bar per option, the number of answers, a QR code and a short link. Scanning the code opens a small answer page on the phone with one button per option. Each phone has one vote and can change it; the bars follow the latest answer.
25
+
26
+ Every poll in a presentation shows the same code, so people scan once. Their phones then follow along: when you reach a slide with a poll, its buttons appear on every phone; between polls, the phones say that the next question will appear there. You can show the code at the start of the talk, or on the first poll.
27
+
28
+ | Setting | Meaning |
29
+ |---|---|
30
+ | `room` | A short name for this question, different for each poll in the deck. Letters, digits, hyphens and underscores. |
31
+ | `answer` | Optional: the right answer, written exactly like the option. A button with a tick then appears under the bars; tapping it outlines the right answer, its bar and its number in green, and tapping again hides it. The audience window shows it too. |
32
+ | `options` | The answers, separated by `\|` |
33
+ | `question` | Optional question text. Without it, phones show the slide's heading. |
34
+ | `qr` | `false` leaves out the QR code, when an earlier slide showed it with `<qrcode join />`. |
35
+
36
+ With `lang: de` in the settings, the slide and the phones show German words, such as “Scannen und abstimmen”. The phones use the presentation's colours and fonts; if you pick another theme or palette in the presenter view, the phones change with it.
37
+
38
+ ## Other kinds of questions
39
+
40
+ Besides a poll, three more kinds work the same way: one line on a slide, answered on the phones.
41
+
42
+ ```markdown
43
+ <scale room="pace" min="1" max="5" low="Too slow" high="Too fast" />
44
+ <wordcloud room="mood" placeholder="One word" />
45
+ <question room="ask" placeholder="Your question" />
46
+ ```
47
+
48
+ | Tag | Phones show | The slide shows |
49
+ |---|---|---|
50
+ | `<scale>` | The numbers from `min` to `max` (1 to 5 unless set, at most 11 steps), with `low` and `high` as labels at the ends | How many chose each number, and the average. Each phone's latest answer counts |
51
+ | `<wordcloud>` | A short text field | A cloud of every answer, packed around the middle, larger the more often it came in and some upright. Capitals and extra spaces do not matter. People may send several. `height` sets its height (520). Colours come from the palette; `--cloud-1` to `--cloud-6` override them |
52
+ | `<question>` | A text field | The newest answers as cards, eight unless `limit` says otherwise. People may send several |
53
+
54
+ All of them take `room`, `question` and `qr` like a poll, and `placeholder` for the text fields.
55
+
56
+ ## Show the code once
57
+
58
+ Each question shows the QR code unless you say otherwise. To invite everybody once, at the start, put the code on a slide of its own and leave it out of the questions:
59
+
60
+ ```markdown
61
+ ---
62
+ # Take out your phone.
63
+
64
+ <qrcode join />
65
+
66
+ ---
67
+ # How do you make slides today?
68
+
69
+ <poll room="tools" options="PowerPoint|Keynote|Markdown" qr="false" />
70
+ ```
71
+
72
+ `<qrcode join />` shows the presentation's join code large, with the link below it. Phones that scanned it follow along to every question. With `qr="false"`, a question leaves out its own code. Leave the join slide up for a while, or come back to it, for people who arrive late. `size` sets the size of the code on the join slide.
73
+
74
+ The code is drawn in the slide's colours, so it fits the theme; phone cameras also read light codes on dark slides.
75
+
76
+ Hover over the results to see **Reset**, which clears the answers, for example before the real session. Only the presenter can reset.
77
+
78
+ ## How phones and slides meet
79
+
80
+ The phones never load your slides. A small **server** connects your screen and the phones: your screen tells it which poll is showing and what the phones should display, and the server shows that on its own answer page, at an address like `example.org/482113`. The six digits are the presentation's session code; they stay the same for the whole talk.
81
+
82
+ So you present from your own computer with `mdeck run`, always with the slides as they are right now, and nothing needs to be uploaded. Only the server has to be reachable by the phones.
83
+
84
+ ## Try it on your computer
85
+
86
+ `mdeck run` has a server built in:
87
+
88
+ ```sh
89
+ mdeck run my-talk.md --network
90
+ ```
91
+
92
+ `--network` makes it reachable from phones and tablets in the same network, and the QR code then points at your computer. Without `--network`, the slide says so and offers a link to try the answer page in another browser tab on your computer. The launch page shows the join link and the session code too.
93
+
94
+ Some networks, often large Wi-Fi networks at universities, do not let devices reach each other. If phones cannot open the page, connect your laptop and the phones to a phone hotspot, or use a server on the internet (below).
95
+
96
+ ## Use it in a real session
97
+
98
+ For a lecture hall, run your own server once on a web server; it can serve all your presentations, and it also lets an iPad present from any network (see [Draw on your slides](drawing.html#present-from-an-ipad)). On that machine, install mdeck and start it:
99
+
100
+ ```sh
101
+ MDECK_SERVER_KEY=choose-a-secret mdeck server
102
+ ```
103
+
104
+ It listens on `127.0.0.1:8787`. Give it a host name of its own, such as `rooms.example.org`, and make your web server pass everything on that name to it, for example with nginx:
105
+
106
+ ```nginx
107
+ # in the http block
108
+ map $http_upgrade $connection_upgrade { default upgrade; '' ''; }
109
+
110
+ server {
111
+ server_name rooms.example.org;
112
+ # your usual listen and certificate lines here
113
+
114
+ location / {
115
+ proxy_pass http://127.0.0.1:8787;
116
+ proxy_http_version 1.1;
117
+ proxy_set_header Upgrade $http_upgrade;
118
+ proxy_set_header Connection $connection_upgrade;
119
+ proxy_set_header X-Forwarded-For $remote_addr;
120
+ proxy_buffering off;
121
+ proxy_read_timeout 1h;
122
+ }
123
+ }
124
+ ```
125
+
126
+ The slides and phones keep a connection open to receive answers live, which is why buffering is off and the timeout long. The `Upgrade` lines let WebSocket connections through, which presenting from an iPad needs; polls alone work without them. `X-Forwarded-For` lets the server limit each phone, not the web server as a whole. A server on a path of a shared host name, such as `example.org/rooms/`, works for polls too, but presenting from an iPad needs a name of its own.
127
+
128
+ `MDECK_SERVER_KEY` is required for presenting from an iPad: a server without a key does not offer it, and a laptop that does not know the key cannot connect. The server only passes requests along: it keeps nothing of your slides, and a presentation shared through it disappears when the laptop disconnects or the server restarts.
129
+
130
+ To keep the server running and start it with the machine, a systemd service works well. Put the key in a file only root can read, for example `/etc/mdeck-server.env` with the line `MDECK_SERVER_KEY=choose-a-secret`, and create `/etc/systemd/system/mdeck-server.service`:
131
+
132
+ ```ini
133
+ [Unit]
134
+ Description=mdeck server
135
+ After=network.target
136
+
137
+ [Service]
138
+ EnvironmentFile=/etc/mdeck-server.env
139
+ ExecStart=/usr/local/bin/mdeck server --port 8787
140
+ User=www-data
141
+ Restart=on-failure
142
+ NoNewPrivileges=true
143
+ ProtectSystem=strict
144
+ ProtectHome=true
145
+
146
+ [Install]
147
+ WantedBy=multi-user.target
148
+ ```
149
+
150
+ Adjust `ExecStart` to where `which mdeck` points; if Node is installed in a home folder, for example with nvm, also remove `ProtectHome=true`. Then run `sudo systemctl enable --now mdeck-server`. `journalctl -u mdeck-server` shows its log. After updating mdeck, `sudo systemctl restart mdeck-server`; rooms start empty again.
151
+
152
+ Then tell the presentation where the server is:
153
+
154
+ ```yaml
155
+ ---
156
+ server: https://rooms.example.org
157
+ ---
158
+ ```
159
+
160
+ To keep the address out of the deck, leave the setting out and set `MDECK_SERVER=https://rooms.example.org` in your shell instead; `mdeck run --server` then finds it. A built or sent presentation reads the address from the deck only, because it has to carry it to wherever it is opened.
161
+
162
+ `MDECK_SERVER_KEY` is the server’s master key. `mdeck run` keeps it on your computer and lets your local browser, or an iPad with a revocable pairing token, move the phones along and reset polls for this deck. Pairing QR codes never include the master key. Start `mdeck run` with the same key (`MDECK_SERVER_KEY=choose-a-secret mdeck run my-talk.md`), and the launch page checks the server, shows the join link and opens the presenter view. Paired devices remember only their token, scoped to this presentation; it never appears in the address bar or in the slide file. A hosted presenter can still unlock the server by opening the presenter view once with `?serverkey=…` in its address. That browser remembers the master key. Without permission to control the server, phones wait. The presenter view says whether the phones follow it, and if not, why.
163
+
164
+ The presenter's screen repeats the current poll every few seconds, so phones that join late, or a server that restarted, catch up. While a presenter view is open, other windows of the presentation in the same browser leave the phones alone. If the presentation is open on several screens, even in other browsers, the phones follow the one where the slides were changed last; just opening it elsewhere does not take them away. A presenter view that has lost the phones says so, and changing the slide there takes them back.
165
+
166
+ Two presentations with the same title share a session code. Give one of them `session: id:`, or set the digits yourself with `session: code:`.
167
+
168
+ ## What is stored
169
+
170
+ Answers are anonymous. Each phone gets a random number so it can change its vote; nothing else identifies it. The server keeps answers in memory only. It forgets a room after twelve hours without visitors and everything when it restarts. Each phone can send a limited number of answers in a short time.
171
+
172
+ ## In a PDF or a shared file
173
+
174
+ A PDF, a file made with `mdeck send` and a folder hosted without a poll server keep your questions as a record of the talk: each shows its question and options, with a note that it was answered live, and no join code. If the deck's `server` still answers when the file is made or opened, the results it holds appear instead. Printed slides never show the join code or the presenter's buttons.
175
+
176
+ ## Make your own activity
177
+
178
+ Polls, scales, word clouds and questions are ordinary components. The [interactive content](components.html#audience-interaction) guide shows how to write your own, such as a quiz, or ask your assistant to write one.