fb-slides 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/bin/fb-slides.mjs +3 -2
- package/lib/build.mjs +15 -1
- package/lib/config.mjs +19 -2
- package/lib/dev.mjs +31 -3
- package/lib/render.mjs +10 -0
- package/lib/server.mjs +7 -1
- package/lib/theme.mjs +57 -0
- package/package.json +1 -1
- package/runtime/annotate.js +36 -12
- package/runtime/index.html +1 -1
- package/runtime/theme.base.css +34 -0
- package/templates/starter/slides.config.js +12 -0
package/README.md
CHANGED
|
@@ -151,6 +151,8 @@ export default {
|
|
|
151
151
|
decks: 'decks', // folder of .md
|
|
152
152
|
demos: 'demo', // folder behind a bare `<!-- demo: name -->`
|
|
153
153
|
static: ['assets', 'demo'], // served and published; auto-detected when omitted
|
|
154
|
+
revealTheme: 'dracula', // one of reveal.js's own themes; omit for this one
|
|
155
|
+
webfonts: false, // let the reveal themes fetch their Google fonts
|
|
154
156
|
theme: 'theme.css', // loaded after the base theme; auto when it exists
|
|
155
157
|
favicon: 'assets/favicon.png',
|
|
156
158
|
|
|
@@ -193,6 +195,34 @@ rather than replaces. Almost everything hangs off the tokens in `:root`:
|
|
|
193
195
|
}
|
|
194
196
|
```
|
|
195
197
|
|
|
198
|
+
### reveal.js themes
|
|
199
|
+
|
|
200
|
+
The fifteen themes from [revealjs.com/themes](https://revealjs.com/themes/) are all in the
|
|
201
|
+
box — pick one by name and nothing else changes:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
export default { revealTheme: 'dracula' };
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`beige` · `black` · `black-contrast` · `blood` · `dracula` · `league` · `moon` · `night` ·
|
|
208
|
+
`serif` · `simple` · `sky` · `solarized` · `white` · `white-contrast`
|
|
209
|
+
|
|
210
|
+
Only the chosen one is ever loaded — one `<link>`, ~7 KB — and the build copies that file
|
|
211
|
+
and nothing else. Two things happen beyond the link:
|
|
212
|
+
|
|
213
|
+
**It is served from the deck, not from a CDN.** Six of the themes open with
|
|
214
|
+
`@import url(https://fonts.googleapis.com/…)`. Those lines are stripped, so a talk still
|
|
215
|
+
renders with the wifi off; the theme falls back to the next font in its own stack. Pass
|
|
216
|
+
`webfonts: true` to let them through and get the typography of the previews exactly. The
|
|
217
|
+
font folders reveal ships itself — league-gothic, source-sans-pro — are copied into
|
|
218
|
+
`dist/` when the theme asks for them.
|
|
219
|
+
|
|
220
|
+
**The chrome follows the theme.** The navigator, the badge and the signature are not
|
|
221
|
+
`.reveal` elements, so a borrowed theme would leave a black panel down the side of a white
|
|
222
|
+
deck. Every reveal 5 theme declares its palette as `--r-*` custom properties, and the base
|
|
223
|
+
theme points its own tokens at them, so the whole page moves together. A `theme.css` in the
|
|
224
|
+
project still has the last word over both.
|
|
225
|
+
|
|
196
226
|
## Publishing
|
|
197
227
|
|
|
198
228
|
```bash
|
package/bin/fb-slides.mjs
CHANGED
|
@@ -69,11 +69,12 @@ const run = async () => {
|
|
|
69
69
|
|
|
70
70
|
const { loadConfig } = await import('../lib/config.mjs');
|
|
71
71
|
const root = process.cwd();
|
|
72
|
-
const
|
|
72
|
+
const reload = () => loadConfig(root, overrides());
|
|
73
|
+
const config = await reload();
|
|
73
74
|
|
|
74
75
|
if (command === 'dev') {
|
|
75
76
|
const { dev } = await import('../lib/dev.mjs');
|
|
76
|
-
return void (await dev(config, RUNTIME));
|
|
77
|
+
return void (await dev(config, RUNTIME, reload));
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if (command === 'build') {
|
package/lib/build.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
|
14
14
|
import { assertUsable } from './config.mjs';
|
|
15
15
|
import { listDecks } from './decks.mjs';
|
|
16
16
|
import { renderIndex } from './render.mjs';
|
|
17
|
+
import { REVEAL_THEME_URL, readRevealTheme } from './theme.mjs';
|
|
17
18
|
import { VENDOR_FILES, VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
18
19
|
|
|
19
20
|
const copyFile = async (from, to) => {
|
|
@@ -59,6 +60,18 @@ export const build = async (config, runtimeDir, { quiet = false } = {}) => {
|
|
|
59
60
|
await cp(from, join(out, dir), { recursive: true, filter: (src) => !excluded(src) });
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// 3b. the reveal theme, rewritten, with the font folders it imports — and
|
|
64
|
+
// only those: source-sans-pro is 1.8 MB and most themes never ask for it.
|
|
65
|
+
if (config.revealTheme) {
|
|
66
|
+
const { css, fonts, dir } = await readRevealTheme(config.revealTheme, { webfonts: config.webfonts });
|
|
67
|
+
await writeFile(join(out, REVEAL_THEME_URL), css);
|
|
68
|
+
for (const font of fonts) {
|
|
69
|
+
await cp(join(dir, 'fonts', font), join(out, 'vendor', VENDOR_MOUNTS['reveal.js'], 'dist/theme/fonts', font), {
|
|
70
|
+
recursive: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
// 4. loose root files the config names — a theme override, a favicon.
|
|
63
76
|
for (const file of [config.theme, config.favicon].filter(Boolean)) {
|
|
64
77
|
const from = resolve(config.root, file);
|
|
@@ -73,7 +86,8 @@ export const build = async (config, runtimeDir, { quiet = false } = {}) => {
|
|
|
73
86
|
await writeFile(join(out, 'decks.json'), `${JSON.stringify(decks, null, 2)}\n`);
|
|
74
87
|
|
|
75
88
|
if (!quiet) {
|
|
76
|
-
|
|
89
|
+
const theme = config.revealTheme ? ` · theme ${config.revealTheme}` : '';
|
|
90
|
+
console.log(` ${config.outDir}/ ← ${decks.length} decks · ${config.static.join(', ') || 'no static dirs'}${theme} · ${Date.now() - started}ms`);
|
|
77
91
|
}
|
|
78
92
|
return { decks, out };
|
|
79
93
|
};
|
package/lib/config.mjs
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
// needs running. A project that follows the default shape needs no config at all.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
import { existsSync } from 'node:fs';
|
|
9
|
+
import { existsSync, statSync } from 'node:fs';
|
|
10
10
|
import { basename, join, resolve } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
|
|
13
|
+
import { assertRevealTheme } from './theme.mjs';
|
|
14
|
+
|
|
13
15
|
const CONFIG_FILES = ['slides.config.js', 'slides.config.mjs', 'slides.config.json'];
|
|
14
16
|
|
|
15
17
|
// Directories a project gets served and published for free when it has them, so
|
|
@@ -22,7 +24,10 @@ export const ALWAYS_EXCLUDED = ['node_modules', '.git', '.angular', '.next', '.c
|
|
|
22
24
|
export const findConfigFile = (root) => CONFIG_FILES.map((name) => join(root, name)).find(existsSync) ?? null;
|
|
23
25
|
|
|
24
26
|
const importConfig = async (file) => {
|
|
25
|
-
|
|
27
|
+
// ESM caches a module by URL for the life of the process, so the dev server's
|
|
28
|
+
// second read of an edited config would be the first one again. The mtime in
|
|
29
|
+
// the query makes it a different URL exactly when the file has changed.
|
|
30
|
+
const url = `${pathToFileURL(file).href}?t=${statSync(file).mtimeMs}`;
|
|
26
31
|
const module = file.endsWith('.json')
|
|
27
32
|
? await import(url, { with: { type: 'json' } })
|
|
28
33
|
: await import(url);
|
|
@@ -53,6 +58,14 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
53
58
|
decksDir,
|
|
54
59
|
demosDir,
|
|
55
60
|
static: statics,
|
|
61
|
+
// One of reveal's own fifteen — 'dracula', 'sky', 'white'. It lands after
|
|
62
|
+
// the base theme and takes the slides; the chrome follows it through the
|
|
63
|
+
// `--r-*` variables every reveal 5 theme declares. Null: this package's look.
|
|
64
|
+
revealTheme: user.revealTheme ?? null,
|
|
65
|
+
// Six of those themes pull their typefaces from fonts.googleapis.com. Off by
|
|
66
|
+
// default — the deck is served whole or not at all — at the cost of falling
|
|
67
|
+
// back to the theme's second-choice font stack.
|
|
68
|
+
webfonts: user.webfonts ?? false,
|
|
56
69
|
// The project's own CSS, loaded *after* the base theme: an override, not a
|
|
57
70
|
// replacement. `theme.css` at the root is picked up without being declared.
|
|
58
71
|
theme: user.theme ?? (existsSync(join(root, 'theme.css')) ? 'theme.css' : null),
|
|
@@ -72,6 +85,10 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
72
85
|
exclude: [...ALWAYS_EXCLUDED, overrides.outDir ?? user.outDir ?? 'dist', ...(user.exclude ?? [])],
|
|
73
86
|
};
|
|
74
87
|
|
|
88
|
+
// Named early: a typo in the config should be a message, not a deck that
|
|
89
|
+
// silently wears the wrong clothes.
|
|
90
|
+
if (config.revealTheme) await assertRevealTheme(config.revealTheme);
|
|
91
|
+
|
|
75
92
|
config.decksPath = resolve(root, config.decksDir);
|
|
76
93
|
config.outPath = resolve(root, config.outDir);
|
|
77
94
|
// What the browser asks for, as opposed to where it is on disk.
|
package/lib/dev.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { join, resolve } from 'node:path';
|
|
|
13
13
|
import { assertUsable } from './config.mjs';
|
|
14
14
|
import { createDeckServer, listen } from './server.mjs';
|
|
15
15
|
import { renderIndex } from './render.mjs';
|
|
16
|
+
import { REVEAL_THEME_URL, readRevealTheme } from './theme.mjs';
|
|
16
17
|
import { VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
17
18
|
|
|
18
19
|
const OPEN = { darwin: 'open', win32: 'start' };
|
|
@@ -52,8 +53,25 @@ const startSideServer = (spec, root, children) => {
|
|
|
52
53
|
});
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// What the page is built from, read again on every request. A config with a
|
|
57
|
+
// syntax error in it — the state it is in halfway through an edit — leaves the
|
|
58
|
+
// last good one standing rather than serving a broken deck.
|
|
59
|
+
const reader = (config, reload) => {
|
|
60
|
+
let good = config;
|
|
61
|
+
return async () => {
|
|
62
|
+
if (!reload) return good;
|
|
63
|
+
try {
|
|
64
|
+
good = await reload();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn(` ⚠ slides.config: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
return good;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const dev = async (config, runtimeDir, reload) => {
|
|
56
73
|
assertUsable(config);
|
|
74
|
+
const current = reader(config, reload);
|
|
57
75
|
|
|
58
76
|
const mounts = [
|
|
59
77
|
// The project first: a file next to the decks shadows the one this package
|
|
@@ -69,8 +87,18 @@ export const dev = async (config, runtimeDir) => {
|
|
|
69
87
|
const server = createDeckServer({
|
|
70
88
|
mounts,
|
|
71
89
|
decksPath: config.decksPath,
|
|
72
|
-
// Rendered per request
|
|
73
|
-
|
|
90
|
+
// Rendered per request from a config read per request: editing the title,
|
|
91
|
+
// the theme or the signature and reloading is enough. What was fixed when
|
|
92
|
+
// the server came up — the mounts, the port, the side processes — still
|
|
93
|
+
// needs a restart.
|
|
94
|
+
index: async () => renderIndex(await current(), runtimeDir),
|
|
95
|
+
generated: {
|
|
96
|
+
[`/${REVEAL_THEME_URL}`]: async () => {
|
|
97
|
+
const now = await current();
|
|
98
|
+
if (!now.revealTheme) return '/* no revealTheme in slides.config.js */';
|
|
99
|
+
return (await readRevealTheme(now.revealTheme, { webfonts: now.webfonts })).css;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
74
102
|
});
|
|
75
103
|
|
|
76
104
|
await listen(server, config.port);
|
package/lib/render.mjs
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
+
import { REVEAL_THEME_URL } from './theme.mjs';
|
|
11
|
+
|
|
10
12
|
const escapeHtml = (value) =>
|
|
11
13
|
String(value).replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
12
14
|
|
|
@@ -28,6 +30,9 @@ const signatureHtml = (signature) => {
|
|
|
28
30
|
const headHtml = (config) => {
|
|
29
31
|
const tags = [];
|
|
30
32
|
if (config.favicon) tags.push(`<link rel="icon" href="${escapeHtml(config.favicon)}" />`);
|
|
33
|
+
// After the base theme, before the project's: reveal's theme dresses the
|
|
34
|
+
// slides, and whatever the project says still has the last word.
|
|
35
|
+
if (config.revealTheme) tags.push(`<link rel="stylesheet" href="${REVEAL_THEME_URL}" />`);
|
|
31
36
|
// After the base theme on purpose: the project's CSS overrides, it does not replace.
|
|
32
37
|
if (config.theme) tags.push(`<link rel="stylesheet" href="${escapeHtml(config.theme)}" />`);
|
|
33
38
|
return tags.join('\n ');
|
|
@@ -43,7 +48,12 @@ export const renderIndex = async (config, runtimeDir) => {
|
|
|
43
48
|
fragmentLists: config.fragmentLists,
|
|
44
49
|
};
|
|
45
50
|
|
|
51
|
+
// The chrome reads this to take its colours from the reveal theme instead of
|
|
52
|
+
// the base one — see the bridge at the top of theme.base.css.
|
|
53
|
+
const htmlAttrs = config.revealTheme ? ` data-reveal-theme="${escapeHtml(config.revealTheme)}"` : '';
|
|
54
|
+
|
|
46
55
|
return template
|
|
56
|
+
.replaceAll('{{htmlAttrs}}', htmlAttrs)
|
|
47
57
|
.replaceAll('{{lang}}', escapeHtml(config.lang))
|
|
48
58
|
.replaceAll('{{title}}', escapeHtml(config.title))
|
|
49
59
|
.replaceAll('{{head}}', headHtml(config))
|
package/lib/server.mjs
CHANGED
|
@@ -99,7 +99,9 @@ const sendFile = (req, res, path, size) => {
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
// mounts: [{ prefix: '/', dir }] — checked in order, first hit wins.
|
|
102
|
-
|
|
102
|
+
// generated: { '/path.css': async () => body } — files this package rewrites
|
|
103
|
+
// rather than serves, checked before any mount so node_modules cannot shadow one.
|
|
104
|
+
export const createDeckServer = ({ mounts, index, decksPath, generated = {} }) => {
|
|
103
105
|
const handler = async (req, res) => {
|
|
104
106
|
if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, 'method not allowed');
|
|
105
107
|
|
|
@@ -120,6 +122,10 @@ export const createDeckServer = ({ mounts, index, decksPath }) => {
|
|
|
120
122
|
return send(res, 200, body, { 'Content-Type': MIME['.json'] });
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
if (generated[pathname]) {
|
|
126
|
+
return send(res, 200, await generated[pathname](), { 'Content-Type': mime(pathname) });
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
if (DENIED.test(pathname)) return send(res, 403, 'forbidden', { 'Content-Type': MIME['.txt'] });
|
|
124
130
|
|
|
125
131
|
for (const { prefix, dir } of mounts) {
|
package/lib/theme.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// reveal.js ships fifteen themes; a deck wears one of them, or none.
|
|
3
|
+
//
|
|
4
|
+
// They are not linked straight out of node_modules. Six of the fifteen open with
|
|
5
|
+
// `@import url(https://fonts.googleapis.com/…)`, which is a third-party request
|
|
6
|
+
// on every load — and a talk that has to survive the conference wifi cannot rest
|
|
7
|
+
// on one. The file is rewritten instead: the remote imports go, the font folders
|
|
8
|
+
// reveal already ships locally stay, and what the browser gets is served from
|
|
9
|
+
// this project like everything else.
|
|
10
|
+
//
|
|
11
|
+
// The rewritten file is generated per request in dev and written once by build,
|
|
12
|
+
// at the same URL either way — index.html never learns which of the two it is.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
+
import { basename, join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
19
|
+
|
|
20
|
+
export const REVEAL_THEME_URL = 'reveal-theme.css';
|
|
21
|
+
|
|
22
|
+
const themeDir = () => join(packageDir('reveal.js'), 'dist', 'theme');
|
|
23
|
+
|
|
24
|
+
// The theme is served from the site root, so reveal's own `./fonts/…` would
|
|
25
|
+
// resolve one folder too high: point it at the vendor mount instead.
|
|
26
|
+
const FONT_BASE = `vendor/${VENDOR_MOUNTS['reveal.js']}/dist/theme/fonts/`;
|
|
27
|
+
|
|
28
|
+
const REMOTE_IMPORT = /@import\s+url\(\s*['"]?https?:[^)]*\)\s*;?[ \t]*\n?/gi;
|
|
29
|
+
const LOCAL_FONT_URL = /url\(\s*['"]?\.\/fonts\//gi;
|
|
30
|
+
|
|
31
|
+
export const listRevealThemes = async () =>
|
|
32
|
+
(await readdir(themeDir()))
|
|
33
|
+
.filter((file) => file.endsWith('.css'))
|
|
34
|
+
.map((file) => basename(file, '.css'))
|
|
35
|
+
.sort();
|
|
36
|
+
|
|
37
|
+
export const assertRevealTheme = async (name) => {
|
|
38
|
+
const themes = await listRevealThemes();
|
|
39
|
+
if (themes.includes(name)) return name;
|
|
40
|
+
throw new Error(
|
|
41
|
+
`unknown revealTheme: ${name}\n` +
|
|
42
|
+
` reveal.js ships: ${themes.join(', ')}\n` +
|
|
43
|
+
` drop the key for this package's own theme`,
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// The font folders the chosen theme actually imports. build copies these and
|
|
48
|
+
// nothing else: source-sans-pro alone is 1.8 MB, and most themes never ask for it.
|
|
49
|
+
const fontDirs = (css) => [...new Set([...css.matchAll(/\.\/fonts\/([^/]+)\//g)].map((m) => m[1]))];
|
|
50
|
+
|
|
51
|
+
// `webfonts: true` keeps the Google Fonts imports — the exact typography of
|
|
52
|
+
// revealjs.com/themes, at the cost of the offline guarantee.
|
|
53
|
+
export const readRevealTheme = async (name, { webfonts = false } = {}) => {
|
|
54
|
+
const source = await readFile(join(themeDir(), `${name}.css`), 'utf8');
|
|
55
|
+
const css = (webfonts ? source : source.replace(REMOTE_IMPORT, '')).replace(LOCAL_FONT_URL, `url(${FONT_BASE}`);
|
|
56
|
+
return { css, fonts: fontDirs(source), dir: themeDir() };
|
|
57
|
+
};
|
package/package.json
CHANGED
package/runtime/annotate.js
CHANGED
|
@@ -25,9 +25,11 @@ const COLOURS = [
|
|
|
25
25
|
{ label: 'Black', glow: '#000000', core: '#000000' },
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
// A released stroke rubs itself out from its
|
|
29
|
-
// FADE_MS is how long
|
|
30
|
-
// the erase front softens as it passes —
|
|
28
|
+
// A released stroke holds on screen for HOLD_MS, then rubs itself out from its
|
|
29
|
+
// oldest point towards its newest. FADE_MS is how long the erase takes end to
|
|
30
|
+
// end, FADE_TAIL how much of the stroke the erase front softens as it passes —
|
|
31
|
+
// at 0 it would be a hard wipe.
|
|
32
|
+
const HOLD_MS = 2000;
|
|
31
33
|
const FADE_MS = 1600;
|
|
32
34
|
const FADE_TAIL = 0.35;
|
|
33
35
|
|
|
@@ -210,6 +212,7 @@ const RevealAnnotate = () => ({
|
|
|
210
212
|
const strokes = [];
|
|
211
213
|
let drawing = null;
|
|
212
214
|
let frame = 0;
|
|
215
|
+
let holdTimer = 0;
|
|
213
216
|
|
|
214
217
|
const addPoint = (event) => {
|
|
215
218
|
const points = drawing.points;
|
|
@@ -284,7 +287,12 @@ const RevealAnnotate = () => ({
|
|
|
284
287
|
|
|
285
288
|
// Returns false once the stroke has faded out and can be dropped.
|
|
286
289
|
const paint = (stroke, now) => {
|
|
287
|
-
|
|
290
|
+
// Clamped at 0 through the hold: a stroke waiting its turn paints exactly
|
|
291
|
+
// like one still under the pen, only released.
|
|
292
|
+
const progress =
|
|
293
|
+
stroke.releasedAt === null
|
|
294
|
+
? null
|
|
295
|
+
: Math.max(0, (now - stroke.releasedAt - HOLD_MS) / FADE_MS);
|
|
288
296
|
if (progress !== null && progress >= 1) return false;
|
|
289
297
|
|
|
290
298
|
const box = boundsOf(stroke.points);
|
|
@@ -334,18 +342,27 @@ const RevealAnnotate = () => ({
|
|
|
334
342
|
if (!paint(strokes[i], now)) strokes.splice(i, 1);
|
|
335
343
|
}
|
|
336
344
|
// Only a fading stroke needs the next frame. A live one changes when a
|
|
337
|
-
// point is added and not before,
|
|
338
|
-
//
|
|
339
|
-
|
|
345
|
+
// point is added and not before, and a released one sits untouched
|
|
346
|
+
// through its hold — for those a single timeout wakes the loop when the
|
|
347
|
+
// first fade is due, instead of repainting a full-screen canvas forever.
|
|
348
|
+
const waits = strokes
|
|
349
|
+
.filter((stroke) => stroke.releasedAt !== null)
|
|
350
|
+
.map((stroke) => stroke.releasedAt + HOLD_MS - now);
|
|
351
|
+
if (waits.some((wait) => wait <= 0)) schedule();
|
|
352
|
+
else if (waits.length) holdTimer = setTimeout(schedule, Math.min(...waits));
|
|
340
353
|
};
|
|
341
354
|
|
|
342
355
|
const schedule = () => {
|
|
356
|
+
clearTimeout(holdTimer);
|
|
357
|
+
holdTimer = 0;
|
|
343
358
|
frame ||= requestAnimationFrame(render);
|
|
344
359
|
};
|
|
345
360
|
|
|
346
361
|
const clear = () => {
|
|
347
362
|
drawing = null;
|
|
348
363
|
strokes.length = 0;
|
|
364
|
+
clearTimeout(holdTimer);
|
|
365
|
+
holdTimer = 0;
|
|
349
366
|
ctx.clearRect(0, 0, width, height);
|
|
350
367
|
};
|
|
351
368
|
|
|
@@ -427,14 +444,21 @@ const RevealAnnotate = () => ({
|
|
|
427
444
|
deck.addKeyBinding({ keyCode: KEY_CODE, key: 'P', description: 'Toggle the pen' }, () =>
|
|
428
445
|
setPen(!penOn),
|
|
429
446
|
);
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
447
|
+
// Keys the pen borrows only while it is armed, taken in capture phase so
|
|
448
|
+
// reveal's own bindings never see them: Escape (otherwise the overview
|
|
449
|
+
// toggle) disarms it, and the number row picks its ink — 1 to 6, in the
|
|
450
|
+
// order the swatches sit in the toolbar. The rest of the time both fall
|
|
451
|
+
// through untouched.
|
|
433
452
|
document.addEventListener(
|
|
434
453
|
'keydown',
|
|
435
454
|
(event) => {
|
|
436
|
-
if (event.
|
|
437
|
-
setPen(false);
|
|
455
|
+
if (!penOn || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
456
|
+
if (event.key === 'Escape') setPen(false);
|
|
457
|
+
else {
|
|
458
|
+
const index = '123456'.indexOf(event.key);
|
|
459
|
+
if (index === -1 || index >= COLOURS.length) return;
|
|
460
|
+
setInk(COLOURS[index]);
|
|
461
|
+
}
|
|
438
462
|
event.stopPropagation();
|
|
439
463
|
event.preventDefault();
|
|
440
464
|
},
|
package/runtime/index.html
CHANGED
package/runtime/theme.base.css
CHANGED
|
@@ -29,6 +29,39 @@
|
|
|
29
29
|
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/* --- wearing one of reveal's own themes ----------------------------------- */
|
|
33
|
+
|
|
34
|
+
/* `revealTheme:` in slides.config.js loads reveal's theme after this file, where
|
|
35
|
+
it takes the slides. The chrome — the navigator, the badge, the signature —
|
|
36
|
+
is not `.reveal`-scoped, so it would sail past untouched and leave a black
|
|
37
|
+
panel down the side of a white deck.
|
|
38
|
+
|
|
39
|
+
Every reveal 5 theme declares its palette as `--r-*` custom properties, so
|
|
40
|
+
the fix is one indirection rather than fifteen stylesheets: point the tokens
|
|
41
|
+
above at reveal's, and the whole of the chrome follows whatever the deck
|
|
42
|
+
wears. The four reveal has no equivalent for are mixed from the two that
|
|
43
|
+
carry the theme, which keeps the panel's depth on a light theme as well as
|
|
44
|
+
a dark one. */
|
|
45
|
+
html[data-reveal-theme] {
|
|
46
|
+
--bg: var(--r-background-color);
|
|
47
|
+
--text: var(--r-main-color);
|
|
48
|
+
--accent: var(--r-link-color);
|
|
49
|
+
--accent-2: var(--r-link-color-hover, var(--r-link-color));
|
|
50
|
+
--sans: var(--r-main-font);
|
|
51
|
+
--mono: var(--r-code-font);
|
|
52
|
+
--bg-2: color-mix(in srgb, var(--r-background-color) 92%, var(--r-main-color));
|
|
53
|
+
--card: color-mix(in srgb, var(--r-background-color) 86%, var(--r-main-color));
|
|
54
|
+
--line: color-mix(in srgb, var(--r-main-color) 20%, transparent);
|
|
55
|
+
--muted: color-mix(in srgb, var(--r-main-color) 62%, var(--r-background-color));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The base look paints two dark halos on <html>, below the deck. They are the
|
|
59
|
+
wrong atmosphere for a borrowed theme, and on a light one they show through
|
|
60
|
+
wherever the viewport does not reach — so the theme's own flat colour takes
|
|
61
|
+
over. <body> is left alone on purpose: reveal paints the deck's background
|
|
62
|
+
there, and that is the theme's job now. */
|
|
63
|
+
html[data-reveal-theme] { background: var(--bg); }
|
|
64
|
+
|
|
32
65
|
/* reveal.js 5 paints the page background on `.reveal-viewport` (the <html>
|
|
33
66
|
element), so setting it on <body> would sit behind it and never show. */
|
|
34
67
|
html,
|
|
@@ -111,6 +144,7 @@ body,
|
|
|
111
144
|
/* Trailing margin would push centred content off-centre. */
|
|
112
145
|
.reveal .slides section > :last-child { margin-bottom: 0; }
|
|
113
146
|
.reveal strong { color: #fff; font-weight: 650; }
|
|
147
|
+
html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--text)); }
|
|
114
148
|
.reveal em { color: var(--accent-2); font-style: italic; }
|
|
115
149
|
.reveal a { color: var(--accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); }
|
|
116
150
|
|
|
@@ -9,6 +9,18 @@ export default {
|
|
|
9
9
|
// Folder behind a bare `<!-- demo: counter -->` marker in the Markdown.
|
|
10
10
|
demos: 'demo',
|
|
11
11
|
|
|
12
|
+
// One of reveal.js's own themes, served from this project rather than from a
|
|
13
|
+
// CDN: beige, black, black-contrast, blood, dracula, league, moon, night,
|
|
14
|
+
// serif, simple, sky, solarized, white, white-contrast. Uncomment to wear one
|
|
15
|
+
// — the deck's chrome follows it. Leave it out for this package's own look.
|
|
16
|
+
// revealTheme: 'dracula',
|
|
17
|
+
|
|
18
|
+
// Six of those themes ask fonts.googleapis.com for their typefaces. Those
|
|
19
|
+
// requests are stripped out, so a deck never depends on the room's wifi; the
|
|
20
|
+
// theme falls back to the next font in its own stack. Set this to true to let
|
|
21
|
+
// them through and get the typography of revealjs.com/themes exactly.
|
|
22
|
+
// webfonts: true,
|
|
23
|
+
|
|
12
24
|
// Port `fb-slides dev` serves the deck on (`preview` uses the next one up).
|
|
13
25
|
// `--port n` on the command line wins over this.
|
|
14
26
|
port: 4000,
|