mdeck 3.6.2 → 3.7.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 +9 -0
- package/README.md +1 -1
- package/assets/extensions/palettes/cobalt/extension.toml +2 -0
- package/assets/extensions/palettes/greyscale/extension.toml +4 -12
- package/assets/extensions/palettes/lagoon/extension.toml +2 -0
- package/assets/extensions/palettes/nordic/extension.toml +2 -0
- package/assets/extensions/palettes/paper/extension.toml +2 -0
- package/assets/extensions/palettes/swiss/extension.toml +2 -0
- package/assets/extensions/themes/academic/styles.css +2 -4
- package/assets/extensions/themes/glass/styles.css +2 -6
- package/assets/extensions/themes/minimal/styles.css +2 -5
- package/assets/extensions/themes/neue/styles.css +2 -5
- package/assets/extensions/themes/plain/styles.css +1 -2
- package/assets/extensions/themes/work/styles.css +2 -5
- package/docs/reference/authoring.md +1 -1
- package/docs/reference/extensions.md +2 -0
- package/docs/reference/palettes.md +7 -4
- package/docs/site/content/drawing.md +5 -1
- package/docs/site/content/media.md +3 -1
- package/docs/site/content/presenting.md +2 -0
- package/docs/site/content/theme-authoring.md +6 -3
- package/package.json +1 -1
- package/skills/write-slides/SKILL.md +1 -1
- package/src/build/check.js +1 -0
- package/src/cli/themes.js +1 -1
- package/src/components/VideoPlayer.jsx +70 -6
- package/src/components/callout.css +11 -21
- package/src/components/video-player.css +15 -0
- package/src/core/labels.js +2 -0
- package/src/editor/components/PalettePicker.jsx +1 -1
- package/src/editor/extensions.js +2 -2
- package/src/extensions/contrast.js +3 -1
- package/src/extensions/manifest.js +7 -3
- package/src/extensions/packages.js +3 -0
- package/src/extensions/tokens.js +10 -2
- package/src/live/client.js +1 -1
- package/src/runtime/chrome.jsx +1 -1
- package/src/runtime/ink/bus.js +9 -0
- package/src/runtime/main.jsx +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.7.1 — 2026-10-09
|
|
4
|
+
|
|
5
|
+
- A video file on a slide (`<videoplayer src="…" />`) follows the presenter view, on the laptop or a paired iPad: play, pause and a jump there do the same in the audience window, which plays the sound while the presenter view's copy stays silent. This works without a server too, in a built folder. An audience window that nobody has clicked yet plays silently and offers **Click for sound** (German: **Klicken für Ton**). Online videos from YouTube, Vimeo or SwitchTube still play only where they are started; the iPad guide says so.
|
|
6
|
+
|
|
7
|
+
## 3.7.0 — 2026-10-09
|
|
8
|
+
|
|
9
|
+
- Palettes have a third accent, `--accent-3`, and the three accents mean something: `--accent` is emphasis, `--accent-2` its companion (gradients, a contrast to the accent), `--accent-3` attention, used sparingly. `--accent-3` marks things and need not be readable as text, so it may be a yellow; `mdeck check` asks only that it can be seen (1.5:1 against the background). A palette without `--accent-3` keeps working and gets an amber (`#d99a00`, at night `#f5c542`); `mdeck check` suggests giving it its own. All built-in palettes and those in the theme repository set it (the repository requires it); `mdeck palettes update` brings installed ones up to date.
|
|
10
|
+
- Callouts take their colours from the palette: note, tip and important the accent, warning and caution the third accent. The five callout colours (`--callout-note` … `--callout-caution`) are gone, and with them the green, purple and blue that matched no palette. The built-in themes follow.
|
|
11
|
+
|
|
3
12
|
## 3.6.2 — 2026-10-09
|
|
4
13
|
|
|
5
14
|
- The docs home page and the README start with what mdeck does: drawing from an iPad, polls from phones, citations, themes, running code and sending one file, each with a picture (`npm run feature-images` renders them). The docs' example deck is now a short tour of mdeck, with its own control bar and drawing, and a picker of every built-in theme as a picture, with light or dark.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ license = "MIT"
|
|
|
16
16
|
"--rule" = "#d5dbf0"
|
|
17
17
|
"--accent" = "#1f3fd1"
|
|
18
18
|
"--accent-2" = "#e8590c"
|
|
19
|
+
"--accent-3" = "#e8a400"
|
|
19
20
|
"--on-accent" = "#ffffff"
|
|
20
21
|
|
|
21
22
|
[dark]
|
|
@@ -27,4 +28,5 @@ license = "MIT"
|
|
|
27
28
|
"--rule" = "#212b5e"
|
|
28
29
|
"--accent" = "#6f8cff"
|
|
29
30
|
"--accent-2" = "#ff922b"
|
|
31
|
+
"--accent-3" = "#ffd43b"
|
|
30
32
|
"--on-accent" = "#0a1030"
|
|
@@ -8,8 +8,8 @@ author = "Tilman Schieber"
|
|
|
8
8
|
license = "MIT"
|
|
9
9
|
|
|
10
10
|
# No colour anywhere: emphasis is black against the softer body text, the
|
|
11
|
-
# second accent a mid grey
|
|
12
|
-
# usual colours.
|
|
11
|
+
# second accent a mid grey, the third (warnings) a lighter grey; code uses
|
|
12
|
+
# greys instead of its usual colours.
|
|
13
13
|
[light]
|
|
14
14
|
"--bg" = "#ffffff"
|
|
15
15
|
"--surface" = "#f0f0f0"
|
|
@@ -19,6 +19,7 @@ license = "MIT"
|
|
|
19
19
|
"--rule" = "#d4d4d4"
|
|
20
20
|
"--accent" = "#000000"
|
|
21
21
|
"--accent-2" = "#6e6e6e"
|
|
22
|
+
"--accent-3" = "#9a9a9a"
|
|
22
23
|
"--on-accent" = "#ffffff"
|
|
23
24
|
"--token-default" = "#141414"
|
|
24
25
|
"--token-comment" = "#6b6b6b"
|
|
@@ -27,11 +28,6 @@ license = "MIT"
|
|
|
27
28
|
"--token-keyword" = "#000000"
|
|
28
29
|
"--token-function" = "#2a2a2a"
|
|
29
30
|
"--token-operator" = "#555555"
|
|
30
|
-
"--callout-note" = "#4a4a4a"
|
|
31
|
-
"--callout-tip" = "#4a4a4a"
|
|
32
|
-
"--callout-important" = "#000000"
|
|
33
|
-
"--callout-warning" = "#7a7a7a"
|
|
34
|
-
"--callout-caution" = "#000000"
|
|
35
31
|
|
|
36
32
|
[dark]
|
|
37
33
|
"--bg" = "#121212"
|
|
@@ -42,6 +38,7 @@ license = "MIT"
|
|
|
42
38
|
"--rule" = "#303030"
|
|
43
39
|
"--accent" = "#ffffff"
|
|
44
40
|
"--accent-2" = "#a6a6a6"
|
|
41
|
+
"--accent-3" = "#6e6e6e"
|
|
45
42
|
"--on-accent" = "#121212"
|
|
46
43
|
"--token-default" = "#f2f2f2"
|
|
47
44
|
"--token-comment" = "#9a9a9a"
|
|
@@ -50,8 +47,3 @@ license = "MIT"
|
|
|
50
47
|
"--token-keyword" = "#ffffff"
|
|
51
48
|
"--token-function" = "#e0e0e0"
|
|
52
49
|
"--token-operator" = "#b0b0b0"
|
|
53
|
-
"--callout-note" = "#b8b8b8"
|
|
54
|
-
"--callout-tip" = "#b8b8b8"
|
|
55
|
-
"--callout-important" = "#ffffff"
|
|
56
|
-
"--callout-warning" = "#8c8c8c"
|
|
57
|
-
"--callout-caution" = "#ffffff"
|
|
@@ -16,6 +16,7 @@ license = "MIT"
|
|
|
16
16
|
"--rule" = "#e0deda"
|
|
17
17
|
"--accent" = "#0d9488"
|
|
18
18
|
"--accent-2" = "#c2410c"
|
|
19
|
+
"--accent-3" = "#ca8a04"
|
|
19
20
|
"--on-accent" = "#ffffff"
|
|
20
21
|
|
|
21
22
|
[dark]
|
|
@@ -27,4 +28,5 @@ license = "MIT"
|
|
|
27
28
|
"--rule" = "#23332f"
|
|
28
29
|
"--accent" = "#2dd4bf"
|
|
29
30
|
"--accent-2" = "#fb923c"
|
|
31
|
+
"--accent-3" = "#facc15"
|
|
30
32
|
"--on-accent" = "#062b26"
|
|
@@ -16,6 +16,7 @@ license = "MIT"
|
|
|
16
16
|
"--rule" = "#cdd8e1"
|
|
17
17
|
"--accent" = "#1f6f9f"
|
|
18
18
|
"--accent-2" = "#7a5c9e"
|
|
19
|
+
"--accent-3" = "#c08a1e"
|
|
19
20
|
"--on-accent" = "#ffffff"
|
|
20
21
|
|
|
21
22
|
[dark]
|
|
@@ -27,4 +28,5 @@ license = "MIT"
|
|
|
27
28
|
"--rule" = "#22343f"
|
|
28
29
|
"--accent" = "#7cc4ea"
|
|
29
30
|
"--accent-2" = "#c3a8e6"
|
|
31
|
+
"--accent-3" = "#e8c170"
|
|
30
32
|
"--on-accent" = "#0e1a24"
|
|
@@ -16,6 +16,7 @@ license = "MIT"
|
|
|
16
16
|
"--rule" = "#e0ddd6"
|
|
17
17
|
"--accent" = "#c2410c"
|
|
18
18
|
"--accent-2" = "#1f2937"
|
|
19
|
+
"--accent-3" = "#c99700"
|
|
19
20
|
"--on-accent" = "#ffffff"
|
|
20
21
|
|
|
21
22
|
[dark]
|
|
@@ -27,4 +28,5 @@ license = "MIT"
|
|
|
27
28
|
"--rule" = "#2e2e2c"
|
|
28
29
|
"--accent" = "#fb8c4a"
|
|
29
30
|
"--accent-2" = "#e7e5e0"
|
|
31
|
+
"--accent-3" = "#f2c94c"
|
|
30
32
|
"--on-accent" = "#151515"
|
|
@@ -16,6 +16,7 @@ license = "MIT"
|
|
|
16
16
|
"--rule" = "#dadada"
|
|
17
17
|
"--accent" = "#e30613"
|
|
18
18
|
"--accent-2" = "#0047bb"
|
|
19
|
+
"--accent-3" = "#f2b700"
|
|
19
20
|
"--on-accent" = "#ffffff"
|
|
20
21
|
|
|
21
22
|
[dark]
|
|
@@ -27,4 +28,5 @@ license = "MIT"
|
|
|
27
28
|
"--rule" = "#2e2e2e"
|
|
28
29
|
"--accent" = "#ff3b30"
|
|
29
30
|
"--accent-2" = "#6f9bff"
|
|
31
|
+
"--accent-3" = "#ffd60a"
|
|
30
32
|
"--on-accent" = "#0d0d0d"
|
|
@@ -476,11 +476,9 @@ strong { color: var(--ink); font-weight: 700; }
|
|
|
476
476
|
.slide-body .callout p { color: var(--ink); }
|
|
477
477
|
.slide-body .callout :is(ul, ol) { gap: 8px; }
|
|
478
478
|
|
|
479
|
-
.slide-body :is(.callout-theorem, .callout-lemma, .callout-corollary, .callout-proposition, .callout-tip, .callout-important) { --block: var(--accent); }
|
|
479
|
+
.slide-body :is(.callout-theorem, .callout-lemma, .callout-corollary, .callout-proposition, .callout-note, .callout-tip, .callout-important) { --block: var(--accent); }
|
|
480
480
|
.slide-body :is(.callout-definition) { --block: var(--accent-2); }
|
|
481
|
-
.slide-body .callout-
|
|
482
|
-
.slide-body .callout-warning { --block: var(--callout-warning); }
|
|
483
|
-
.slide-body .callout-caution { --block: var(--callout-caution); }
|
|
481
|
+
.slide-body :is(.callout-warning, .callout-caution) { --block: var(--accent-3); }
|
|
484
482
|
.slide-body :is(.callout-theorem, .callout-lemma, .callout-corollary, .callout-proposition) > :not(.callout-title) {
|
|
485
483
|
font-family: var(--font-serif);
|
|
486
484
|
font-style: italic;
|
|
@@ -640,13 +640,9 @@ deck-stage[data-deck-static] .slide { animation: none; }
|
|
|
640
640
|
box-shadow: 0 0 10px currentColor;
|
|
641
641
|
}
|
|
642
642
|
.slide-body .callout p { padding-left: 0; margin: 0; color: var(--ink); }
|
|
643
|
-
.slide-body .callout-note { --tint: var(--
|
|
644
|
-
.slide-body .callout-
|
|
645
|
-
.slide-body .callout-important { --tint: var(--callout-important); }
|
|
646
|
-
.slide-body .callout-warning { --tint: var(--callout-warning); }
|
|
647
|
-
.slide-body .callout-caution { --tint: var(--callout-caution); }
|
|
643
|
+
.slide-body :is(.callout-note, .callout-tip, .callout-important) { --tint: var(--accent); }
|
|
644
|
+
.slide-body :is(.callout-warning, .callout-caution) { --tint: var(--accent-3); }
|
|
648
645
|
.slide-body .callout-definition { --tint: var(--accent-2); }
|
|
649
|
-
.slide-body .callout-tip .callout-title { color: color-mix(in oklab, var(--callout-tip) 60%, var(--ink)); }
|
|
650
646
|
.slide-body :is(.callout-theorem, .callout-lemma, .callout-corollary) .callout-title { color: var(--em); }
|
|
651
647
|
.slide-body .callout-definition .callout-title { color: light-dark(color-mix(in oklab, var(--accent-2) 80%, var(--ink)), var(--accent-2)); }
|
|
652
648
|
|
|
@@ -297,11 +297,8 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
297
297
|
margin: 32px 0 0;
|
|
298
298
|
max-width: 1200px;
|
|
299
299
|
}
|
|
300
|
-
.slide-body .callout-tip { border-left-color: var(--accent); }
|
|
301
|
-
.slide-body .callout-
|
|
302
|
-
.slide-body .callout-important { border-left-color: var(--callout-important); }
|
|
303
|
-
.slide-body .callout-warning { border-left-color: var(--callout-warning); }
|
|
304
|
-
.slide-body .callout-caution { border-left-color: var(--callout-caution); }
|
|
300
|
+
.slide-body :is(.callout-note, .callout-tip, .callout-important) { border-left-color: var(--accent); }
|
|
301
|
+
.slide-body :is(.callout-warning, .callout-caution) { border-left-color: var(--accent-3); }
|
|
305
302
|
.slide-body .callout-title { margin-bottom: 8px; }
|
|
306
303
|
.slide-body .callout p { padding-left: 0; color: var(--ink); font-size: var(--fs-body); }
|
|
307
304
|
/* Lecture blocks: theorems in the accent, definitions in the second */
|
|
@@ -434,12 +434,9 @@ strong { color: var(--ink); font-weight: 600; }
|
|
|
434
434
|
margin: 40px 0 0;
|
|
435
435
|
max-width: 1200px;
|
|
436
436
|
}
|
|
437
|
-
.slide-body .callout-tip { border-top-color: var(--accent); }
|
|
437
|
+
.slide-body :is(.callout-note, .callout-tip, .callout-important) { border-top-color: var(--accent); }
|
|
438
438
|
.slide-body .callout-tip .callout-title { color: var(--accent); }
|
|
439
|
-
.slide-body .callout-
|
|
440
|
-
.slide-body .callout-important { border-top-color: var(--callout-important); }
|
|
441
|
-
.slide-body .callout-warning { border-top-color: var(--callout-warning); }
|
|
442
|
-
.slide-body .callout-caution { border-top-color: var(--callout-caution); }
|
|
439
|
+
.slide-body :is(.callout-warning, .callout-caution) { border-top-color: var(--accent-3); }
|
|
443
440
|
.slide-body .callout-title { letter-spacing: 0.12em; margin-bottom: 10px; }
|
|
444
441
|
.slide-body .callout p { padding-left: 0; color: var(--ink); font-size: var(--fs-body); }
|
|
445
442
|
/* Lecture blocks: theorems under a rule in the accent, definitions in the second */
|
|
@@ -418,8 +418,7 @@ strong { font-weight: 600; color: var(--ink); }
|
|
|
418
418
|
}
|
|
419
419
|
.slide-body .callout p { padding-left: 0; margin: 0; color: var(--ink-soft); }
|
|
420
420
|
.slide-body .callout :is(.callout-title) { color: var(--ink); }
|
|
421
|
-
.slide-body .callout:is(.callout-warning, .callout-caution) { border-left-color: var(--
|
|
422
|
-
.slide-body .callout.callout-caution { border-left-color: var(--callout-caution); }
|
|
421
|
+
.slide-body .callout:is(.callout-warning, .callout-caution) { border-left-color: var(--accent-3); }
|
|
423
422
|
.slide-body .callout.callout-proof { background: none; border: 0; padding: 0; }
|
|
424
423
|
|
|
425
424
|
/* ─────────────────────────────────────────────
|
|
@@ -448,11 +448,8 @@ strong { font-weight: 600; color: var(--ink); }
|
|
|
448
448
|
margin: 0 0 8px 0;
|
|
449
449
|
}
|
|
450
450
|
.slide-body .callout p { padding-left: 0; margin: 0; color: var(--ink); }
|
|
451
|
-
.slide-body .callout-note { --bar: var(--
|
|
452
|
-
.slide-body .callout-
|
|
453
|
-
.slide-body .callout-important { --bar: var(--callout-important); }
|
|
454
|
-
.slide-body .callout-warning { --bar: var(--callout-warning); }
|
|
455
|
-
.slide-body .callout-caution { --bar: var(--callout-caution); }
|
|
451
|
+
.slide-body :is(.callout-note, .callout-tip, .callout-important) { --bar: var(--accent); }
|
|
452
|
+
.slide-body :is(.callout-warning, .callout-caution) { --bar: var(--accent-3); }
|
|
456
453
|
.slide-body .callout-definition { --bar: var(--accent-2); }
|
|
457
454
|
|
|
458
455
|
/* ─────────────────────────────────────────────
|
|
@@ -686,7 +686,7 @@ A local SVG whose source contains `var(--…)` is drawn inline instead of as an
|
|
|
686
686
|
layout's `image` (`title`, `chapter`, `image-text`, `full-bleed-image`, and
|
|
687
687
|
local layouts that render it with `Picture` from `mdeck/layout`). It can use the
|
|
688
688
|
palette's custom properties (`--bg`, `--surface`, `--ink`, `--ink-soft`,
|
|
689
|
-
`--muted`, `--rule`, `--accent`, `--accent-2`, `--on-accent`) and follows the
|
|
689
|
+
`--muted`, `--rule`, `--accent`, `--accent-2`, `--accent-3`, `--on-accent`) and follows the
|
|
690
690
|
theme, palette and appearance. Give each a fallback (`var(--accent, #1f3fd1)`)
|
|
691
691
|
for places that show the file on its own, set `fit: contain` to keep the whole
|
|
692
692
|
drawing, prefix class names in its `<style>` because inline styles apply to the
|
|
@@ -83,6 +83,7 @@ description = "Deep blue with a warm highlight."
|
|
|
83
83
|
"--rule" = "#c9d7e2"
|
|
84
84
|
"--accent" = "#0a6aa8"
|
|
85
85
|
"--accent-2" = "#b45309"
|
|
86
|
+
"--accent-3" = "#c99700"
|
|
86
87
|
"--on-accent" = "#ffffff"
|
|
87
88
|
|
|
88
89
|
[dark]
|
|
@@ -94,6 +95,7 @@ description = "Deep blue with a warm highlight."
|
|
|
94
95
|
"--rule" = "#24384b"
|
|
95
96
|
"--accent" = "#ffbd69"
|
|
96
97
|
"--accent-2" = "#7cc4ff"
|
|
98
|
+
"--accent-3" = "#f2c94c"
|
|
97
99
|
"--on-accent" = "#102030"
|
|
98
100
|
```
|
|
99
101
|
|
|
@@ -23,7 +23,7 @@ The presenter view and the reader view switch palette and light or dark without
|
|
|
23
23
|
| `swiss` | white, black and signal red | black, white and red |
|
|
24
24
|
| `nordic` | ice grey, deep navy and a cool steel blue | polar night with ice blue |
|
|
25
25
|
| `cobalt` | strong ultramarine on white, vivid orange | midnight blue, bright blue and orange |
|
|
26
|
-
| `greyscale` | black, white and greys only, code
|
|
26
|
+
| `greyscale` | black, white and greys only, code too | the same, inverted |
|
|
27
27
|
| `lagoon` | teal on warm stone, orange second accent | deep teal-black, bright teal |
|
|
28
28
|
| `paper` | off-white paper and near-black ink with one orange accent | charcoal with a softer orange |
|
|
29
29
|
|
|
@@ -35,7 +35,7 @@ More palettes are in the [theme repository](https://github.com/tilman-schieber/m
|
|
|
35
35
|
|
|
36
36
|
## Colours
|
|
37
37
|
|
|
38
|
-
Every variant sets all
|
|
38
|
+
Every variant sets all ten (a palette without `--accent-3` gets an amber, and `mdeck check` suggests setting one):
|
|
39
39
|
|
|
40
40
|
| Colour | Role |
|
|
41
41
|
|---|---|
|
|
@@ -45,8 +45,9 @@ Every variant sets all nine:
|
|
|
45
45
|
| `--ink-soft` | Body text |
|
|
46
46
|
| `--muted` | Small labels: headers, footers, captions |
|
|
47
47
|
| `--rule` | Lines and dividers |
|
|
48
|
-
| `--accent` |
|
|
49
|
-
| `--accent-2` |
|
|
48
|
+
| `--accent` | Emphasis: highlighted words, markers, chapter slides; notes, tips and important callouts |
|
|
49
|
+
| `--accent-2` | The accent's companion: gradients, a contrast to the accent, definitions |
|
|
50
|
+
| `--accent-3` | Attention: warnings and cautions, and a colour that pops, used sparingly. It marks things (bars, shapes) and need not be readable as text, so it may be a yellow |
|
|
50
51
|
| `--on-accent` | Text set on the accent, such as a chapter slide's title |
|
|
51
52
|
|
|
52
53
|
Themes may mix them (for example a light tint of the accent), and read the other variant as `--inverse-bg`, `--inverse-ink`, `--inverse-accent` and so on.
|
|
@@ -71,6 +72,7 @@ description = "Sea blue and rust on chalk white."
|
|
|
71
72
|
"--rule" = "#c9d7e2"
|
|
72
73
|
"--accent" = "#0a6aa8"
|
|
73
74
|
"--accent-2" = "#b45309"
|
|
75
|
+
"--accent-3" = "#c99700"
|
|
74
76
|
"--on-accent" = "#ffffff"
|
|
75
77
|
|
|
76
78
|
[dark]
|
|
@@ -82,6 +84,7 @@ description = "Sea blue and rust on chalk white."
|
|
|
82
84
|
"--rule" = "#24384b"
|
|
83
85
|
"--accent" = "#5fb3f0"
|
|
84
86
|
"--accent-2" = "#f59e5b"
|
|
87
|
+
"--accent-3" = "#f2c94c"
|
|
85
88
|
"--on-accent" = "#0d1a24"
|
|
86
89
|
```
|
|
87
90
|
|
|
@@ -48,7 +48,11 @@ There are two ways.
|
|
|
48
48
|
|
|
49
49
|
1. Start `mdeck run my-talk.md --network`, so the iPad in the same network can reach your laptop.
|
|
50
50
|
2. On the launch page, under **Present from an iPad**, choose **Show pairing code** and scan it with the iPad's camera. The presenter view opens on the iPad, paired with your laptop, so it may save drawings and steer the phones of a poll. It is the same presenter view as on the laptop; pairing only gives it these rights. Once it is open, the launch page says so.
|
|
51
|
-
3. On the laptop, open the **audience window** from the same place and move it to the projector. It follows the iPad: slides, revealed points,
|
|
51
|
+
3. On the laptop, open the **audience window** from the same place and move it to the projector. It follows the iPad: slides, revealed points, drawings as you draw them, and videos. Navigation in the audience window also updates the iPad's slide, notes and next-slide preview.
|
|
52
|
+
|
|
53
|
+
**Videos.** A video file on a slide (`<videoplayer src="…" />`) plays, pauses and jumps on the projector as you tap it on the iPad. The sound comes from the projector: while the audience window is open, the iPad's copy plays silently. Browsers start sound only on a page someone has clicked, so click the audience window once before the talk (going full screen counts); otherwise the video plays there without sound and shows **Click for sound**.
|
|
54
|
+
|
|
55
|
+
Online videos (`<videoplayer url="…" />` from YouTube, Vimeo or SwitchTube) do not follow the iPad. They run inside the video service's own player, which mdeck cannot start or stop, so they play only on the screen where you start them. For a talk from the iPad, start them in the audience window on the laptop, or download the video and use a video file.
|
|
52
56
|
|
|
53
57
|
A pairing code works once, for ten minutes. **Unpair** on the launch page ends every pairing; so does stopping `mdeck run`. Scan a new code afterwards.
|
|
54
58
|
|
|
@@ -86,6 +86,8 @@ To start a video when its slide appears, add `play="auto"`:
|
|
|
86
86
|
|
|
87
87
|
Automatic playback is muted. Leaving the slide stops the local video; automatic videos also return to the beginning.
|
|
88
88
|
|
|
89
|
+
Presenting from an iPad paired with your laptop? Play, pause or jump in the video on the iPad, and the audience window on the projector does the same, with the sound; see [Present from an iPad](drawing.html#present-from-an-ipad).
|
|
90
|
+
|
|
89
91
|
## Show an online video
|
|
90
92
|
|
|
91
93
|
Use `url` instead of `src` for a YouTube, Vimeo, or SwitchTube address:
|
|
@@ -94,7 +96,7 @@ Use `url` instead of `src` for a YouTube, Vimeo, or SwitchTube address:
|
|
|
94
96
|
<videoplayer url="https://www.youtube.com/watch?v=YOUR_VIDEO_ID" />
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
Replace the example address with your video's address. Online videos need an internet connection and permission from the video service to appear in another page. Test them before your talk.
|
|
99
|
+
Replace the example address with your video's address. Online videos need an internet connection and permission from the video service to appear in another page. Test them before your talk. They play only where you start them: a paired iPad cannot start one on the projector, as it can a video file.
|
|
98
100
|
|
|
99
101
|
## If a picture does not appear
|
|
100
102
|
|
|
@@ -40,6 +40,8 @@ Use its audience-window button to open a separate view of the slides. If you hav
|
|
|
40
40
|
|
|
41
41
|
The two windows follow the same slide and reveal position: navigate in either window and the other follows. This works between windows in the same browser and between your laptop and a paired iPad. To connect the iPad, see [Draw on your slides](drawing.html#present-from-an-ipad).
|
|
42
42
|
|
|
43
|
+
A video file you play, pause or move in the presenter view does the same in the audience window, which plays the sound; the presenter view's copy stays silent while the audience window is open. Click the audience window once before the talk, so the browser lets it play sound.
|
|
44
|
+
|
|
43
45
|
Small icons next to **Speaker View** show what is connected to the deck: presenter views, audience windows and, for decks with polls, how many phones. Point at them to see on which devices, such as "Presenter view on an iPad". The launch page shows the same under **Present**, and marks each view that is open.
|
|
44
46
|
|
|
45
47
|
For a single screen, switch the presenter view to its slide-only layout. **F** or the fullscreen button fills the screen in either the presenter view or the audience window; fullscreen does not select another view.
|
|
@@ -48,6 +48,7 @@ title = "Blue notebook"
|
|
|
48
48
|
"--rule" = "#dbe3ee"
|
|
49
49
|
"--accent" = "#2455c7"
|
|
50
50
|
"--accent-2" = "#17625c"
|
|
51
|
+
"--accent-3" = "#c99700"
|
|
51
52
|
"--on-accent" = "#ffffff"
|
|
52
53
|
|
|
53
54
|
[dark]
|
|
@@ -59,6 +60,7 @@ title = "Blue notebook"
|
|
|
59
60
|
"--rule" = "#24354d"
|
|
60
61
|
"--accent" = "#7aa2ff"
|
|
61
62
|
"--accent-2" = "#5ec4b6"
|
|
63
|
+
"--accent-3" = "#f2c94c"
|
|
62
64
|
"--on-accent" = "#0f1a2b"
|
|
63
65
|
```
|
|
64
66
|
|
|
@@ -76,11 +78,12 @@ The folder must be called `notebook` to match `id`. Use it with `palette: notebo
|
|
|
76
78
|
| `--ink-soft` | Body text |
|
|
77
79
|
| `--muted` | Secondary labels |
|
|
78
80
|
| `--rule` | Borders |
|
|
79
|
-
| `--accent` |
|
|
80
|
-
| `--accent-2` |
|
|
81
|
+
| `--accent` | Emphasis; notes, tips and important callouts |
|
|
82
|
+
| `--accent-2` | The accent's companion: gradients, a contrast to the accent |
|
|
83
|
+
| `--accent-3` | Attention: warnings and cautions. Use it sparingly, for bars and shapes, never as text: it may be a yellow |
|
|
81
84
|
| `--on-accent` | Text placed on an accent background |
|
|
82
85
|
|
|
83
|
-
A theme has no colours of its own: it uses these, and mixes of them, so every palette repaints it. For an inverted slide it uses the palette's other version, as `--inverse-bg`, `--inverse-ink`, `--inverse-accent` and so on:
|
|
86
|
+
A theme has no colours of its own: it uses these, and mixes of them, so every palette repaints it. Give each accent the job its name says: a theme that colours callouts colours note, tip and important with `--accent` and warning and caution with `--accent-3`. For an inverted slide it uses the palette's other version, as `--inverse-bg`, `--inverse-ink`, `--inverse-accent` and so on:
|
|
84
87
|
|
|
85
88
|
```css
|
|
86
89
|
.slide--focus {
|
package/package.json
CHANGED
|
@@ -142,7 +142,7 @@ To see slides yourself, if you can read pictures, run `mdeck snapshot <deck>.md
|
|
|
142
142
|
|
|
143
143
|
When the user wants a look the built-in themes and palettes do not give (their institute's colours, a font, the style of a poster or picture), first run `mdeck themes search` or `mdeck palettes search` (with a word or two): someone may have shared it, and `mdeck themes install <theme> <deck>.md` (with its palette) or `mdeck palettes install <palette> <deck>.md` puts it beside the deck, so the slide folder carries it. Otherwise make it as an extension in `extensions/` beside the deck, never as styling on single slides. Read `extensions.md` from the reference folder first; it lists every setting.
|
|
144
144
|
|
|
145
|
-
- **Colours only: a palette.** `extensions/<id>/extension.toml` with `kind = "palette"`, and every colour role (`--bg`, `--surface`, `--ink`, `--ink-soft`, `--muted`, `--rule`, `--accent`, `--accent-2`, `--on-accent`) in both `[light]` and `[dark]
|
|
145
|
+
- **Colours only: a palette.** `extensions/<id>/extension.toml` with `kind = "palette"`, and every colour role (`--bg`, `--surface`, `--ink`, `--ink-soft`, `--muted`, `--rule`, `--accent`, `--accent-2`, `--accent-3`, `--on-accent`) in both `[light]` and `[dark]`: `--accent` is emphasis, `--accent-2` its companion, `--accent-3` attention (warnings; it may be too light for text, such as a yellow). Set all of them. Text colours must stay readable on `--bg` and `--surface` in both variants, because themes use the other variant for inverted slides.
|
|
146
146
|
- **Fonts, sizes, spacing or arrangement: a theme.** Copy the folder of the closest built-in theme (`assets/extensions/themes/<id>/` in the mdeck repository or in `$(npm root -g)/mdeck/`) to `extensions/<new-id>/`, change `id` and `title`, then its tokens and `styles.css`. A theme takes no colours of its own: it names its default `palette`, and the colours come from palettes.
|
|
147
147
|
- **Use it:** set `theme:` and `palette:` in the deck settings. `mdeck list <deck>.md` must show it as local, and `mdeck check <deck>.md` reports any mistake in its `extension.toml`.
|
|
148
148
|
- **Hand over:** tell the user to run `mdeck design <deck>.md` (or `mdeck design <folder>` when there is no deck yet). It shows the look on a sample deck with every kind of slide, light and dark, and lets them fine-tune colours, fonts and sizes there.
|
package/src/build/check.js
CHANGED
|
@@ -26,6 +26,7 @@ export function checkDeck(slidesPath, registry, source = readFileSync(slidesPath
|
|
|
26
26
|
for (const record of Object.values(registry.palettes)) {
|
|
27
27
|
if (record.source !== 'local') continue
|
|
28
28
|
for (const problem of paletteProblems(record.manifest)) diagnostics.push({ severity: 'warning', code: 'palette-contrast', message: `Palette "${record.id}", ${problem}`, line: 1, column: 1 })
|
|
29
|
+
for (const role of record.manifest.fallbacks ?? []) diagnostics.push({ severity: 'warning', code: 'palette-fallback', message: `Palette "${record.id}" sets no ${role}, so it gets mdeck's amber; give it a colour of its own in [light] and [dark]`, line: 1, column: 1 })
|
|
29
30
|
}
|
|
30
31
|
// The deck's drawings file: readable, and only for slides that still exist.
|
|
31
32
|
const inkPath = inkFileFor(resolve(slidesPath))
|
package/src/cli/themes.js
CHANGED
|
@@ -27,7 +27,7 @@ function looksOf(registry, catalogue) {
|
|
|
27
27
|
const palettes = manifestsOf(registry, 'palette')
|
|
28
28
|
// Each theme's icon for the gallery's list (see renderPreviews).
|
|
29
29
|
const icons = new Map(catalogue.themes.map(entry => [entry.id, entry.icon]))
|
|
30
|
-
const colours = tokens => Object.fromEntries(['--bg', '--surface', '--ink', '--accent', '--accent-2'].map(key => [key, tokens?.[key] ?? null]))
|
|
30
|
+
const colours = tokens => Object.fromEntries(['--bg', '--surface', '--ink', '--accent', '--accent-2', '--accent-3'].map(key => [key, tokens?.[key] ?? null]))
|
|
31
31
|
return {
|
|
32
32
|
mdeck: VERSION,
|
|
33
33
|
themes: Object.values(manifestsOf(registry, 'theme')).map(theme => ({ id: theme.id, title: theme.title, description: theme.description ?? '', builtIn: builtIn.has(theme.id + ':t'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { h } from 'preact'
|
|
2
2
|
import { useEffect, useRef, useState } from 'preact/hooks'
|
|
3
|
+
import { t } from '../core/labels.js'
|
|
3
4
|
import './video-player.css'
|
|
4
5
|
|
|
5
6
|
function getEmbedUrl(rawUrl, autoplay = false) {
|
|
@@ -58,6 +59,66 @@ export default function VideoPlayer({ src, url, play = 'click', aspect, muted })
|
|
|
58
59
|
return () => stage.removeEventListener('slidechange', onSlideChange)
|
|
59
60
|
}, [play, src])
|
|
60
61
|
|
|
62
|
+
// Local video between the screens of a talk: play, pause or a jump on one
|
|
63
|
+
// (an iPad presenting) does the same on the others (the projector),
|
|
64
|
+
// through the stage event `mediacontrol`, which the ink bus carries
|
|
65
|
+
// (src/runtime/ink/bus.js). A video is known by its slide and its place on
|
|
66
|
+
// it. Only the slide on screen follows, and a phone following the talk
|
|
67
|
+
// does not. The presenter view's own copy goes silent while another
|
|
68
|
+
// screen shows the deck, so the sound comes from there. A screen that may
|
|
69
|
+
// not start sound yet (nobody clicked it) plays silently and offers sound.
|
|
70
|
+
const [offerSound, setOfferSound] = useState(false)
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const video = videoRef.current
|
|
73
|
+
const stage = containerRef.current?.closest('deck-stage')
|
|
74
|
+
if (!src || !video || !stage || new URLSearchParams(location.search).get('view') === 'follow') return
|
|
75
|
+
const slide = () => containerRef.current?.closest('deck-stage > *')
|
|
76
|
+
const key = () => {
|
|
77
|
+
const own = slide()
|
|
78
|
+
if (!own) return null
|
|
79
|
+
return `${[...stage.children].indexOf(own)}:${[...own.querySelectorAll('.video-player-container')].indexOf(containerRef.current)}`
|
|
80
|
+
}
|
|
81
|
+
// Changes made to follow another screen are not sent back.
|
|
82
|
+
let following = 0
|
|
83
|
+
const tell = action => {
|
|
84
|
+
if (Date.now() < following || !slide()?.hasAttribute('data-deck-active')) return
|
|
85
|
+
if (action === 'play' && stage.hasAttribute('data-others-watching')) video.muted = true
|
|
86
|
+
stage.dispatchEvent(new CustomEvent('mediacontrol', { detail: { video: key(), action, time: video.currentTime, local: true } }))
|
|
87
|
+
}
|
|
88
|
+
const onPlay = () => tell('play')
|
|
89
|
+
const onPause = () => tell('pause')
|
|
90
|
+
const onSeeked = () => tell(video.paused ? 'pause' : 'play')
|
|
91
|
+
const follow = ({ detail }) => {
|
|
92
|
+
if (!detail || detail.local || detail.video !== key() || !slide()?.hasAttribute('data-deck-active')) return
|
|
93
|
+
following = Date.now() + 600
|
|
94
|
+
if (Number.isFinite(detail.time) && Math.abs(video.currentTime - detail.time) > 0.4) video.currentTime = detail.time
|
|
95
|
+
if (detail.action === 'pause') { video.pause(); return }
|
|
96
|
+
// Only a refusal to start sound is retried silently; a pause that came
|
|
97
|
+
// in while it was starting cancels it (AbortError) and stays.
|
|
98
|
+
video.play().catch(error => {
|
|
99
|
+
if (error?.name !== 'NotAllowedError') return
|
|
100
|
+
following = Date.now() + 600
|
|
101
|
+
video.muted = true
|
|
102
|
+
setOfferSound(true)
|
|
103
|
+
return video.play()
|
|
104
|
+
}).catch(() => {})
|
|
105
|
+
}
|
|
106
|
+
video.addEventListener('play', onPlay)
|
|
107
|
+
video.addEventListener('pause', onPause)
|
|
108
|
+
video.addEventListener('seeked', onSeeked)
|
|
109
|
+
stage.addEventListener('mediacontrol', follow)
|
|
110
|
+
return () => {
|
|
111
|
+
video.removeEventListener('play', onPlay)
|
|
112
|
+
video.removeEventListener('pause', onPause)
|
|
113
|
+
video.removeEventListener('seeked', onSeeked)
|
|
114
|
+
stage.removeEventListener('mediacontrol', follow)
|
|
115
|
+
}
|
|
116
|
+
}, [src])
|
|
117
|
+
const turnSoundOn = () => {
|
|
118
|
+
if (videoRef.current) videoRef.current.muted = false
|
|
119
|
+
setOfferSound(false)
|
|
120
|
+
}
|
|
121
|
+
|
|
61
122
|
// Web video auto mode: clear/restore src on slide change to stop/start playback
|
|
62
123
|
useEffect(() => {
|
|
63
124
|
if (play !== 'auto' || !url) return
|
|
@@ -112,12 +173,15 @@ export default function VideoPlayer({ src, url, play = 'click', aspect, muted })
|
|
|
112
173
|
style={{ width: '100%', height: fixedAspect ? '100%' : 'auto', aspectRatio: fixedAspect ? undefined : '16 / 9', border: '0', display: 'block' }} />
|
|
113
174
|
: <div class="video-player-placeholder" />
|
|
114
175
|
) : (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
176
|
+
<>
|
|
177
|
+
<video
|
|
178
|
+
ref={videoRef}
|
|
179
|
+
src={src}
|
|
180
|
+
controls
|
|
181
|
+
style={{ width: '100%', height: fixedAspect ? '100%' : 'auto', display: 'block' }}
|
|
182
|
+
/>
|
|
183
|
+
{offerSound && <button type="button" class="video-player-sound" onClick={turnSoundOn}>{t('video.sound')}</button>}
|
|
184
|
+
</>
|
|
121
185
|
)}
|
|
122
186
|
</div>
|
|
123
187
|
)
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
--callout-important: #8250df;
|
|
5
|
-
--callout-warning: #eab308;
|
|
6
|
-
--callout-caution: #cf222e;
|
|
7
|
-
}
|
|
8
|
-
|
|
1
|
+
/* Callouts take the palette's accents by what they mean: a note, a tip and
|
|
2
|
+
something important are emphasis, in the accent; a warning and a caution
|
|
3
|
+
call for attention, in the third accent. */
|
|
9
4
|
.callout {
|
|
10
5
|
border-left: 4px solid var(--rule);
|
|
11
6
|
padding: 20px 28px;
|
|
@@ -21,20 +16,15 @@
|
|
|
21
16
|
}
|
|
22
17
|
.callout p { margin: 0; color: var(--ink-soft); padding-left: 28px; }
|
|
23
18
|
|
|
24
|
-
.callout-note
|
|
25
|
-
.callout-
|
|
26
|
-
.callout-important { border-left-color: var(--callout-important); }
|
|
27
|
-
.callout-warning { border-left-color: var(--callout-warning); }
|
|
28
|
-
.callout-caution { border-left-color: var(--callout-caution); }
|
|
19
|
+
.callout:is(.callout-note, .callout-tip, .callout-important) { border-left-color: var(--accent); }
|
|
20
|
+
.callout:is(.callout-warning, .callout-caution) { border-left-color: var(--accent-3); }
|
|
29
21
|
|
|
30
|
-
/* Titles are text: the
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.callout-tip
|
|
35
|
-
.callout-
|
|
36
|
-
.callout-warning .callout-title { color: color-mix(in oklab, var(--callout-warning) 45%, var(--ink)); }
|
|
37
|
-
.callout-caution .callout-title { color: color-mix(in oklab, var(--callout-caution) 65%, var(--ink)); }
|
|
22
|
+
/* Titles are text: the accent mixed with the text colour, darker on light
|
|
23
|
+
slides and lighter on dark ones, so they can be read on both. The third
|
|
24
|
+
accent is a marker colour that need not be readable as text (a yellow, say),
|
|
25
|
+
so it gets more of the text colour. */
|
|
26
|
+
.callout:is(.callout-note, .callout-tip, .callout-important) .callout-title { color: color-mix(in oklab, var(--accent) 65%, var(--ink)); }
|
|
27
|
+
.callout:is(.callout-warning, .callout-caution) .callout-title { color: color-mix(in oklab, var(--accent-3) 45%, var(--ink)); }
|
|
38
28
|
|
|
39
29
|
/* Blocks for lectures, in every theme. Theorems, lemmas and corollaries in
|
|
40
30
|
the accent with the statement in italics, definitions in the second
|
|
@@ -12,3 +12,18 @@
|
|
|
12
12
|
height: 100%;
|
|
13
13
|
background: #111;
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/* A screen that followed another into playing, silently because nobody has
|
|
17
|
+
clicked it yet: one click turns the sound on. */
|
|
18
|
+
.video-player-sound {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 12px;
|
|
21
|
+
right: 12px;
|
|
22
|
+
padding: 8px 14px;
|
|
23
|
+
border: 0;
|
|
24
|
+
border-radius: 999px;
|
|
25
|
+
background: rgba(0, 0, 0, .72);
|
|
26
|
+
color: #fff;
|
|
27
|
+
font: 600 15px/1.2 system-ui, sans-serif;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
package/src/core/labels.js
CHANGED
|
@@ -33,6 +33,7 @@ export const LABELS = {
|
|
|
33
33
|
'deck.reset': 'Reset',
|
|
34
34
|
'deck.resetHint': 'Reset to first slide',
|
|
35
35
|
'poll.scan': 'Scan to vote',
|
|
36
|
+
'video.sound': 'Click for sound',
|
|
36
37
|
'poll.answer': '1 answer',
|
|
37
38
|
'poll.answers': '{n} answers',
|
|
38
39
|
'poll.reset': 'Reset',
|
|
@@ -105,6 +106,7 @@ export const LABELS = {
|
|
|
105
106
|
'deck.reset': 'Neustart',
|
|
106
107
|
'deck.resetHint': 'Zurück zur ersten Folie',
|
|
107
108
|
'poll.scan': 'Scannen und abstimmen',
|
|
109
|
+
'video.sound': 'Klicken für Ton',
|
|
108
110
|
'poll.answer': '1 Antwort',
|
|
109
111
|
'poll.answers': '{n} Antworten',
|
|
110
112
|
'poll.reset': 'Zurücksetzen',
|
|
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'
|
|
|
3
3
|
import { Icon } from '../../components/Icon.jsx'
|
|
4
4
|
|
|
5
5
|
export function Swatches({ tokens = {} }) {
|
|
6
|
-
return <span class="swatches">{['--bg', '--surface', '--rule', '--accent', '--accent-2', '--ink'].map(key => tokens[key] ? <i key={key} style={{ background: tokens[key] }} /> : null)}</span>
|
|
6
|
+
return <span class="swatches">{['--bg', '--surface', '--rule', '--accent', '--accent-2', '--accent-3', '--ink'].map(key => tokens[key] ? <i key={key} style={{ background: tokens[key] }} /> : null)}</span>
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
// The palettes a theme offers, each with its colours in the light or dark
|
package/src/editor/extensions.js
CHANGED
|
@@ -7,8 +7,8 @@ export const ID_RE = /^[a-z][a-z0-9-]*$/
|
|
|
7
7
|
export { COLOR_ROLES as CORE_TOKENS } from '../extensions/tokens.js'
|
|
8
8
|
// Readable colours a new palette starts from.
|
|
9
9
|
const STARTER_COLORS = {
|
|
10
|
-
light: { '--bg': '#ffffff', '--surface': '#f2f2f2', '--ink': '#111111', '--ink-soft': '#333333', '--muted': '#666666', '--rule': '#dadada', '--accent': '#0b6bcb', '--accent-2': '#c2410c', '--on-accent': '#ffffff' },
|
|
11
|
-
dark: { '--bg': '#111111', '--surface': '#1e1e1e', '--ink': '#f2f2f2', '--ink-soft': '#cfcfcf', '--muted': '#8f8f8f', '--rule': '#2e2e2e', '--accent': '#6aaeff', '--accent-2': '#fb923c', '--on-accent': '#111111' },
|
|
10
|
+
light: { '--bg': '#ffffff', '--surface': '#f2f2f2', '--ink': '#111111', '--ink-soft': '#333333', '--muted': '#666666', '--rule': '#dadada', '--accent': '#0b6bcb', '--accent-2': '#c2410c', '--accent-3': '#e0a100', '--on-accent': '#ffffff' },
|
|
11
|
+
dark: { '--bg': '#111111', '--surface': '#1e1e1e', '--ink': '#f2f2f2', '--ink-soft': '#cfcfcf', '--muted': '#8f8f8f', '--rule': '#2e2e2e', '--accent': '#6aaeff', '--accent-2': '#fb923c', '--accent-3': '#f5c542', '--on-accent': '#111111' },
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const entries = table => Object.entries(table ?? {}).map(([name, value]) => ({ name, ...(value && typeof value === 'object' && !Array.isArray(value) ? value : { value }) }))
|
|
@@ -35,6 +35,8 @@ const RULES = [
|
|
|
35
35
|
['--muted', '--bg', 4.5, 'muted text'],
|
|
36
36
|
['--accent', '--bg', 3, 'the accent'],
|
|
37
37
|
['--accent-2', '--bg', 3, 'the second accent'],
|
|
38
|
+
// A marker (bars, shapes), not text: it has to be seen, not read.
|
|
39
|
+
['--accent-3', '--bg', 1.5, 'the third accent'],
|
|
38
40
|
['--on-accent', '--accent', 3, 'text on the accent'],
|
|
39
41
|
]
|
|
40
42
|
|
|
@@ -48,7 +50,7 @@ export function paletteProblems(palette) {
|
|
|
48
50
|
for (const appearance of ['light', 'dark']) {
|
|
49
51
|
const tokens = palette[appearance] ?? {}
|
|
50
52
|
for (const [fore, back, minimum, what] of RULES) {
|
|
51
|
-
if (palette.theme && (fore === '--accent' || fore === '--accent-2')) continue
|
|
53
|
+
if (palette.theme && (fore === '--accent' || fore === '--accent-2' || fore === '--accent-3')) continue
|
|
52
54
|
const ratio = contrast(tokens[fore], tokens[back])
|
|
53
55
|
if (ratio != null && ratio < minimum) problems.push(`${appearance}: ${what} (${fore} on ${back}) has a contrast of ${ratio.toFixed(1)}:1; at least ${minimum}:1 is needed`)
|
|
54
56
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync } from 'node:fs'
|
|
5
5
|
import { resolve, relative, isAbsolute, sep } from 'node:path'
|
|
6
6
|
import { parse as parseToml, TomlError } from 'smol-toml'
|
|
7
|
-
import { TOKEN_NAME_RE, COLOR_ROLES, APPEARANCES } from './tokens.js'
|
|
7
|
+
import { TOKEN_NAME_RE, COLOR_ROLES, COLOR_FALLBACKS, APPEARANCES } from './tokens.js'
|
|
8
8
|
import { propertyErrors } from '../layouts/layoutProps.js'
|
|
9
9
|
|
|
10
10
|
export const SCHEMA_VERSION = 1
|
|
@@ -247,12 +247,16 @@ function validateGuide(table, fail) {
|
|
|
247
247
|
function validatePalette(raw, shared, fail) {
|
|
248
248
|
if (raw.theme != null && (typeof raw.theme !== 'string' || !ID_RE.test(raw.theme))) fail('theme must be the id of the only theme that may use this palette', 'theme')
|
|
249
249
|
const variants = {}
|
|
250
|
+
const fallbacks = new Set()
|
|
250
251
|
for (const appearance of APPEARANCES) {
|
|
251
252
|
if (!isPlainObject(raw[appearance])) fail(`a [${appearance}] table with the ${appearance} colours is required`, appearance)
|
|
252
253
|
const tokens = validateTokens(raw[appearance], appearance, fail, { required: true })
|
|
253
|
-
const missing = COLOR_ROLES.filter(role => !Object.hasOwn(tokens, role))
|
|
254
|
+
const missing = COLOR_ROLES.filter(role => !Object.hasOwn(tokens, role) && !COLOR_FALLBACKS[role])
|
|
254
255
|
if (missing.length) fail(`missing colours: ${missing.join(', ')}`, appearance)
|
|
256
|
+
for (const [role, values] of Object.entries(COLOR_FALLBACKS)) {
|
|
257
|
+
if (!Object.hasOwn(tokens, role)) { tokens[role] = values[appearance]; fallbacks.add(role) }
|
|
258
|
+
}
|
|
255
259
|
variants[appearance] = tokens
|
|
256
260
|
}
|
|
257
|
-
return { ...shared, ...(raw.theme ? { theme: raw.theme } : {}), light: variants.light, dark: variants.dark }
|
|
261
|
+
return { ...shared, ...(raw.theme ? { theme: raw.theme } : {}), light: variants.light, dark: variants.dark, ...(fallbacks.size ? { fallbacks: [...fallbacks] } : {}) }
|
|
258
262
|
}
|
|
@@ -74,6 +74,9 @@ export function checkPackage({ kind, id, files }, { published = false } = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
if (record.kind !== kind) fail(`${id}/extension.toml says kind = "${record.kind}", but it is in the ${kind}s`)
|
|
76
76
|
if (published) for (const key of ['version', 'author', 'license']) if (!record.package?.[key]) fail(`The ${kind} ${id}: ${key} is required in the theme repository`)
|
|
77
|
+
// Palettes made before a colour existed get mdeck's fallback; a new one in
|
|
78
|
+
// the repository sets every colour itself.
|
|
79
|
+
if (published && record.manifest.fallbacks?.length) fail(`The palette ${id}: ${record.manifest.fallbacks.join(', ')} is required in the theme repository`)
|
|
77
80
|
if (kind === 'theme') {
|
|
78
81
|
for (const url of record.manifest.fonts) if (!FONT_HOSTS.some(host => url.startsWith(host))) fail(`The theme ${id} loads fonts from ${url}; allowed are ${FONT_HOSTS.join(', ')}`)
|
|
79
82
|
checkCss(files['styles.css'] ?? '', `The theme ${id}: styles.css`)
|
package/src/extensions/tokens.js
CHANGED
|
@@ -9,8 +9,16 @@ export function tokensToCss(tokens = {}, selector = ':root') {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
// The colours every palette variant defines. Themes use only these (and
|
|
12
|
-
// mixes of them), so any palette fits any theme.
|
|
13
|
-
|
|
12
|
+
// mixes of them), so any palette fits any theme. The three accents mean
|
|
13
|
+
// something: --accent is emphasis (and notes, tips, important callouts),
|
|
14
|
+
// --accent-2 its companion (gradients, a contrast to the accent), --accent-3
|
|
15
|
+
// attention (warnings and cautions), used sparingly; it marks things and
|
|
16
|
+
// need not be readable as text.
|
|
17
|
+
export const COLOR_ROLES = ['--bg', '--surface', '--ink', '--ink-soft', '--muted', '--rule', '--accent', '--accent-2', '--accent-3', '--on-accent']
|
|
18
|
+
// Colours a palette may leave out, and what it gets instead: palettes made
|
|
19
|
+
// before the third accent get an amber that calls for attention in light and
|
|
20
|
+
// dark alike (`mdeck check` suggests setting one's own).
|
|
21
|
+
export const COLOR_FALLBACKS = { '--accent-3': { light: '#d99a00', dark: '#f5c542' } }
|
|
14
22
|
export const APPEARANCES = ['light', 'dark']
|
|
15
23
|
|
|
16
24
|
/**
|
package/src/live/client.js
CHANGED
|
@@ -237,7 +237,7 @@ export function latestByDevice(messages) {
|
|
|
237
237
|
// The deck's look as the answer page needs it: theme variables, font
|
|
238
238
|
// stylesheets and the style of a slide heading, read from the page that shows
|
|
239
239
|
// the slides (the presenter's current-slide frame, or the deck itself).
|
|
240
|
-
const LOOK_TOKENS = ['--bg', '--ink', '--ink-soft', '--muted', '--rule', '--surface', '--accent', '--accent-2', '--on-accent', '--font-body', '--font-display']
|
|
240
|
+
const LOOK_TOKENS = ['--bg', '--ink', '--ink-soft', '--muted', '--rule', '--surface', '--accent', '--accent-2', '--accent-3', '--on-accent', '--font-body', '--font-display']
|
|
241
241
|
let lookSource = () => document
|
|
242
242
|
export function setLookSource(source) { lookSource = source }
|
|
243
243
|
export function captureLook(doc = lookSource()) {
|
package/src/runtime/chrome.jsx
CHANGED
|
@@ -43,7 +43,7 @@ export const S = {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function PaletteSwatches({ tokens }) {
|
|
46
|
-
const keys = ['--bg', '--surface', '--rule', '--accent', '--accent-2', '--ink']
|
|
46
|
+
const keys = ['--bg', '--surface', '--rule', '--accent', '--accent-2', '--accent-3', '--ink']
|
|
47
47
|
return (
|
|
48
48
|
<div style={{ display: 'flex', gap: '2px', alignItems: 'center', flexShrink: 0 }}>
|
|
49
49
|
{keys.map(k => tokens[k]
|
package/src/runtime/ink/bus.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// window shows (stage zoom), at most every ~50 ms
|
|
15
15
|
// { type: 'controls', key, room, controls } an activity closed or opened,
|
|
16
16
|
// its right answer or results shown or hidden (components/activity.jsx)
|
|
17
|
+
// { type: 'media', key, video, action, time } a video in the deck played,
|
|
18
|
+
// paused or moved to `time` (components/VideoPlayer.jsx)
|
|
17
19
|
// { type: 'op', op } a change of the saved ink (applyOp)
|
|
18
20
|
// Keys start with the sending window's id, so strokes never mix.
|
|
19
21
|
import { changeInk, renameSlide } from './store.js'
|
|
@@ -52,6 +54,8 @@ export function createInkBus(stage, transports) {
|
|
|
52
54
|
stage.liveStroke(message.key, known)
|
|
53
55
|
} else if (message.type === 'controls') {
|
|
54
56
|
stage.dispatchEvent(new CustomEvent('pollcontrols', { detail: { room: message.room, controls: message.controls } }))
|
|
57
|
+
} else if (message.type === 'media') {
|
|
58
|
+
stage.dispatchEvent(new CustomEvent('mediacontrol', { detail: { video: message.video, action: message.action, time: message.time } }))
|
|
55
59
|
} else if (message.type === 'zoom') {
|
|
56
60
|
if (message.slideId && message.slideId === stage._inkSlideId()) stage.setZoom(message.scale > 1 ? message : null, 'remote', { announce: false })
|
|
57
61
|
} else if (message.type === 'dot') {
|
|
@@ -105,6 +109,11 @@ export function createInkBus(stage, transports) {
|
|
|
105
109
|
if (detail?.local) send({ type: 'controls', key: `${me}:controls`, room: detail.room, controls: detail.controls })
|
|
106
110
|
})
|
|
107
111
|
|
|
112
|
+
// A video played, paused or moved here: the others do the same.
|
|
113
|
+
stage.addEventListener('mediacontrol', ({ detail }) => {
|
|
114
|
+
if (detail?.local && detail.video) send({ type: 'media', key: `${me}:media`, video: detail.video, action: detail.action, time: detail.time })
|
|
115
|
+
})
|
|
116
|
+
|
|
108
117
|
// The zoom: the latest view, not every step of a pinch.
|
|
109
118
|
let zoom = null, zoomTimer = null
|
|
110
119
|
const sendZoom = () => { zoomTimer = null; if (zoom) send({ type: 'zoom', key: `${me}:zoom`, ...zoom }) }
|
package/src/runtime/main.jsx
CHANGED
|
@@ -50,6 +50,10 @@ if (requestedView(controlUrl) === 'presenter' && !controlUrl.searchParams.has('s
|
|
|
50
50
|
history.replaceState(null, '', controlUrl)
|
|
51
51
|
}
|
|
52
52
|
const DECK_CHANNEL = `deck-control:${controlUrl.pathname}:${controlUrl.searchParams.get('session') ?? 'default'}`
|
|
53
|
+
// An audience window says it is there this often, and counts as gone when
|
|
54
|
+
// it has not said so for this long (or says so as it closes).
|
|
55
|
+
const SCREEN_SAY_MS = 3000
|
|
56
|
+
const SCREEN_GONE_MS = 8000
|
|
53
57
|
|
|
54
58
|
function withConfigOverrides(deckConfig) {
|
|
55
59
|
const url = new URL(window.location.href)
|
|
@@ -788,11 +792,33 @@ async function init() {
|
|
|
788
792
|
followStageRoom(inkStage)
|
|
789
793
|
announceStagePosition(inkStage)
|
|
790
794
|
}
|
|
795
|
+
// Whether another screen shows the deck, so that a video played in the
|
|
796
|
+
// presenter view is silent there and its sound comes from that screen
|
|
797
|
+
// (components/VideoPlayer.jsx). The server says who is connected; an
|
|
798
|
+
// audience window also says so to the windows of its own browser, for a
|
|
799
|
+
// deck opened without a server (a built folder).
|
|
800
|
+
const screens = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel(`mdeck-screens:${inkStorageKey}`) : null
|
|
801
|
+
let watchedOnServer = false, audienceHere = 0
|
|
802
|
+
const markWatched = () => inkStage?.toggleAttribute('data-others-watching', watchedOnServer || Date.now() - audienceHere < SCREEN_GONE_MS)
|
|
803
|
+
if (screens && audienceMode) {
|
|
804
|
+
const say = message => screens.postMessage(message)
|
|
805
|
+
say('audience')
|
|
806
|
+
setInterval(() => say('audience'), SCREEN_SAY_MS)
|
|
807
|
+
window.addEventListener('pagehide', () => say('gone'))
|
|
808
|
+
} else if (screens && embedded && drawHere) {
|
|
809
|
+
screens.onmessage = ({ data }) => { audienceHere = data === 'audience' ? Date.now() : 0; markWatched() }
|
|
810
|
+
setInterval(markWatched, SCREEN_SAY_MS)
|
|
811
|
+
}
|
|
812
|
+
|
|
791
813
|
if (inkStage && drawHere) {
|
|
792
814
|
attachInk(inkStage, {
|
|
793
815
|
storageKey: inkStorageKey,
|
|
794
816
|
// The presenter's main frame tells the presenter view who is connected.
|
|
795
|
-
transports: [roomTransport({ receive: true, view: embedded ? 'presenter' : 'deck', onPresence: embedded ? presence =>
|
|
817
|
+
transports: [roomTransport({ receive: true, view: embedded ? 'presenter' : 'deck', onPresence: embedded ? presence => {
|
|
818
|
+
watchedOnServer = (presence?.views ?? []).some(entry => entry.view === 'audience' || entry.view === 'deck')
|
|
819
|
+
markWatched()
|
|
820
|
+
window.parent.postMessage({ presence }, window.location.origin)
|
|
821
|
+
} : null })],
|
|
796
822
|
onMode: inking => { if (embedded) window.parent.postMessage({ inkMode: inking }, window.location.origin) },
|
|
797
823
|
})
|
|
798
824
|
// Drawing turns itself on, with the laser and the toolbar folded into
|