mdeck 3.6.1 → 3.7.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 +11 -0
- package/README.md +44 -2
- 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/assets/site.css +21 -0
- package/docs/site/assets/site.js +17 -7
- package/docs/site/build.js +15 -6
- package/docs/site/content/drawing.md +1 -1
- package/docs/site/content/sharing.md +1 -1
- package/docs/site/content/theme-authoring.md +6 -3
- 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/check.js +1 -0
- package/src/cli/themes.js +1 -1
- package/src/components/callout.css +11 -21
- package/src/core/labels.js +6 -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/ReaderView.jsx +21 -12
- package/src/runtime/chrome.jsx +1 -1
- package/src/runtime/deck-stage.js +5 -1
- package/src/runtime/reader.css +2 -0
- package/docs/site/examples/notebook.svg +0 -58
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,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
|
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',
|
|
@@ -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()) {
|
|
@@ -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>}
|
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]
|
|
@@ -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; }
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000">
|
|
2
|
-
<!-- An open lab notebook on a dark desk: ruled pages with entries, a results table, a plot and a pen.
|
|
3
|
-
The lower part of the picture stays dark so a heading can sit on it. -->
|
|
4
|
-
<defs>
|
|
5
|
-
<linearGradient id="desk" x1="0" y1="0" x2="0" y2="1">
|
|
6
|
-
<stop offset="0" stop-color="#4a3b30"/>
|
|
7
|
-
<stop offset="1" stop-color="#211a15"/>
|
|
8
|
-
</linearGradient>
|
|
9
|
-
<linearGradient id="spine" x1="0" x2="1">
|
|
10
|
-
<stop offset="0" stop-color="#fbf8f1" stop-opacity="0"/>
|
|
11
|
-
<stop offset=".5" stop-color="#b9ad98"/>
|
|
12
|
-
<stop offset="1" stop-color="#fbf8f1" stop-opacity="0"/>
|
|
13
|
-
</linearGradient>
|
|
14
|
-
<pattern id="rule" width="10" height="40" patternUnits="userSpaceOnUse">
|
|
15
|
-
<line x1="0" y1="39.5" x2="10" y2="39.5" stroke="#bfd3e6" stroke-width="2"/>
|
|
16
|
-
</pattern>
|
|
17
|
-
</defs>
|
|
18
|
-
<rect width="1600" height="1000" fill="url(#desk)"/>
|
|
19
|
-
<g transform="translate(260 40) scale(.68) rotate(-3 800 500)">
|
|
20
|
-
<rect x="150" y="90" width="1300" height="830" rx="14" fill="#1b2533"/>
|
|
21
|
-
<rect x="170" y="105" width="625" height="800" fill="#fbf8f1"/>
|
|
22
|
-
<rect x="805" y="105" width="625" height="800" fill="#fbf8f1"/>
|
|
23
|
-
<rect x="770" y="105" width="60" height="800" fill="url(#spine)"/>
|
|
24
|
-
<rect x="170" y="180" width="625" height="700" fill="url(#rule)"/>
|
|
25
|
-
<rect x="805" y="180" width="625" height="700" fill="url(#rule)"/>
|
|
26
|
-
<line x1="240" y1="105" x2="240" y2="905" stroke="#e39a9a" stroke-width="3"/>
|
|
27
|
-
<line x1="875" y1="105" x2="875" y2="905" stroke="#e39a9a" stroke-width="3"/>
|
|
28
|
-
<g fill="none" stroke="#24304a" stroke-width="5" stroke-linecap="round">
|
|
29
|
-
<path d="M262 165 h120 M402 165 h70"/>
|
|
30
|
-
<path d="M262 245 h300" stroke-width="7"/>
|
|
31
|
-
<path d="M262 325 h420 M262 365 h380 M262 405 h300"/>
|
|
32
|
-
<path d="M262 485 h30 M310 485 h300 M262 525 h30 M310 525 h340 M262 565 h30 M310 565 h260 M262 605 h30 M310 605 h320"/>
|
|
33
|
-
<path d="M262 685 h400 M262 725 h360"/>
|
|
34
|
-
<path d="M262 805 h250" stroke="#b5452f"/>
|
|
35
|
-
</g>
|
|
36
|
-
<path d="M520 798 q20 -18 40 0 t40 0" fill="none" stroke="#b5452f" stroke-width="4" stroke-linecap="round"/>
|
|
37
|
-
<g fill="none" stroke="#24304a" stroke-width="4" stroke-linecap="round">
|
|
38
|
-
<path d="M897 165 h160"/>
|
|
39
|
-
<rect x="897" y="220" width="460" height="200"/>
|
|
40
|
-
<path d="M897 270 h460 M897 320 h460 M897 370 h460 M1050 220 v200 M1200 220 v200"/>
|
|
41
|
-
<path d="M915 247 h90 M1068 247 h90 M1218 247 h90" stroke-width="5"/>
|
|
42
|
-
<path d="M915 297 h60 M1068 297 h70 M1218 297 h50 M915 347 h60 M1068 347 h40 M1218 347 h80 M915 397 h60 M1068 397 h60 M1218 397 h60" stroke-width="3"/>
|
|
43
|
-
<path d="M917 820 V480 M917 820 H1360"/>
|
|
44
|
-
<path d="M930 800 C1010 790 1060 700 1110 610 S1240 500 1340 495" stroke="#2455c7" stroke-width="5"/>
|
|
45
|
-
</g>
|
|
46
|
-
<g fill="#2455c7">
|
|
47
|
-
<circle cx="960" cy="792" r="9"/><circle cx="1040" cy="742" r="9"/><circle cx="1105" cy="628" r="9"/>
|
|
48
|
-
<circle cx="1175" cy="545" r="9"/><circle cx="1260" cy="510" r="9"/><circle cx="1335" cy="500" r="9"/>
|
|
49
|
-
</g>
|
|
50
|
-
<g transform="rotate(32 1260 690)">
|
|
51
|
-
<rect x="1000" y="676" width="430" height="30" rx="14" fill="#1d2735"/>
|
|
52
|
-
<rect x="1330" y="676" width="34" height="30" fill="#c9a24b"/>
|
|
53
|
-
<path d="M1000 676 l-46 15 l46 15 z" fill="#e8dcc4"/>
|
|
54
|
-
<path d="M954 691 l14 -4 v8 z" fill="#1d2735"/>
|
|
55
|
-
<rect x="1080" y="668" width="150" height="10" rx="5" fill="#c9a24b"/>
|
|
56
|
-
</g>
|
|
57
|
-
</g>
|
|
58
|
-
</svg>
|