mdeck 3.7.1 → 3.8.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.
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/assets/base.css +9 -5
- package/assets/extensions/themes/academic/styles.css +22 -22
- package/assets/extensions/themes/glass/styles.css +17 -17
- package/assets/extensions/themes/minimal/styles.css +14 -14
- package/assets/extensions/themes/neue/styles.css +14 -14
- package/assets/extensions/themes/plain/styles.css +17 -17
- package/assets/extensions/themes/work/styles.css +26 -26
- package/docs/reference/authoring.md +12 -0
- package/docs/site/content/drawing.md +1 -1
- package/docs/site/content/presenting.md +45 -0
- package/examples/python/slides.de.md +493 -0
- package/examples/python/{slides.md → slides.en.md} +1 -1
- package/examples/python/slides.fa.md +492 -0
- package/examples/python/slides.hi.md +492 -0
- package/examples/showcase/slides.md +33 -0
- package/package.json +1 -1
- package/skills/write-slides/SKILL.md +1 -0
- package/src/build/config.js +1 -1
- package/src/build/slidesPlugin.js +34 -4
- package/src/components/callout.css +10 -10
- package/src/components/citation.css +2 -2
- package/src/components/code-block.css +3 -3
- package/src/components/poll.css +6 -6
- package/src/components/video-player.css +1 -1
- package/src/core/editDeck.js +3 -2
- package/src/core/icons.js +1 -0
- package/src/core/languages.js +51 -0
- package/src/core/parseSlides.js +15 -3
- package/src/core/validateDeck.js +5 -0
- package/src/editor/components/DeckSettings.jsx +4 -0
- package/src/editor/components/MetaFields.jsx +7 -0
- package/src/layouts/layoutApi.jsx +7 -1
- package/src/live/answer/answer.css +2 -2
- package/src/runtime/chrome.jsx +6 -7
- package/src/runtime/deck-stage.js +249 -11
- package/src/runtime/hotspots.js +98 -0
- package/src/runtime/ink/bus.js +12 -1
- package/src/runtime/ink/room.js +20 -8
- package/src/runtime/main.jsx +152 -22
- package/src/runtime/zoom/camera.js +77 -0
- /package/examples/python/{slides.drawings.json → slides.en.drawings.json} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
something important are emphasis, in the accent; a warning and a caution
|
|
3
3
|
call for attention, in the third accent. */
|
|
4
4
|
.callout {
|
|
5
|
-
border-
|
|
5
|
+
border-inline-start: 4px solid var(--rule);
|
|
6
6
|
padding: 20px 28px;
|
|
7
7
|
background: var(--surface, var(--bg));
|
|
8
8
|
margin: var(--gap-item) 0;
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
text-transform: uppercase;
|
|
15
15
|
margin: 0 0 8px 0;
|
|
16
16
|
}
|
|
17
|
-
.callout p { margin: 0; color: var(--ink-soft); padding-
|
|
17
|
+
.callout p { margin: 0; color: var(--ink-soft); padding-inline-start: 28px; }
|
|
18
18
|
|
|
19
|
-
.callout:is(.callout-note, .callout-tip, .callout-important) { border-
|
|
20
|
-
.callout:is(.callout-warning, .callout-caution) { border-
|
|
19
|
+
.callout:is(.callout-note, .callout-tip, .callout-important) { border-inline-start-color: var(--accent); }
|
|
20
|
+
.callout:is(.callout-warning, .callout-caution) { border-inline-start-color: var(--accent-3); }
|
|
21
21
|
|
|
22
22
|
/* Titles are text: the accent mixed with the text colour, darker on light
|
|
23
23
|
slides and lighter on dark ones, so they can be read on both. The third
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
into its text, and a square ends it. Themes with their own callout style
|
|
33
33
|
colour these blocks their way; the proof is shared, and themes that set
|
|
34
34
|
it differently (academic, sketch) use the same selectors. */
|
|
35
|
-
.callout:is(.callout-theorem, .callout-lemma, .callout-corollary) { border-
|
|
36
|
-
.callout.callout-definition { border-
|
|
35
|
+
.callout:is(.callout-theorem, .callout-lemma, .callout-corollary) { border-inline-start-color: var(--accent); }
|
|
36
|
+
.callout.callout-definition { border-inline-start-color: var(--accent-2); }
|
|
37
37
|
.callout:is(.callout-theorem, .callout-lemma, .callout-corollary) .callout-title { color: color-mix(in oklab, var(--accent) 75%, var(--ink)); }
|
|
38
38
|
.callout.callout-definition .callout-title { color: color-mix(in oklab, var(--accent-2) 75%, var(--ink)); }
|
|
39
39
|
.callout:is(.callout-theorem, .callout-lemma, .callout-corollary) > :not(.callout-title) { font-style: italic; }
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
.slide-body .callout.callout-proof::before { display: none; }
|
|
51
51
|
.slide-body .callout.callout-proof .callout-title {
|
|
52
52
|
display: inline;
|
|
53
|
-
float:
|
|
54
|
-
margin: 0
|
|
53
|
+
float: inline-start;
|
|
54
|
+
margin-block: 0; margin-inline: 0 0.4em;
|
|
55
55
|
padding: 0;
|
|
56
56
|
background: none;
|
|
57
57
|
font-size: inherit;
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
}
|
|
65
65
|
.slide-body .callout.callout-proof .callout-title::before { content: none; }
|
|
66
66
|
.slide-body .callout.callout-proof .callout-title::after { content: '.'; }
|
|
67
|
-
.slide-body .callout.callout-proof > :not(.callout-title) { padding-
|
|
68
|
-
.slide-body .callout.callout-proof > :last-child::after { content: '\220E'; margin-
|
|
67
|
+
.slide-body .callout.callout-proof > :not(.callout-title) { padding-inline-start: 0; font-style: normal; }
|
|
68
|
+
.slide-body .callout.callout-proof > :last-child::after { content: '\220E'; margin-inline-start: 0.6em; color: var(--ink); }
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
.slide-citations .csl-bib-body { display: flex; flex-direction: column; gap: 4px; }
|
|
19
19
|
|
|
20
20
|
/* Entries hang; numeric styles put the number in a margin column. */
|
|
21
|
-
.csl-entry { line-height: 1.4; padding-
|
|
22
|
-
.csl-entry:has(.csl-left-margin) { display: grid; grid-template-columns: auto 1fr; column-gap: 0.6em; padding-
|
|
21
|
+
.csl-entry { line-height: 1.4; padding-inline-start: 1.5em; text-indent: -1.5em; overflow-wrap: anywhere; }
|
|
22
|
+
.csl-entry:has(.csl-left-margin) { display: grid; grid-template-columns: auto 1fr; column-gap: 0.6em; padding-inline-start: 0; text-indent: 0; }
|
|
23
23
|
.csl-left-margin { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
24
24
|
.csl-entry a { color: inherit; text-decoration: none; }
|
|
25
25
|
|
|
@@ -25,7 +25,7 @@ code[class*="language-"] { color: var(--token-default); }
|
|
|
25
25
|
margin: 40px 0;
|
|
26
26
|
width: 100%;
|
|
27
27
|
/* Centred layouts (focus, title) must not centre the lines of code. */
|
|
28
|
-
text-align:
|
|
28
|
+
text-align: start;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/* ── Action buttons ─────────────────────────────────────────────────────── */
|
|
@@ -103,11 +103,11 @@ code[class*="language-"] { color: var(--token-default); }
|
|
|
103
103
|
max-height: 10em;
|
|
104
104
|
overflow-y: auto;
|
|
105
105
|
border-radius: 0 8px 8px 8px;
|
|
106
|
-
border-
|
|
106
|
+
border-inline-start: 4px solid var(--rule);
|
|
107
107
|
}
|
|
108
108
|
.code-output--error {
|
|
109
109
|
color: #f87171;
|
|
110
|
-
border-
|
|
110
|
+
border-inline-start-color: #ef4444;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/* ── Code taller than its place on the slide (src/runtime/fit.js) ───────── */
|
package/src/components/poll.css
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
.poll-row { display: grid; grid-template-columns: minmax(120px, 28%) 1fr 3ch; gap: 24px; align-items: center; }
|
|
10
10
|
.poll-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
11
11
|
/* buttons="letters|numbers": the key phones show, before the option. */
|
|
12
|
-
.poll-key { display: inline-grid; place-items: center; min-width: 1.6em; height: 1.6em; margin-
|
|
12
|
+
.poll-key { display: inline-grid; place-items: center; min-width: 1.6em; height: 1.6em; margin-inline-end: .5em; padding: 0 .3em; border-radius: .4em; background: color-mix(in srgb, var(--accent, #0d9488) 14%, transparent); color: var(--accent, #0d9488); font-weight: 700; font-size: .8em; vertical-align: .1em; }
|
|
13
13
|
/* A picture as an option: a thumbnail beside its bar. */
|
|
14
14
|
.poll-label:has(img) { white-space: normal; }
|
|
15
15
|
.poll-label img { max-height: 3.2em; max-width: 100%; vertical-align: middle; }
|
|
16
16
|
.poll .poll-several { margin: -20px 0 24px; font-size: var(--fs-small, 24px); color: var(--muted, #666); }
|
|
17
17
|
.poll-track { height: 1.1em; background: color-mix(in srgb, var(--ink, #111) 8%, transparent); border-radius: 6px; overflow: hidden; }
|
|
18
18
|
.poll-fill { display: block; height: 100%; background: var(--accent, #0d9488); border-radius: 6px; transition: width .6s cubic-bezier(.2, .8, .2, 1); }
|
|
19
|
-
.poll-count { text-align:
|
|
19
|
+
.poll-count { text-align: end; font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
20
20
|
.poll .poll-total { display: flex; align-items: center; gap: 14px; margin: 36px 0 0; font-size: var(--fs-small, 24px); color: var(--muted, #666); }
|
|
21
21
|
.poll-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--muted, #999); opacity: .5; }
|
|
22
22
|
.poll-dot.is-live { background: #16a34a; opacity: 1; }
|
|
@@ -44,16 +44,16 @@ deck-stage[data-follower] .poll.is-results-hidden .poll-fill, deck-stage[data-fo
|
|
|
44
44
|
.poll-link { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 20px; color: var(--muted, #555); }
|
|
45
45
|
/* The join code in the slide's own colours (see QrCode.jsx). */
|
|
46
46
|
.poll-join { flex: none; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 18px; border: 1px solid var(--rule, rgba(0,0,0,.12)); border-radius: 12px; color: var(--ink, #111); text-decoration: none; font-size: var(--fs-small, 24px); }
|
|
47
|
-
.poll-join--local { display: block; max-width: 460px; text-align:
|
|
47
|
+
.poll-join--local { display: block; max-width: 460px; text-align: start; line-height: 1.4; color: var(--ink-soft, var(--ink, #333)); }
|
|
48
48
|
.poll-join--local code { display: inline; font-size: .9em; }
|
|
49
49
|
.poll-join--local a { color: var(--accent, #0d9488); }
|
|
50
50
|
|
|
51
51
|
/* <question>: the newest answers as cards */
|
|
52
52
|
.question-cards { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 18px; }
|
|
53
53
|
.slide-body .question-cards li::before, .question-cards li::before, .question-cards li::marker { content: none; display: none; }
|
|
54
|
-
.slide-body .question-cards li { padding-
|
|
55
|
-
.question-cards li { margin: 0; padding: 18px 22px; border-radius: 10px; background: var(--surface, color-mix(in srgb, var(--ink, #111) 5%, transparent)); border-
|
|
56
|
-
.question-cards li:first-child { border-
|
|
54
|
+
.slide-body .question-cards li { padding-inline-start: 22px; }
|
|
55
|
+
.question-cards li { margin: 0; padding: 18px 22px; border-radius: 10px; background: var(--surface, color-mix(in srgb, var(--ink, #111) 5%, transparent)); border-inline-start: 5px solid var(--accent, #0d9488); font-size: .8em; line-height: 1.35; overflow-wrap: anywhere; animation: poll-in .4s ease both; }
|
|
56
|
+
.question-cards li:first-child { border-inline-start-color: var(--accent-2, var(--accent, #0d9488)); }
|
|
57
57
|
.question-empty { margin: 0; color: var(--muted, #666); font-style: italic; }
|
|
58
58
|
@keyframes poll-in { from { opacity: 0; transform: translateY(8px); } }
|
|
59
59
|
|
package/src/core/editDeck.js
CHANGED
|
@@ -135,12 +135,13 @@ export function setSlideMeta(deck, slideId, patch) {
|
|
|
135
135
|
if (yamlText) return edit(deck, [{ ...range, text: yamlText, expected: current }])
|
|
136
136
|
return edit(deck, [{ start: slide.source.start, end: lineEnd(deck.source, range.end), text: '' }])
|
|
137
137
|
}
|
|
138
|
-
// Legacy `---` metadata: keep its form while a slide key leads, otherwise
|
|
138
|
+
// Legacy `---` metadata: keep its form while a slide-only key leads, otherwise
|
|
139
139
|
// convert it into an explicit :::meta block (consuming the inner `---`).
|
|
140
140
|
const yamlText = patchYamlMapping(current, patch, { newline: nl, leadKeys: SLIDE_KEYS })
|
|
141
141
|
const end = slide.bodySource ? slide.bodySource.start : range.end
|
|
142
142
|
if (!yamlText) return slide.bodySource ? edit(deck, [{ start: range.start, end, text: '' }]) : removeSlide(deck, slideId).source
|
|
143
|
-
|
|
143
|
+
const lead = firstYamlKey(yamlText)
|
|
144
|
+
if (SLIDE_KEYS.has(lead) && !DECK_KEYS.has(lead)) return edit(deck, [{ ...range, text: yamlText, expected: current }])
|
|
144
145
|
return edit(deck, [{ start: range.start, end, text: block(yamlText) }])
|
|
145
146
|
}
|
|
146
147
|
|
package/src/core/icons.js
CHANGED
|
@@ -59,6 +59,7 @@ export const ICONS = {
|
|
|
59
59
|
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>', // moon
|
|
60
60
|
'fullscreen-exit': '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>', // minimize
|
|
61
61
|
play: '<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>', // play
|
|
62
|
+
hotspots: '<path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="3"/><path d="m16 16-1.9-1.9"/>', // scan-search
|
|
62
63
|
pause: '<rect x="14" y="3" width="5" height="18" rx="1"/><rect x="5" y="3" width="5" height="18" rx="1"/>', // pause
|
|
63
64
|
loading: '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>', // loader-circle
|
|
64
65
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// A deck in several languages is one file per language, side by side:
|
|
2
|
+
// talk.en.md and talk.de.md. The file name says the language; the part before
|
|
3
|
+
// it names the deck, so files with the same name in other languages belong
|
|
4
|
+
// together. A language is a two-letter code, with a region if needed (de-CH).
|
|
5
|
+
|
|
6
|
+
const LANGUAGE_FILE = /^(.+)\.([a-z]{2}(?:-[A-Za-z0-9]{2,8})?)\.md$/i
|
|
7
|
+
|
|
8
|
+
let names = null
|
|
9
|
+
try { names = new Intl.DisplayNames(['en'], { type: 'language', fallback: 'none' }) } catch {}
|
|
10
|
+
// `talk.v1.md` or `talk.ok.md` is not a language; Intl knows which codes are.
|
|
11
|
+
const isLanguage = code => {
|
|
12
|
+
try { return !names || names.of(code) !== undefined } catch { return false }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `{ name, lang }` for `talk.de.md`, or null when the file name names no language. */
|
|
16
|
+
export function languageOfFile(fileName) {
|
|
17
|
+
const match = LANGUAGE_FILE.exec(fileName)
|
|
18
|
+
if (!match || !isLanguage(match[2])) return null
|
|
19
|
+
return { name: match[1], lang: match[2] }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The languages among `fileNames` (one folder's files) that `fileName` is
|
|
24
|
+
* written in, as `[{ lang, file }]` sorted by language, or an empty list when
|
|
25
|
+
* there is no second language.
|
|
26
|
+
*/
|
|
27
|
+
export function languageFiles(fileName, fileNames) {
|
|
28
|
+
const own = languageOfFile(fileName)
|
|
29
|
+
if (!own) return []
|
|
30
|
+
const found = fileNames
|
|
31
|
+
.map(file => ({ file, language: languageOfFile(file) }))
|
|
32
|
+
.filter(({ language }) => language && language.name === own.name)
|
|
33
|
+
.map(({ file, language }) => ({ lang: language.lang, file }))
|
|
34
|
+
.sort((a, b) => a.lang.localeCompare(b.lang))
|
|
35
|
+
return found.length > 1 ? found : []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Scripts written from right to left, by language; Intl knows it where the
|
|
39
|
+
// browser offers getTextInfo (not Firefox).
|
|
40
|
+
const RIGHT_TO_LEFT = new Set(['ar', 'arc', 'ckb', 'dv', 'fa', 'he', 'iw', 'ks', 'ku', 'nqo', 'ps', 'sd', 'syr', 'ug', 'ur', 'yi'])
|
|
41
|
+
|
|
42
|
+
/** 'rtl' for a language written from right to left (fa, ar, he …), else 'ltr'. */
|
|
43
|
+
export function textDirection(lang) {
|
|
44
|
+
if (typeof lang !== 'string' || !lang.trim()) return 'ltr'
|
|
45
|
+
try {
|
|
46
|
+
const locale = new Intl.Locale(lang.trim())
|
|
47
|
+
const info = locale.getTextInfo?.() ?? locale.textInfo
|
|
48
|
+
if (info?.direction) return info.direction
|
|
49
|
+
} catch {}
|
|
50
|
+
return RIGHT_TO_LEFT.has(lang.trim().toLowerCase().split('-')[0]) ? 'rtl' : 'ltr'
|
|
51
|
+
}
|
package/src/core/parseSlides.js
CHANGED
|
@@ -2,8 +2,8 @@ import yaml from 'js-yaml'
|
|
|
2
2
|
import { sourceLines, fenceState, scanDirectives, diagnostic } from './source.js'
|
|
3
3
|
export { sourceLines, fenceState } from './source.js'
|
|
4
4
|
|
|
5
|
-
export const DECK_KEYS = new Set('theme palette appearance params meta width height show lang labels callouts reader components server session bibliography csl nocite'.split(' '))
|
|
6
|
-
export const SLIDE_KEYS = new Set('layout id title section number part description label image alt overlay eyebrow attribution props'.split(' '))
|
|
5
|
+
export const DECK_KEYS = new Set('theme palette appearance params meta width height show lang labels callouts reader components server session bibliography csl nocite hotspots'.split(' '))
|
|
6
|
+
export const SLIDE_KEYS = new Set('layout id title section number part description label image alt overlay eyebrow attribution props hotspots'.split(' '))
|
|
7
7
|
|
|
8
8
|
// Names from before 2.0. They are still recognised, so that an old deck gets a
|
|
9
9
|
// message naming the new setting (see validateDeck) instead of being read as
|
|
@@ -43,6 +43,18 @@ function segmentsOf(source) {
|
|
|
43
43
|
return segments.filter(s => source.slice(s.start, s.end).trim())
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// The first block is the deck's settings when its first key is only a deck
|
|
47
|
+
// key. A key both may have (hotspots) leaves it to the others: a slide's own
|
|
48
|
+
// key makes it the first slide's, otherwise it is the deck's.
|
|
49
|
+
function isDeckBlock(text) {
|
|
50
|
+
const keys = sourceLines(text).map(line => line.text.match(/^([\w-]+):/)?.[1]).filter(Boolean)
|
|
51
|
+
const deckOnly = key => ANY_DECK_KEYS.has(key) && !SLIDE_KEYS.has(key)
|
|
52
|
+
if (!keys.length || deckOnly(keys[0])) return !!keys.length
|
|
53
|
+
if (!ANY_DECK_KEYS.has(keys[0])) return false
|
|
54
|
+
if (keys.some(deckOnly)) return true
|
|
55
|
+
return !keys.some(key => SLIDE_KEYS.has(key) && !ANY_DECK_KEYS.has(key))
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
export function looksLikeMeta(text, keys, { frontmatter = false } = {}) {
|
|
47
59
|
const first = sourceLines(text).find(line => line.text.trim() && !/^\s*#/.test(line.text))
|
|
48
60
|
const key = first?.text.match(/^([\w-]+):/)
|
|
@@ -109,7 +121,7 @@ export function parseSlides(source) {
|
|
|
109
121
|
let deckConfig = {}
|
|
110
122
|
let configSource = null
|
|
111
123
|
let i = 0
|
|
112
|
-
if (segments[0] && /^\s*---[ \t]*\r?\n/.test(source) && looksLikeMeta(text(segments[0]), ANY_DECK_KEYS, { frontmatter: true }) &&
|
|
124
|
+
if (segments[0] && /^\s*---[ \t]*\r?\n/.test(source) && looksLikeMeta(text(segments[0]), ANY_DECK_KEYS, { frontmatter: true }) && isDeckBlock(text(segments[0]))) {
|
|
113
125
|
deckConfig = readMeta(segments[0])
|
|
114
126
|
configSource = { start: segments[0].start, end: segments[0].end }
|
|
115
127
|
i++
|
package/src/core/validateDeck.js
CHANGED
|
@@ -5,6 +5,9 @@ import { LABEL_KEYS, CALLOUT_TYPES } from './labels.js'
|
|
|
5
5
|
import { resolveLayoutProps, propertyErrors } from '../layouts/layoutProps.js'
|
|
6
6
|
import { movedHint } from '../extensions/moved.js'
|
|
7
7
|
|
|
8
|
+
// Hotspots (runtime/hotspots.js): the deck's setting and a slide's own.
|
|
9
|
+
const HOTSPOT_SETTINGS = ['auto', 'marked', 'off', true, false]
|
|
10
|
+
|
|
8
11
|
// `layouts`, `themes` and `palettes` are manifest maps from the extension
|
|
9
12
|
// registry (see src/extensions/discover.js). Checks for a kind are skipped when
|
|
10
13
|
// its map is not supplied.
|
|
@@ -47,6 +50,7 @@ export function validateDeck(deck, { layouts = null, themes = null, palettes = n
|
|
|
47
50
|
for (const key of Object.keys(config.session)) if (!['id', 'code'].includes(key)) add('invalid-config', `session.${key} is not a setting; available: id, code`, deck.configSource?.start, 'warning')
|
|
48
51
|
if (config.session.code != null && !/^\d{4,8}$/.test(String(config.session.code))) add('invalid-config', 'session.code must be 4 to 8 digits', deck.configSource?.start)
|
|
49
52
|
}
|
|
53
|
+
if (config.hotspots != null && !HOTSPOT_SETTINGS.includes(config.hotspots)) add('invalid-config', 'hotspots must be auto, marked, off, true or false', deck.configSource?.start)
|
|
50
54
|
const enums = { organization: ['title', 'all', 'none'], author: ['title', 'all', 'none'], numbers: ['slides', 'all', 'none'], sections: ['all', 'none'] }
|
|
51
55
|
const switches = ['citations']
|
|
52
56
|
if (isPlainObject(config.show)) {
|
|
@@ -84,6 +88,7 @@ export function validateDeck(deck, { layouts = null, themes = null, palettes = n
|
|
|
84
88
|
}
|
|
85
89
|
if (slide.meta.props != null && !isPlainObject(slide.meta.props)) add('invalid-props', 'props must be a mapping', offset)
|
|
86
90
|
if (slide.meta.overlay != null && typeof slide.meta.overlay !== 'boolean') add('invalid-metadata', 'overlay must be true or false', offset)
|
|
91
|
+
if (slide.meta.hotspots != null && !HOTSPOT_SETTINGS.includes(slide.meta.hotspots)) add('invalid-metadata', 'hotspots must be auto, marked, off, true or false', offset)
|
|
87
92
|
for (const node of directivesIn(deck.source.slice(slide.source.start, slide.source.end))) {
|
|
88
93
|
if (DIRECTIVES.includes(node.name) || calloutTypes.includes(node.name.toLowerCase())) continue
|
|
89
94
|
const near = closest(node.name.toLowerCase(), [...DIRECTIVES, ...calloutTypes])
|
|
@@ -9,6 +9,7 @@ import { Diagnostics } from './Diagnostics.jsx'
|
|
|
9
9
|
import { PalettePicker } from './PalettePicker.jsx'
|
|
10
10
|
import { LookThumbnail } from './LookThumbnail.jsx'
|
|
11
11
|
import { designUrl, DESIGN_TAB } from '../designLink.js'
|
|
12
|
+
import { normalMode } from '../../runtime/hotspots.js'
|
|
12
13
|
|
|
13
14
|
// What the `show` setting controls: the label for each choice and its values.
|
|
14
15
|
const SHOW = { organization: ['Organization', ['title', 'all', 'none']], author: ['Author and date', ['title', 'all', 'none']], numbers: ['Slide numbers', ['slides', 'all', 'none']], sections: ['Section labels', ['all', 'none']] }
|
|
@@ -93,6 +94,9 @@ export function DeckSettings({ state, edit }) {
|
|
|
93
94
|
{Object.entries(SHOW).map(([key, [label, options]]) => <Field key={key} label={label}>
|
|
94
95
|
<select value={config.show?.[key] ?? ''} onChange={e => show(key, e.currentTarget.value)}><option value="">default</option>{options.map(o => <option key={o} value={o}>{o}</option>)}</select>
|
|
95
96
|
</Field>)}
|
|
97
|
+
<Field label="Hotspots" hint="1–9 zoom in on a slide's pictures, code, tables and other parts while presenting; marked: only parts marked data-hotspot">
|
|
98
|
+
<select value={[null, 'off'].includes(normalMode(config.hotspots)) ? '' : normalMode(config.hotspots)} onChange={e => text('hotspots', e.currentTarget.value)}><option value="">off</option>{['auto', 'marked'].map(o => <option key={o} value={o}>{o}</option>)}</select>
|
|
99
|
+
</Field>
|
|
96
100
|
<Field label="Language" hint="of the deck, e.g. en, de or de-CH"><input type="text" value={config.lang ?? ''} placeholder="en" onInput={e => text('lang', e.currentTarget.value)} /></Field>
|
|
97
101
|
</div>
|
|
98
102
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { h } from 'preact'
|
|
2
2
|
import { setSlideMeta, setSlideNotes } from '../../core/editDeck.js'
|
|
3
3
|
import { Field, TextArea } from './Field.jsx'
|
|
4
|
+
import { normalMode } from '../../runtime/hotspots.js'
|
|
4
5
|
|
|
5
6
|
const MORE = [['section', 'Section label shown in the header'], ['number', 'Chapter number'], ['part', 'Part name for chapter slides'], ['label', 'Replaces the word "Chapter"'], ['description', 'Chapter description'], ['eyebrow', 'Small line above a heading'], ['attribution', 'Source of a quote']]
|
|
6
7
|
|
|
@@ -13,6 +14,12 @@ export function MetaFields({ slide, slideEdit }) {
|
|
|
13
14
|
}} />
|
|
14
15
|
return <>
|
|
15
16
|
<Field label="Slide id" hint="stable anchor and link target">{text('id')}</Field>
|
|
17
|
+
<Field label="Hotspots" hint="number keys zoom in on this slide's parts while presenting; default: the deck's setting">
|
|
18
|
+
<select value={normalMode(meta.hotspots) ?? ''} onChange={event => {
|
|
19
|
+
const value = event.currentTarget.value
|
|
20
|
+
slideEdit((deck, id) => setSlideMeta(deck, id, { hotspots: value === '' ? undefined : value === 'off' ? false : value }), { group: 'meta:hotspots' })
|
|
21
|
+
}}><option value="">the deck's</option>{['auto', 'marked', 'off'].map(o => <option key={o} value={o}>{o}</option>)}</select>
|
|
22
|
+
</Field>
|
|
16
23
|
<Field label="Speaker notes"><TextArea rows={4} value={slide.meta.notes ?? ''} onInput={value => slideEdit((deck, id) => setSlideNotes(deck, id, value), { group: 'notes' })} /></Field>
|
|
17
24
|
<details class="more" open={MORE.some(([key]) => meta[key] != null)}>
|
|
18
25
|
<summary>More metadata</summary>
|
|
@@ -5,6 +5,7 @@ import { marked, Parser } from 'marked'
|
|
|
5
5
|
import { registry } from '../runtime/registry'
|
|
6
6
|
import { scanTags, closeTags } from '../core/tags.js'
|
|
7
7
|
import { expandPollLists } from '../core/pollList.js'
|
|
8
|
+
import { textDirection } from '../core/languages.js'
|
|
8
9
|
import { InkLayer } from '../runtime/ink/InkLayer.jsx'
|
|
9
10
|
import themedSvgs from 'virtual:deck-svgs'
|
|
10
11
|
import { renderCitations, citationsHtml as referencesOf, bibliographyOf } from '../runtime/citations.js'
|
|
@@ -222,7 +223,12 @@ export function SlideFrame({ meta = {}, props = {}, deckConfig = {}, index = 0,
|
|
|
222
223
|
const classes = ['slide', layout && 'slide--' + String(layout).replace(/[^a-zA-Z0-9_-]/g, '-'),
|
|
223
224
|
image && ['title', 'chapter'].includes(layout) && 'has-image',
|
|
224
225
|
layout === 'full-bleed-image' && props.overlay && 'has-overlay'].filter(Boolean).join(' ')
|
|
225
|
-
|
|
226
|
+
// A deck in Farsi, Arabic or Hebrew runs from right to left; mdeck's own
|
|
227
|
+
// controls around the slides keep their direction.
|
|
228
|
+
const dir = textDirection(deckConfig.lang) === 'rtl' ? 'rtl' : undefined
|
|
229
|
+
// Its own hotspots setting, if any (runtime/hotspots.js).
|
|
230
|
+
const hotspots = meta.hotspots == null ? undefined : String(meta.hotspots)
|
|
231
|
+
return <section class={classes} dir={dir} data-slide-id={id} data-label={`${slideNum(index)} ${manifest.title}`} data-hotspots={hotspots}>
|
|
226
232
|
{frame !== 'none' && <SlideHeader deckConfig={deckConfig} logo={title ? deckConfig.meta?.logo : undefined}
|
|
227
233
|
isTitle={title} right={frame === 'chapter' ? meta.part ?? '' : meta.section ?? ''} />}
|
|
228
234
|
{children}
|
|
@@ -21,7 +21,7 @@ body { min-height: 100vh; display: flex; flex-direction: column; font: 18px/1.45
|
|
|
21
21
|
.answer-scale { grid-template-columns: repeat(auto-fit, minmax(48px, 1fr)); }
|
|
22
22
|
.answer button {
|
|
23
23
|
padding: 18px 20px; border: 2px solid var(--rule); border-radius: 14px; background: var(--bg); color: var(--ink);
|
|
24
|
-
font: inherit; font-size: 20px; font-weight: 600; text-align:
|
|
24
|
+
font: inherit; font-size: 20px; font-weight: 600; text-align: start; cursor: pointer; -webkit-tap-highlight-color: transparent;
|
|
25
25
|
}
|
|
26
26
|
.answer-scale button { padding: 16px 0; text-align: center; }
|
|
27
27
|
.answer button:hover { border-color: color-mix(in srgb, var(--accent) 60%, var(--rule)); }
|
|
@@ -39,7 +39,7 @@ footer { padding: 16px 20px; text-align: center; color: var(--muted); font-size:
|
|
|
39
39
|
.answer-keys { display: grid; grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); gap: 12px; }
|
|
40
40
|
.answer .answer-keys button { padding: 22px 0; text-align: center; font-size: 28px; }
|
|
41
41
|
/* `multiple`: a box that is ticked when picked. */
|
|
42
|
-
.answer-check { display: inline-block; width: 1em; height: 1em; margin-
|
|
42
|
+
.answer-check { display: inline-block; width: 1em; height: 1em; margin-inline-end: 12px; border: 2px solid currentColor; border-radius: 4px; vertical-align: -.12em; opacity: .6; }
|
|
43
43
|
.answer button.is-picked .answer-check { opacity: 1; background: currentColor; box-shadow: inset 0 0 0 2px var(--accent); }
|
|
44
44
|
/* Closed: no more answers. */
|
|
45
45
|
.answer button:disabled, .answer textarea:disabled, .answer input:disabled { opacity: .45; cursor: default; }
|
package/src/runtime/chrome.jsx
CHANGED
|
@@ -42,14 +42,13 @@ export const S = {
|
|
|
42
42
|
},
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// A palette as the theme gallery shows it (mdeck-themes): one chip of its
|
|
46
|
+
// background, accent and second accent.
|
|
45
47
|
export function PaletteSwatches({ tokens }) {
|
|
46
|
-
const
|
|
48
|
+
const stripe = key => <i style={{ display: 'block', background: tokens[key] ?? tokens['--accent'] ?? 'transparent' }} />
|
|
47
49
|
return (
|
|
48
|
-
<
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
: null
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
50
|
+
<span aria-hidden="true" style={{ display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', width: '30px', height: '18px', borderRadius: '999px', overflow: 'hidden', border: '1px solid rgba(127,127,127,0.35)', flexShrink: 0 }}>
|
|
51
|
+
{stripe('--bg')}{stripe('--accent')}{stripe('--accent-2')}
|
|
52
|
+
</span>
|
|
54
53
|
)
|
|
55
54
|
}
|