mdeck 3.6.0 → 3.6.2
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 +11 -0
- package/README.md +48 -3
- package/docs/site/assets/site.css +21 -0
- package/docs/site/assets/site.js +17 -7
- package/docs/site/build.js +15 -6
- package/docs/site/content/commands.md +2 -1
- package/docs/site/content/drawing.md +1 -1
- package/docs/site/content/sharing.md +1 -1
- package/docs/site/content/welcome.md +70 -6
- package/docs/site/examples/chart.svg +5 -0
- package/docs/site/examples/contours.jpg +0 -0
- package/docs/site/examples/first-talk.drawings.json +1 -0
- package/docs/site/examples/first-talk.md +125 -35
- package/docs/site/examples/first-talk.results.json +171 -0
- package/docs/site/examples/mdeck.svg +66 -0
- package/docs/site/examples/references.bib +23 -0
- package/docs/site/images/features/citations.webp +0 -0
- package/docs/site/images/features/code.webp +0 -0
- package/docs/site/images/features/drawing.webp +0 -0
- package/docs/site/images/features/polls.webp +0 -0
- package/docs/site/images/features/reader.webp +0 -0
- package/docs/site/images/features/themes.webp +0 -0
- package/docs/site/images/themes/academic-thumb.webp +0 -0
- package/docs/site/images/themes/glass-thumb.webp +0 -0
- package/docs/site/images/themes/minimal-thumb.webp +0 -0
- package/docs/site/images/themes/neue-thumb.webp +0 -0
- package/docs/site/images/themes/plain-thumb.webp +0 -0
- package/docs/site/images/themes/work-thumb.webp +0 -0
- package/package.json +3 -2
- package/skills/write-slides/SKILL.md +1 -1
- package/src/build/chrome.js +33 -2
- package/src/build/renderCheck.js +36 -5
- package/src/cli/args.js +1 -1
- package/src/cli/index.js +2 -1
- package/src/core/labels.js +6 -0
- package/src/runtime/ReaderView.jsx +21 -12
- package/src/runtime/deck-stage.js +5 -1
- package/src/runtime/reader.css +2 -0
- package/docs/site/examples/notebook.svg +0 -58
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"rooms": {
|
|
4
|
+
"first": {
|
|
5
|
+
"savedAt": "2026-10-09T09:13:00.000Z",
|
|
6
|
+
"closed": false,
|
|
7
|
+
"answers": [
|
|
8
|
+
{
|
|
9
|
+
"phone": 1,
|
|
10
|
+
"at": "2026-10-09T09:12:00.000Z",
|
|
11
|
+
"value": "Drawing on slides"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"phone": 2,
|
|
15
|
+
"at": "2026-10-09T09:12:01.700Z",
|
|
16
|
+
"value": "A new theme"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"phone": 3,
|
|
20
|
+
"at": "2026-10-09T09:12:03.400Z",
|
|
21
|
+
"value": "Citations"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"phone": 4,
|
|
25
|
+
"at": "2026-10-09T09:12:05.100Z",
|
|
26
|
+
"value": "Drawing on slides"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"phone": 5,
|
|
30
|
+
"at": "2026-10-09T09:12:06.800Z",
|
|
31
|
+
"value": "A new theme"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"phone": 6,
|
|
35
|
+
"at": "2026-10-09T09:12:08.500Z",
|
|
36
|
+
"value": "Polls from phones"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"phone": 7,
|
|
40
|
+
"at": "2026-10-09T09:12:10.200Z",
|
|
41
|
+
"value": "Drawing on slides"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"phone": 8,
|
|
45
|
+
"at": "2026-10-09T09:12:11.900Z",
|
|
46
|
+
"value": "A new theme"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"phone": 9,
|
|
50
|
+
"at": "2026-10-09T09:12:13.600Z",
|
|
51
|
+
"value": "Polls from phones"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"phone": 10,
|
|
55
|
+
"at": "2026-10-09T09:12:15.300Z",
|
|
56
|
+
"value": "Drawing on slides"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"phone": 11,
|
|
60
|
+
"at": "2026-10-09T09:12:17.000Z",
|
|
61
|
+
"value": "A new theme"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"phone": 12,
|
|
65
|
+
"at": "2026-10-09T09:12:18.700Z",
|
|
66
|
+
"value": "Polls from phones"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"phone": 13,
|
|
70
|
+
"at": "2026-10-09T09:12:20.400Z",
|
|
71
|
+
"value": "Drawing on slides"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"phone": 14,
|
|
75
|
+
"at": "2026-10-09T09:12:22.100Z",
|
|
76
|
+
"value": "Citations"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"phone": 15,
|
|
80
|
+
"at": "2026-10-09T09:12:23.800Z",
|
|
81
|
+
"value": "Polls from phones"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"phone": 16,
|
|
85
|
+
"at": "2026-10-09T09:12:25.500Z",
|
|
86
|
+
"value": "Drawing on slides"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"phone": 17,
|
|
90
|
+
"at": "2026-10-09T09:12:27.200Z",
|
|
91
|
+
"value": "Citations"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"phone": 18,
|
|
95
|
+
"at": "2026-10-09T09:12:28.900Z",
|
|
96
|
+
"value": "Drawing on slides"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"phone": 19,
|
|
100
|
+
"at": "2026-10-09T09:12:30.600Z",
|
|
101
|
+
"value": "A new theme"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"phone": 20,
|
|
105
|
+
"at": "2026-10-09T09:12:32.300Z",
|
|
106
|
+
"value": "Polls from phones"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"phone": 21,
|
|
110
|
+
"at": "2026-10-09T09:12:34.000Z",
|
|
111
|
+
"value": "Drawing on slides"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"phone": 22,
|
|
115
|
+
"at": "2026-10-09T09:12:35.700Z",
|
|
116
|
+
"value": "A new theme"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"phone": 23,
|
|
120
|
+
"at": "2026-10-09T09:12:37.400Z",
|
|
121
|
+
"value": "Polls from phones"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"phone": 24,
|
|
125
|
+
"at": "2026-10-09T09:12:39.100Z",
|
|
126
|
+
"value": "Drawing on slides"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"phone": 25,
|
|
130
|
+
"at": "2026-10-09T09:12:40.800Z",
|
|
131
|
+
"value": "A new theme"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"phone": 26,
|
|
135
|
+
"at": "2026-10-09T09:12:42.500Z",
|
|
136
|
+
"value": "Polls from phones"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"phone": 27,
|
|
140
|
+
"at": "2026-10-09T09:12:44.200Z",
|
|
141
|
+
"value": "Drawing on slides"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"phone": 28,
|
|
145
|
+
"at": "2026-10-09T09:12:45.900Z",
|
|
146
|
+
"value": "Citations"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"phone": 29,
|
|
150
|
+
"at": "2026-10-09T09:12:47.600Z",
|
|
151
|
+
"value": "Polls from phones"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"phone": 30,
|
|
155
|
+
"at": "2026-10-09T09:12:49.300Z",
|
|
156
|
+
"value": "Drawing on slides"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"phone": 31,
|
|
160
|
+
"at": "2026-10-09T09:12:51.000Z",
|
|
161
|
+
"value": "Citations"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"phone": 32,
|
|
165
|
+
"at": "2026-10-09T09:12:52.700Z",
|
|
166
|
+
"value": "Polls from phones"
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="800" viewBox="0 0 960 800">
|
|
2
|
+
<!-- A Markdown file becoming a stack of slides. The colours come from the
|
|
3
|
+
deck's palette (var(--accent) …), so the picture follows the theme;
|
|
4
|
+
the fallbacks apply where it is shown as a plain image. -->
|
|
5
|
+
<style>
|
|
6
|
+
.mdk-paper { fill: var(--surface, #eef1fb); stroke: var(--rule, #d5dbf0); stroke-width: 3; }
|
|
7
|
+
.mdk-fold { fill: var(--rule, #d5dbf0); }
|
|
8
|
+
.mdk-shadow { fill: var(--ink, #0c1033); opacity: .12; }
|
|
9
|
+
.mdk-code { font-family: ui-monospace, 'JetBrains Mono', Menlo, Consolas, monospace; font-size: 30px; fill: var(--ink-soft, #2a2f55); }
|
|
10
|
+
.mdk-mark { fill: var(--accent, #1f3fd1); font-weight: 700; }
|
|
11
|
+
.mdk-quiet { fill: var(--muted, #5b6080); }
|
|
12
|
+
.mdk-slide { fill: var(--bg, #ffffff); stroke: var(--ink, #0c1033); stroke-width: 3; }
|
|
13
|
+
.mdk-accent { fill: var(--accent, #1f3fd1); }
|
|
14
|
+
.mdk-accent-2 { fill: var(--accent-2, #e8590c); }
|
|
15
|
+
.mdk-rule { fill: var(--rule, #d5dbf0); }
|
|
16
|
+
.mdk-on-accent { fill: var(--on-accent, #ffffff); font-family: var(--font-display, sans-serif); font-weight: 800; font-size: 96px; }
|
|
17
|
+
.mdk-title { fill: var(--ink, #0c1033); font-family: var(--font-display, sans-serif); font-weight: 800; font-size: 46px; }
|
|
18
|
+
.mdk-arrow { fill: none; stroke: var(--accent-2, #e8590c); stroke-width: 5; stroke-linecap: round; stroke-linejoin: round; }
|
|
19
|
+
</style>
|
|
20
|
+
|
|
21
|
+
<!-- The text file -->
|
|
22
|
+
<g transform="rotate(-5 230 400)">
|
|
23
|
+
<rect class="mdk-shadow" x="56" y="146" width="370" height="510" rx="18"/>
|
|
24
|
+
<path class="mdk-paper" d="M58 130 h290 l62 62 v438 a18 18 0 0 1 -18 18 h-316 a18 18 0 0 1 -18 -18 v-482 a18 18 0 0 1 18 -18 z"/>
|
|
25
|
+
<path class="mdk-fold" d="M348 130 v44 a18 18 0 0 0 18 18 h44 z"/>
|
|
26
|
+
<text class="mdk-code" x="84" y="232"><tspan class="mdk-mark">#</tspan> A talk</text>
|
|
27
|
+
<text class="mdk-code" x="84" y="300"><tspan class="mdk-mark">-</tspan> one idea</text>
|
|
28
|
+
<text class="mdk-code" x="84" y="344"><tspan class="mdk-mark">-</tspan> an example</text>
|
|
29
|
+
<text class="mdk-code mdk-mark" x="84" y="412">---</text>
|
|
30
|
+
<text class="mdk-code mdk-quiet" x="84" y="480">:::notes</text>
|
|
31
|
+
<text class="mdk-code mdk-quiet" x="84" y="524">pause here</text>
|
|
32
|
+
<text class="mdk-code mdk-quiet" x="84" y="568">:::</text>
|
|
33
|
+
</g>
|
|
34
|
+
|
|
35
|
+
<!-- From text to slides -->
|
|
36
|
+
<path class="mdk-arrow" d="M300 112 C 380 20, 560 22, 640 150" stroke-dasharray="2 16"/>
|
|
37
|
+
<path class="mdk-arrow" d="M612 138 l30 16 l4 -34"/>
|
|
38
|
+
<path class="mdk-accent" d="M470 60 l7 17 l17 7 l-17 7 l-7 17 l-7 -17 l-17 -7 l17 -7 z"/>
|
|
39
|
+
<path class="mdk-accent-2" d="M760 210 l5 12 l12 5 l-12 5 l-5 12 l-5 -12 l-12 -5 l12 -5 z"/>
|
|
40
|
+
|
|
41
|
+
<!-- The slides: a chapter, a statement, and a content slide in front -->
|
|
42
|
+
<g transform="rotate(9 680 330)">
|
|
43
|
+
<rect class="mdk-accent-2" x="440" y="200" width="480" height="270" rx="14"/>
|
|
44
|
+
</g>
|
|
45
|
+
<g transform="rotate(3 660 420)">
|
|
46
|
+
<rect class="mdk-accent" x="420" y="290" width="480" height="270" rx="14"/>
|
|
47
|
+
<text class="mdk-on-accent" x="452" y="386">2</text>
|
|
48
|
+
<rect class="mdk-accent-2" x="512" y="350" width="80" height="10" rx="5"/>
|
|
49
|
+
</g>
|
|
50
|
+
<g transform="rotate(-3 640 540)">
|
|
51
|
+
<rect class="mdk-shadow" x="412" y="420" width="480" height="270" rx="14"/>
|
|
52
|
+
<rect class="mdk-slide" x="400" y="404" width="480" height="270" rx="14"/>
|
|
53
|
+
<text class="mdk-title" x="432" y="470">A talk</text>
|
|
54
|
+
<rect class="mdk-accent" x="432" y="486" width="88" height="8" rx="4"/>
|
|
55
|
+
<rect class="mdk-accent" x="432" y="524" width="14" height="14" rx="3"/>
|
|
56
|
+
<rect class="mdk-rule" x="458" y="525" width="150" height="12" rx="6"/>
|
|
57
|
+
<rect class="mdk-accent" x="432" y="562" width="14" height="14" rx="3"/>
|
|
58
|
+
<rect class="mdk-rule" x="458" y="563" width="120" height="12" rx="6"/>
|
|
59
|
+
<rect class="mdk-accent" x="432" y="600" width="14" height="14" rx="3"/>
|
|
60
|
+
<rect class="mdk-rule" x="458" y="601" width="136" height="12" rx="6"/>
|
|
61
|
+
<rect class="mdk-accent-2" x="682" y="580" width="34" height="62" rx="4"/>
|
|
62
|
+
<rect class="mdk-accent" x="726" y="530" width="34" height="112" rx="4"/>
|
|
63
|
+
<rect class="mdk-accent-2" x="770" y="556" width="34" height="86" rx="4"/>
|
|
64
|
+
<rect class="mdk-accent" x="814" y="500" width="34" height="142" rx="4"/>
|
|
65
|
+
</g>
|
|
66
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@misc{gruber2004,
|
|
2
|
+
author = {Gruber, John},
|
|
3
|
+
title = {Markdown},
|
|
4
|
+
howpublished = {Daring Fireball},
|
|
5
|
+
year = {2004},
|
|
6
|
+
url = {https://daringfireball.net/projects/markdown/}
|
|
7
|
+
}
|
|
8
|
+
@article{knuth1984,
|
|
9
|
+
author = {Knuth, Donald E.},
|
|
10
|
+
title = {Literate Programming},
|
|
11
|
+
journal = {The Computer Journal},
|
|
12
|
+
volume = {27},
|
|
13
|
+
number = {2},
|
|
14
|
+
pages = {97--111},
|
|
15
|
+
year = {1984},
|
|
16
|
+
doi = {10.1093/comjnl/27.2.97}
|
|
17
|
+
}
|
|
18
|
+
@misc{pandoc,
|
|
19
|
+
author = {MacFarlane, John},
|
|
20
|
+
title = {Pandoc: A Universal Document Converter},
|
|
21
|
+
year = {2006},
|
|
22
|
+
url = {https://pandoc.org/}
|
|
23
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdeck",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.2",
|
|
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",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"playground": "vite --config playground/vite.config.js",
|
|
55
55
|
"playground:build": "vite build --config playground/vite.config.js",
|
|
56
56
|
"readme-images": "node tools/readme-images.mjs",
|
|
57
|
-
"theme-images": "node tools/theme-images.mjs"
|
|
57
|
+
"theme-images": "node tools/theme-images.mjs",
|
|
58
|
+
"feature-images": "node tools/feature-images.mjs"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
60
61
|
"@citation-js/core": "^0.9.0",
|
|
@@ -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 --
|
|
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
|
|
package/src/build/chrome.js
CHANGED
|
@@ -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,
|
|
114
|
-
|
|
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)
|
package/src/build/renderCheck.js
CHANGED
|
@@ -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.
|
|
83
|
-
|
|
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,
|
|
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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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') {
|
package/src/core/labels.js
CHANGED
|
@@ -13,6 +13,9 @@ export const LABELS = {
|
|
|
13
13
|
'reader.appearance': 'Light or dark',
|
|
14
14
|
'reader.light': 'Light',
|
|
15
15
|
'reader.dark': 'Dark',
|
|
16
|
+
'reader.drawings': 'Drawings',
|
|
17
|
+
'reader.hideDrawings': 'Hide the drawings',
|
|
18
|
+
'reader.showDrawings': 'Show the drawings',
|
|
16
19
|
'reader.downloadPdf': 'Download PDF',
|
|
17
20
|
'reader.savePdf': 'Save as PDF…',
|
|
18
21
|
'reader.savePdfHint': "Opens the browser's print dialog; choose Save as PDF",
|
|
@@ -82,6 +85,9 @@ export const LABELS = {
|
|
|
82
85
|
'reader.appearance': 'Hell oder dunkel',
|
|
83
86
|
'reader.light': 'Hell',
|
|
84
87
|
'reader.dark': 'Dunkel',
|
|
88
|
+
'reader.drawings': 'Zeichnungen',
|
|
89
|
+
'reader.hideDrawings': 'Zeichnungen ausblenden',
|
|
90
|
+
'reader.showDrawings': 'Zeichnungen einblenden',
|
|
85
91
|
'reader.downloadPdf': 'PDF herunterladen',
|
|
86
92
|
'reader.savePdf': 'Als PDF sichern…',
|
|
87
93
|
'reader.savePdfHint': 'Öffnet den Druckdialog des Browsers; dort „Als PDF sichern“ wählen',
|
|
@@ -10,23 +10,25 @@ import './reader.css'
|
|
|
10
10
|
import { t, stageLabels } from '../core/labels.js'
|
|
11
11
|
import { followActiveRooms } from '../live/follow.js'
|
|
12
12
|
import { Icon } from '../components/Icon.jsx'
|
|
13
|
+
import { currentInk, onInkChange } from './ink/store.js'
|
|
13
14
|
|
|
14
15
|
// The reader view for decks sent around by email: outline, the slides at a
|
|
15
|
-
// comfortable size or stacked for reading, light or dark,
|
|
16
|
-
// into the full-screen deck. The theme and palette stay the sender's: a sent
|
|
16
|
+
// comfortable size or stacked for reading, light or dark, the drawings shown
|
|
17
|
+
// or hidden, PDF and a way back into the full-screen deck. The theme and palette stay the sender's: a sent
|
|
17
18
|
// file carries only those, with their fonts.
|
|
18
19
|
|
|
19
20
|
const lookKey = deckConfig => `mdeck-reader-look:${deckConfig.meta?.title ?? location.pathname}`
|
|
20
21
|
|
|
21
|
-
// The reader's
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return ''
|
|
22
|
+
// The reader's choices: light or dark ('' for the deck's own), and whether
|
|
23
|
+
// the drawings show.
|
|
24
|
+
function restoreLook(deckConfig) {
|
|
25
|
+
let stored = null
|
|
26
|
+
try { stored = JSON.parse(localStorage.getItem(lookKey(deckConfig)) ?? 'null') } catch {}
|
|
27
|
+
return { appearance: ['light', 'dark'].includes(stored?.appearance) ? stored.appearance : '', drawings: stored?.drawings !== false }
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const hasInk = () => Object.values(currentInk()?.slides ?? {}).some(strokes => strokes?.length)
|
|
31
|
+
|
|
30
32
|
// Every button carries an icon so the bar reads at a glance.
|
|
31
33
|
const ReaderIcon = ({ name }) => <Icon name={name} class="reader-icon" />
|
|
32
34
|
|
|
@@ -57,7 +59,10 @@ function ReadPage({ slide, index, total, deckConfig, width, height, scale, showN
|
|
|
57
59
|
export function ReaderView({ deck, deckConfig }) {
|
|
58
60
|
const { slides } = deck
|
|
59
61
|
const width = deckConfig.width ?? 1920, height = deckConfig.height ?? 1080
|
|
60
|
-
const [appearance, setAppearance] = useState(() =>
|
|
62
|
+
const [appearance, setAppearance] = useState(() => restoreLook(deckConfig).appearance)
|
|
63
|
+
// Drawings come with the file; a reader may hide them, also for printing.
|
|
64
|
+
const [drawings, setDrawings] = useState(() => restoreLook(deckConfig).drawings)
|
|
65
|
+
const [inked, setInked] = useState(hasInk)
|
|
61
66
|
const [mode, setMode] = useState('slides')
|
|
62
67
|
const [index, setIndex] = useState(() => initialIndex(slides))
|
|
63
68
|
const [navOpen, setNavOpen] = useState(false)
|
|
@@ -75,8 +80,11 @@ export function ReaderView({ deck, deckConfig }) {
|
|
|
75
80
|
|
|
76
81
|
useEffect(() => {
|
|
77
82
|
loadTheme({ ...deckConfig, appearance: shown }).catch(error => console.warn(error.message))
|
|
78
|
-
try { localStorage.setItem(lookKey(deckConfig), JSON.stringify({ appearance })) } catch {}
|
|
79
83
|
}, [appearance])
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
try { localStorage.setItem(lookKey(deckConfig), JSON.stringify({ appearance, drawings })) } catch {}
|
|
86
|
+
}, [appearance, drawings])
|
|
87
|
+
useEffect(() => onInkChange(() => setInked(hasInk())), [])
|
|
80
88
|
|
|
81
89
|
useEffect(() => {
|
|
82
90
|
const stage = stageRef.current
|
|
@@ -145,7 +153,7 @@ export function ReaderView({ deck, deckConfig }) {
|
|
|
145
153
|
const metaLine = [meta.author, meta.organization, meta.date].filter(Boolean).join(' · ')
|
|
146
154
|
const other = shown === 'dark' ? 'light' : 'dark'
|
|
147
155
|
|
|
148
|
-
return <div class=
|
|
156
|
+
return <div class={`reader-view${drawings ? '' : ' is-ink-hidden'}`}>
|
|
149
157
|
<header class="reader-top">
|
|
150
158
|
<button class="reader-btn reader-nav-toggle" onClick={() => setNavOpen(open => !open)} aria-label={t('reader.outline')}><ReaderIcon name="menu" /></button>
|
|
151
159
|
<h1>{meta.title ?? t('reader.untitled')}</h1>
|
|
@@ -155,6 +163,7 @@ export function ReaderView({ deck, deckConfig }) {
|
|
|
155
163
|
<button class={`reader-btn${mode === 'slides' ? ' is-active' : ''}`} onClick={() => switchMode('slides')}><ReaderIcon name="slides" />{t('reader.slides')}</button>
|
|
156
164
|
<button class={`reader-btn${mode === 'read' ? ' is-active' : ''}`} onClick={() => switchMode('read')}><ReaderIcon name="read" />{t('reader.read')}</button>
|
|
157
165
|
{toggleAllowed && <button class="reader-btn" onClick={() => setAppearance(other === ownAppearance ? '' : other)} title={t('reader.appearance')}><ReaderIcon name={other === 'dark' ? 'moon' : 'sun'} />{t(other === 'dark' ? 'reader.dark' : 'reader.light')}</button>}
|
|
166
|
+
{inked && <button class="reader-btn" onClick={() => setDrawings(on => !on)} aria-pressed={drawings} title={t(drawings ? 'reader.hideDrawings' : 'reader.showDrawings')}><ReaderIcon name={drawings ? 'eye' : 'eye-off'} />{t('reader.drawings')}</button>}
|
|
158
167
|
{pdf
|
|
159
168
|
? <a class="reader-btn" href={pdf} download={`${(meta.title ?? 'slides').replace(/[^\w.-]+/g, '-')}.pdf`}><ReaderIcon name="download" />{t('reader.downloadPdf')}</a>
|
|
160
169
|
: <button class="reader-btn" onClick={savePdf} title={t('reader.savePdfHint')}><ReaderIcon name="download" />{t('reader.savePdf')}</button>}
|
|
@@ -89,7 +89,11 @@ import { inkPaint, snapAngle } from '../core/ink.js'
|
|
|
89
89
|
// Zoom: up to this many times the fitted size.
|
|
90
90
|
const ZOOM_MAX = 6;
|
|
91
91
|
const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text';
|
|
92
|
+
// An embedded deck (the presenter view's frames, a preview) hides its
|
|
93
|
+
// controls, unless the page embedding it asks for them with ?controls=1, as
|
|
94
|
+
// the docs' example deck does.
|
|
92
95
|
const isEmbedded = new URLSearchParams(location.search).has('embedded');
|
|
96
|
+
const hideControls = isEmbedded && !new URLSearchParams(location.search).has('controls');
|
|
93
97
|
|
|
94
98
|
const pad2 = (n) => String(n).padStart(2, '0');
|
|
95
99
|
|
|
@@ -742,7 +746,7 @@ import { inkPaint, snapAngle } from '../core/ink.js'
|
|
|
742
746
|
}
|
|
743
747
|
|
|
744
748
|
_flashOverlay() {
|
|
745
|
-
if (!this._overlay ||
|
|
749
|
+
if (!this._overlay || hideControls) return;
|
|
746
750
|
this._overlay.setAttribute('data-visible', '');
|
|
747
751
|
if (this._hideTimer) clearTimeout(this._hideTimer);
|
|
748
752
|
this._hideTimer = setTimeout(() => {
|
package/src/runtime/reader.css
CHANGED
|
@@ -57,3 +57,5 @@
|
|
|
57
57
|
.reader-entry { position: fixed; left: 14px; bottom: 14px; z-index: 50; padding: 6px 10px; border-radius: 6px; background: rgba(20,20,20,0.7); color: #fff; font: 13px system-ui, sans-serif; text-decoration: none; opacity: 0.25; transition: opacity 200ms; }
|
|
58
58
|
.reader-entry:hover { opacity: 1; }
|
|
59
59
|
@media print { .reader-entry { display: none !important; } }
|
|
60
|
+
/* The reader hid the drawings: on the slides, in Read and in print. */
|
|
61
|
+
.reader-view.is-ink-hidden .slide-ink { display: none; }
|