fb-slides 0.9.1 → 0.11.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 +64 -0
- package/README.md +96 -5
- package/lib/build.mjs +4 -2
- package/lib/config.mjs +10 -3
- package/lib/dev.mjs +30 -9
- package/lib/edit.mjs +3 -1
- package/lib/render.mjs +16 -4
- package/lib/theme.mjs +53 -5
- package/package.json +1 -1
- package/runtime/annotate.js +36 -0
- package/runtime/assets.js +42 -4
- package/runtime/deck.js +48 -3
- package/runtime/edit.js +45 -21
- package/runtime/snippets.js +43 -0
- package/runtime/theme.base.css +320 -14
- package/runtime/themes/custom-aurora.css +184 -0
- package/runtime/themes/custom-editoriale.css +185 -0
- package/runtime/themes.js +250 -0
- package/templates/starter/assets/author/JSDAY-26.jpg +0 -0
- package/templates/starter/assets/author/devfest-pisa-26.jpg +0 -0
- package/templates/starter/assets/author/devfestroma-25.jpg +0 -0
- package/templates/starter/assets/author/fb-mountain.jpg +0 -0
- package/templates/starter/assets/author/fb-mtb.jpeg +0 -0
- package/templates/starter/assets/author/gemini-masterclass-26.jpg +0 -0
- package/templates/starter/assets/author/webday-26.jpg +0 -0
- package/templates/starter/assets/jshd-sticker.png +0 -0
- package/templates/starter/decks/01-intro.md +4 -57
- package/templates/starter/decks/02-author.md +64 -0
- package/templates/starter/decks/02-demos.md +36 -0
- package/templates/starter/slides.config.js +26 -6
- package/templates/starter/assets/author/photo-1.svg +0 -11
- package/templates/starter/assets/author/photo-2.svg +0 -11
- package/templates/starter/assets/screen_2026_08_05_00_24_16.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,70 @@ this — which makes it worth being able to see what arrived.
|
|
|
7
7
|
Entries are written as the work lands, under **Unreleased**; the release commit
|
|
8
8
|
that stamps the version renames that heading to the version and its date.
|
|
9
9
|
|
|
10
|
+
## 0.11.0 — 2026-09-23
|
|
11
|
+
|
|
12
|
+
- **Two named themes, and a picker to try them with.** `themePack:` in
|
|
13
|
+
slides.config.js takes one of the looks this package now ships —
|
|
14
|
+
`custom-aurora`, atmospheric, coloured light behind glass panels under a
|
|
15
|
+
display serif, and `custom-editoriale`, flat and typographic, hairlines and
|
|
16
|
+
almost no colour. They are neither reveal themes nor a second renderer: a pack
|
|
17
|
+
is written against the same tokens as the base theme and dresses the same slide
|
|
18
|
+
vocabulary, so both of them fit the *same Markdown*, and the three layers
|
|
19
|
+
compose — reveal's theme, then the pack, then your own `theme.css`, which still
|
|
20
|
+
has the last word. Neither pack fetches a webfont.
|
|
21
|
+
- **A theme picker in the toolbar, under `dev`.** The last button on the strip
|
|
22
|
+
lists everything the deck could wear under two headings — the packs, then
|
|
23
|
+
reveal's fifteen, with the bare base theme above both. Picking one swaps a
|
|
24
|
+
single `<link>` (and, for a reveal theme, the `data-reveal-theme` the chrome
|
|
25
|
+
follows), with no reload and no restart, and a refresh keeps it. It is one
|
|
26
|
+
choice rather than two: the config will let you name a pack *and* a reveal
|
|
27
|
+
theme, but layering them gives neither look. It previews rather than saves —
|
|
28
|
+
the panel prints the `themePack:` or `revealTheme:` line to paste once you
|
|
29
|
+
have decided, because slides.config.js is a module with your comments in it.
|
|
30
|
+
It exists only under `dev` by construction: `build` renders the page without
|
|
31
|
+
the list the picker reads, so a published deck never has one.
|
|
32
|
+
- Reveal's themes are now served one URL each, `reveal-themes/<name>.css`,
|
|
33
|
+
rather than all of them through a single `reveal-theme.css` whose contents
|
|
34
|
+
depended on the config. `dev` rewrites any of them on request — that is what
|
|
35
|
+
the picker switches between — while `build` still writes only the one the
|
|
36
|
+
config names, with only the font folders that one imports. A stylesheet whose
|
|
37
|
+
bytes change under a fixed URL is one a browser is right to keep serving
|
|
38
|
+
stale, and the picker changes it between two keystrokes.
|
|
39
|
+
- **Four blocks the packs are built around**, all of them in the edit drawer's
|
|
40
|
+
`+` picker: `chips` (the line above the title), `rows` and `rows--cta` (two or
|
|
41
|
+
three things with a glyph and a sentence each, or the last slide's ways out),
|
|
42
|
+
and `<!-- file: cart.component.ts -->` above a fence, which draws a header bar
|
|
43
|
+
on the code block — the language comes from the file's own extension, so there
|
|
44
|
+
is nothing extra to write. A Markdown table with an empty header row is now
|
|
45
|
+
laid out as a definition list instead of drawing an empty grey bar.
|
|
46
|
+
- **A published deck no longer asks for the edit API.** The edit button was
|
|
47
|
+
already dev-server only — a static host 404s the probe and the drawer never
|
|
48
|
+
exists — but the probe itself went out on every load, and on GitHub Pages
|
|
49
|
+
that was a red `404 /api/editable` in the console of a deck with nothing
|
|
50
|
+
wrong. The runtime now looks at the address bar first and asks only from
|
|
51
|
+
`localhost` (or `127.0.0.1`), the one place the server would answer; from a
|
|
52
|
+
LAN address or a tunnel it stays quiet, as the server would have refused it
|
|
53
|
+
anyway.
|
|
54
|
+
- **The toolbar stays down until you put it away.** It still comes down when
|
|
55
|
+
the mouse goes for the top edge, a tool arms or focus lands on it — but it
|
|
56
|
+
no longer slides back up when the mouse wanders off, nor when the pen or the
|
|
57
|
+
spotlight is switched off. A presenter who has just picked a colour and moved
|
|
58
|
+
down to draw should not have the bar leave from under the next click. An `X`
|
|
59
|
+
chip at the end of the strip puts it away; the `X` key does the same, and
|
|
60
|
+
brings it back.
|
|
61
|
+
- Docs: the key table said `T` for the drawer; it has been `Shift+E` since 0.9.0.
|
|
62
|
+
- The starter deck gains an about-me deck of its own, `decks/02-author.md`, in
|
|
63
|
+
place of the two placeholder photos on the intro slide, and its demos deck
|
|
64
|
+
shows the chips, rows and file-headed code blocks the packs are built around.
|
|
65
|
+
|
|
66
|
+
## 0.10.0 — 2026-09-08
|
|
67
|
+
|
|
68
|
+
- **A file in the assets panel opens on click.** The name on a row carries the
|
|
69
|
+
full path in its tooltip, and clicking it shows the file: an image comes up
|
|
70
|
+
right there, over the deck — full size, the path under it, a click anywhere
|
|
71
|
+
or Escape puts it away — and everything else opens in a new tab, where the
|
|
72
|
+
browser plays the clip, renders the PDF, and offers to save the rest.
|
|
73
|
+
|
|
10
74
|
## 0.9.1 — 2026-09-08
|
|
11
75
|
|
|
12
76
|
- **The assets panel shows the folder, not just what you dropped.** The list
|
package/README.md
CHANGED
|
@@ -293,8 +293,10 @@ one layer per press.
|
|
|
293
293
|
whatever else `static:` says, so a path the drawer hands out works in the build
|
|
294
294
|
too.
|
|
295
295
|
|
|
296
|
-
Both the drawer and the drop exist only under `fb-slides dev
|
|
297
|
-
static files and never shows the
|
|
296
|
+
Both the drawer and the drop exist only under `fb-slides dev`, and only at
|
|
297
|
+
`localhost` (or `127.0.0.1`): a built deck is static files and never shows the
|
|
298
|
+
button, and from any other address — a phone on the LAN, a tunnel, GitHub
|
|
299
|
+
Pages — the page does not even ask for the edit API.
|
|
298
300
|
|
|
299
301
|
## Presenting
|
|
300
302
|
|
|
@@ -305,7 +307,8 @@ static files and never shows the button.
|
|
|
305
307
|
| `W` | the arrow — click the tail, then the tip; same colours as the pen |
|
|
306
308
|
| `E` | the spotlight — drag a rectangle, the rest of the slide dims and blurs |
|
|
307
309
|
| `R` | the pointer — replaces the cursor: a glowing halo, a dart aimed at the centre, or the normal mouse |
|
|
308
|
-
| `
|
|
310
|
+
| `X` | put the toolbar away, or bring it back — it comes down when the mouse goes for the top edge or a tool arms, and stays until this |
|
|
311
|
+
| `Shift+E` | edit the slide — markdown and notes, saved back into the `.md` (dev server only) |
|
|
309
312
|
| `S` | speaker view — notes, timer, next slide |
|
|
310
313
|
| `Esc` / `O` | overview — the slides as a grid |
|
|
311
314
|
| `V` | the vertical navigator — every slide by title, down the left edge |
|
|
@@ -330,7 +333,8 @@ export default {
|
|
|
330
333
|
static: ['assets', 'demo'], // served and published; auto-detected when omitted
|
|
331
334
|
revealTheme: 'dracula', // one of reveal.js's own themes; omit for this one
|
|
332
335
|
webfonts: false, // let the reveal themes fetch their Google fonts
|
|
333
|
-
|
|
336
|
+
themePack: 'custom-aurora', // one of this package's own named themes
|
|
337
|
+
theme: 'theme.css', // loaded last, over both; auto when it exists
|
|
334
338
|
favicon: 'assets/favicon.png',
|
|
335
339
|
|
|
336
340
|
signature: { // the corner logo. Omit the key, omit the logo
|
|
@@ -439,7 +443,8 @@ export default { revealTheme: 'dracula' };
|
|
|
439
443
|
`serif` · `simple` · `sky` · `solarized` · `white` · `white-contrast`
|
|
440
444
|
|
|
441
445
|
Only the chosen one is ever loaded — one `<link>`, ~7 KB — and the build copies that file
|
|
442
|
-
and nothing else.
|
|
446
|
+
and nothing else. Under `dev` all fifteen are reachable, one URL each, so the picker below
|
|
447
|
+
can try them without a restart. Two things happen beyond the link:
|
|
443
448
|
|
|
444
449
|
**It is served from the deck, not from a CDN.** Six of the themes open with
|
|
445
450
|
`@import url(https://fonts.googleapis.com/…)`. Those lines are stripped, so a talk still
|
|
@@ -454,6 +459,92 @@ deck. Every reveal 5 theme declares its palette as `--r-*` custom properties, an
|
|
|
454
459
|
theme points its own tokens at them, so the whole page moves together. A `theme.css` in the
|
|
455
460
|
project still has the last word over both.
|
|
456
461
|
|
|
462
|
+
### The named themes
|
|
463
|
+
|
|
464
|
+
Two whole looks ship with the package, picked by name:
|
|
465
|
+
|
|
466
|
+
```js
|
|
467
|
+
export default { themePack: 'custom-aurora' };
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`custom-aurora` — atmospheric. Coloured light pooling behind the slide, glass panels
|
|
471
|
+
floating on it, a display serif carrying the titles.
|
|
472
|
+
|
|
473
|
+
`custom-editoriale` — flat and typographic. No panels and almost no colour: one serif,
|
|
474
|
+
hairlines for dividing, and the accent kept for the two or three words a slide is about.
|
|
475
|
+
|
|
476
|
+
They are not reveal themes and they are not a second renderer. A pack is written against
|
|
477
|
+
the same `--bg` / `--accent` / `--text` tokens as the base theme and styles the same slide
|
|
478
|
+
vocabulary, which is what lets both of them dress the *same Markdown*: the `chips` line
|
|
479
|
+
that is a row of pills under `custom-aurora` is a `// LIKE THIS` rule under
|
|
480
|
+
`custom-editoriale`, and the `.md` does not change a character.
|
|
481
|
+
|
|
482
|
+
The three layers compose, in this order — reveal's theme, then the pack, then your
|
|
483
|
+
`theme.css`. Wearing a pack and still overriding two of its colours is a two-line file.
|
|
484
|
+
|
|
485
|
+
Neither pack fetches a webfont. The serif is whatever the machine already has, closest
|
|
486
|
+
first; a talk that has to survive the conference wifi cannot open with a request to
|
|
487
|
+
fonts.googleapis.com.
|
|
488
|
+
|
|
489
|
+
### Switching themes while you work
|
|
490
|
+
|
|
491
|
+
`fb-slides dev` puts a theme picker in the toolbar — the last button, next to the pen. It
|
|
492
|
+
lists everything the deck could wear, under two headings: the packs above, reveal's fifteen
|
|
493
|
+
below, with the bare base theme at the top. Picking swaps one `<link>`: no reload, no
|
|
494
|
+
restart, and the choice survives a refresh.
|
|
495
|
+
|
|
496
|
+
One choice, not two. The config will let you name a pack and a reveal theme at once, but
|
|
497
|
+
layering them gives you neither look — only whichever rule happened to come last — so the
|
|
498
|
+
picker treats them as one question. Your `theme.css` still lands on top of whatever is
|
|
499
|
+
picked, as it always does.
|
|
500
|
+
|
|
501
|
+
It is a preview, not a setting. Nothing is written to disk — `slides.config.js` is a module
|
|
502
|
+
with your own comments in it, and a picker that rewrites it is a worse problem than the one
|
|
503
|
+
it solves — so the panel prints the line to paste when you have decided.
|
|
504
|
+
|
|
505
|
+
The picker exists only under `dev`, and by construction rather than by a flag: `dev` renders
|
|
506
|
+
the page with the theme list in it and `build` does not, so a published deck has nothing for
|
|
507
|
+
the switcher to read and it never builds itself. The fifteen are the same way: `dev` can
|
|
508
|
+
rewrite any of them on request, `build` writes only the one the config names.
|
|
509
|
+
|
|
510
|
+
### The slide vocabulary the packs dress
|
|
511
|
+
|
|
512
|
+
Four blocks both packs know. All four are in the edit drawer's `+` picker, so none of this
|
|
513
|
+
has to be remembered:
|
|
514
|
+
|
|
515
|
+
```html
|
|
516
|
+
<p class="chips"><span>Angular</span><span>Signals</span><span>v22</span></p>
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
The line above the title. The first chip is the loud one — in a row of equals nothing is
|
|
520
|
+
being asked.
|
|
521
|
+
|
|
522
|
+
```html
|
|
523
|
+
<div class="rows">
|
|
524
|
+
<div class="row">
|
|
525
|
+
<span class="row-icon">( )</span>
|
|
526
|
+
<div><strong>Signals</strong><p>UI state, derived values, clean templates.</p></div>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Two or three things, each with a glyph and one sentence. `class="rows rows--cta"` turns the
|
|
532
|
+
same block into the last slide's ways out: every row grows an arrow, and the first is filled.
|
|
533
|
+
|
|
534
|
+
````md
|
|
535
|
+
<!-- file: cart.component.ts -->
|
|
536
|
+
```ts
|
|
537
|
+
…
|
|
538
|
+
```
|
|
539
|
+
````
|
|
540
|
+
|
|
541
|
+
A header bar on the code block: the name on the left, the language on the right. The
|
|
542
|
+
language is never written down — it is the file's own extension. The marker is a comment,
|
|
543
|
+
in the shape the demo slides already use, so the `.md` still renders anywhere else.
|
|
544
|
+
|
|
545
|
+
A two-column Markdown table with an **empty header row** is a definition list — the term on
|
|
546
|
+
the left, one sentence on the right — and the empty bar is dropped rather than drawn.
|
|
547
|
+
|
|
457
548
|
## Publishing
|
|
458
549
|
|
|
459
550
|
```bash
|
package/lib/build.mjs
CHANGED
|
@@ -14,7 +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 {
|
|
17
|
+
import { readRevealTheme, revealThemeUrl } from './theme.mjs';
|
|
18
18
|
import { VENDOR_FILES, VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
19
19
|
|
|
20
20
|
const copyFile = async (from, to) => {
|
|
@@ -64,7 +64,9 @@ export const build = async (config, runtimeDir, { quiet = false } = {}) => {
|
|
|
64
64
|
// only those: source-sans-pro is 1.8 MB and most themes never ask for it.
|
|
65
65
|
if (config.revealTheme) {
|
|
66
66
|
const { css, fonts, dir } = await readRevealTheme(config.revealTheme, { webfonts: config.webfonts });
|
|
67
|
-
|
|
67
|
+
const themeFile = join(out, revealThemeUrl(config.revealTheme));
|
|
68
|
+
await mkdir(dirname(themeFile), { recursive: true });
|
|
69
|
+
await writeFile(themeFile, css);
|
|
68
70
|
for (const font of fonts) {
|
|
69
71
|
await cp(join(dir, 'fonts', font), join(out, 'vendor', VENDOR_MOUNTS['reveal.js'], 'dist/theme/fonts', font), {
|
|
70
72
|
recursive: true,
|
package/lib/config.mjs
CHANGED
|
@@ -10,7 +10,7 @@ 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';
|
|
13
|
+
import { assertDeckTheme, assertRevealTheme } from './theme.mjs';
|
|
14
14
|
|
|
15
15
|
const CONFIG_FILES = ['slides.config.js', 'slides.config.mjs', 'slides.config.json'];
|
|
16
16
|
|
|
@@ -110,8 +110,14 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
110
110
|
// default — the deck is served whole or not at all — at the cost of falling
|
|
111
111
|
// back to the theme's second-choice font stack.
|
|
112
112
|
webfonts: user.webfonts ?? false,
|
|
113
|
-
//
|
|
114
|
-
//
|
|
113
|
+
// One of this package's own named themes — a whole skin over the base one,
|
|
114
|
+
// from runtime/themes/. It sits between reveal's theme and the project's CSS,
|
|
115
|
+
// so all three compose: a pack can be worn and still be overridden. Null is
|
|
116
|
+
// the base look, bare.
|
|
117
|
+
themePack: user.themePack ?? null,
|
|
118
|
+
// The project's own CSS, loaded *after* the base theme and after any pack:
|
|
119
|
+
// an override, not a replacement. `theme.css` at the root is picked up
|
|
120
|
+
// without being declared.
|
|
115
121
|
theme: user.theme ?? (existsSync(join(root, 'theme.css')) ? 'theme.css' : null),
|
|
116
122
|
favicon: user.favicon ?? null,
|
|
117
123
|
signature: user.signature ?? null,
|
|
@@ -136,6 +142,7 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
136
142
|
// Named early: a typo in the config should be a message, not a deck that
|
|
137
143
|
// silently wears the wrong clothes.
|
|
138
144
|
if (config.revealTheme) await assertRevealTheme(config.revealTheme);
|
|
145
|
+
if (config.themePack) await assertDeckTheme(config.themePack);
|
|
139
146
|
|
|
140
147
|
// `--no-editor` is its own flag rather than a part of `--no-servers`: that one
|
|
141
148
|
// is for skipping a demo's dev server, and a demo that is not running is
|
package/lib/dev.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { EDITOR_PORT_TRIES, assertUsable, portOf } from './config.mjs';
|
|
|
15
15
|
import { createEditApi } from './edit.mjs';
|
|
16
16
|
import { createDeckServer, listen } from './server.mjs';
|
|
17
17
|
import { renderIndex } from './render.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import { listDeckThemes, listRevealThemes, readRevealTheme, revealThemeUrl } from './theme.mjs';
|
|
19
19
|
import { VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
20
20
|
|
|
21
21
|
const OPEN = { darwin: 'open', win32: 'start' };
|
|
@@ -197,6 +197,9 @@ export const dev = async (config, runtimeDir, reload) => {
|
|
|
197
197
|
assertUsable(config);
|
|
198
198
|
const current = reader(config, reload);
|
|
199
199
|
let editorUrl = null;
|
|
200
|
+
// Fixed for the life of the server: they come out of node_modules, which does
|
|
201
|
+
// not change while a talk is being given.
|
|
202
|
+
const revealThemes = await listRevealThemes();
|
|
200
203
|
|
|
201
204
|
const mounts = [
|
|
202
205
|
// The project first: a file next to the decks shadows the one this package
|
|
@@ -221,20 +224,38 @@ export const dev = async (config, runtimeDir, reload) => {
|
|
|
221
224
|
// Read at request time, not at wiring time: the editor is started below,
|
|
222
225
|
// after the port is bound, and the first page load comes later still.
|
|
223
226
|
const editor = editorUrl ? { url: editorUrl, root: config.root, sources: now.demoSources } : null;
|
|
224
|
-
|
|
227
|
+
// The theme switcher's whole list, and what the config currently says. It
|
|
228
|
+
// rides in `extra` rather than behind an /api/ probe because that is what
|
|
229
|
+
// makes it dev-only *by construction*: `build` calls renderIndex without
|
|
230
|
+
// it, so a published page has no list, and the switcher never builds.
|
|
231
|
+
const themes = {
|
|
232
|
+
packs: await listDeckThemes(),
|
|
233
|
+
reveal: revealThemes,
|
|
234
|
+
// What the config says the deck is wearing. A project that names both —
|
|
235
|
+
// nothing stops it — is showing the pack, so that is what the switcher
|
|
236
|
+
// opens on.
|
|
237
|
+
current: now.themePack ?? now.revealTheme ?? null,
|
|
238
|
+
};
|
|
239
|
+
return renderIndex(now, runtimeDir, { ...(editor ? { editor } : {}), themes });
|
|
225
240
|
},
|
|
226
241
|
// Editing a slide in the browser saves it back into its .md, and dropping
|
|
227
242
|
// a file on the deck puts it in assets/ — dev only, which is what makes the
|
|
228
243
|
// edit button appear at all (the runtime probes /api/editable). Build and
|
|
229
244
|
// preview never mount this.
|
|
230
245
|
api: createEditApi(config.decksPath, { dir: config.assetsDir, path: config.assetsPath }),
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
// All fifteen, each at its own URL, rewritten on request. `build` writes only
|
|
247
|
+
// the one the config names — it has no switcher to feed — but here any of
|
|
248
|
+
// them can be asked for at any moment, and the rewrite is a string replace
|
|
249
|
+
// over a 7 KB file, so there is nothing to gain by being clever about it.
|
|
250
|
+
//
|
|
251
|
+
// `webfonts` is read per request like everything else: flipping it in the
|
|
252
|
+
// config and reloading changes what these hand back.
|
|
253
|
+
generated: Object.fromEntries(
|
|
254
|
+
revealThemes.map((name) => [
|
|
255
|
+
`/${revealThemeUrl(name)}`,
|
|
256
|
+
async () => (await readRevealTheme(name, { webfonts: (await current()).webfonts })).css,
|
|
257
|
+
]),
|
|
258
|
+
),
|
|
238
259
|
});
|
|
239
260
|
|
|
240
261
|
await listen(server, config.port);
|
package/lib/edit.mjs
CHANGED
|
@@ -78,7 +78,9 @@ const readBody = (req, limit = 1024 * 1024) =>
|
|
|
78
78
|
// web page you have open can POST to it. Cross-origin requests cannot carry a
|
|
79
79
|
// custom header without a CORS preflight — which is never answered — so
|
|
80
80
|
// requiring one shuts that door; the Host check covers DNS rebinding, where
|
|
81
|
-
// the request arrives same-origin under a name that is not ours.
|
|
81
|
+
// the request arrives same-origin under a name that is not ours. The runtime
|
|
82
|
+
// keeps a copy of this list (runtime/edit.js) and does not ask from anywhere
|
|
83
|
+
// else: change one, change the other.
|
|
82
84
|
const LOCAL_HOST = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
|
|
83
85
|
|
|
84
86
|
// `assets` is `{ dir, path }` — the folder a dropped file lands in. Without it
|
package/lib/render.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { readFile } from 'node:fs/promises';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { deckThemeUrl, revealThemeUrl } from './theme.mjs';
|
|
11
11
|
|
|
12
12
|
const escapeHtml = (value) =>
|
|
13
13
|
String(value).replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
@@ -32,9 +32,21 @@ const headHtml = (config) => {
|
|
|
32
32
|
if (config.favicon) tags.push(`<link rel="icon" href="${escapeHtml(config.favicon)}" />`);
|
|
33
33
|
// After the base theme, before the project's: reveal's theme dresses the
|
|
34
34
|
// slides, and whatever the project says still has the last word.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// The id, like the pack's below, is the handle the dev-only switcher swaps.
|
|
36
|
+
if (config.revealTheme)
|
|
37
|
+
tags.push(
|
|
38
|
+
`<link id="deck-reveal-theme" rel="stylesheet" href="${escapeHtml(revealThemeUrl(config.revealTheme))}" />`,
|
|
39
|
+
);
|
|
40
|
+
// A named pack from this package, between reveal's theme and the project's.
|
|
41
|
+
// The id is the handle the dev-only switcher swaps; a built deck carries it
|
|
42
|
+
// too and simply never has anything that looks it up.
|
|
43
|
+
if (config.themePack)
|
|
44
|
+
tags.push(`<link id="deck-theme" rel="stylesheet" href="${escapeHtml(deckThemeUrl(config.themePack))}" />`);
|
|
45
|
+
// After the base theme and after any pack, on purpose: the project's CSS
|
|
46
|
+
// overrides, it does not replace. The id is what the switcher inserts a pack
|
|
47
|
+
// *before* on a deck that started with none — order is the whole contract here.
|
|
48
|
+
if (config.theme)
|
|
49
|
+
tags.push(`<link id="deck-theme-project" rel="stylesheet" href="${escapeHtml(config.theme)}" />`);
|
|
38
50
|
return tags.join('\n ');
|
|
39
51
|
};
|
|
40
52
|
|
package/lib/theme.mjs
CHANGED
|
@@ -13,17 +13,28 @@
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
|
|
15
15
|
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
-
import { basename, join } from 'node:path';
|
|
16
|
+
import { basename, dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
17
18
|
|
|
18
19
|
import { VENDOR_MOUNTS, packageDir } from './vendor.mjs';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// One URL per theme rather than one URL for "the theme". A single
|
|
22
|
+
// `reveal-theme.css` was enough while the answer could only change by editing
|
|
23
|
+
// the config and reloading; the dev switcher changes it between two keystrokes,
|
|
24
|
+
// and a stylesheet whose bytes depend on state the browser cannot see is a
|
|
25
|
+
// stylesheet the browser is right to keep serving stale.
|
|
26
|
+
//
|
|
27
|
+
// Relative, like every other URL the page holds: dist/ has to survive being
|
|
28
|
+
// published in a subfolder.
|
|
29
|
+
export const revealThemeUrl = (name) => `reveal-themes/${name}.css`;
|
|
21
30
|
|
|
22
31
|
const themeDir = () => join(packageDir('reveal.js'), 'dist', 'theme');
|
|
23
32
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
33
|
+
// A url() in a stylesheet resolves against the stylesheet, not the page. These
|
|
34
|
+
// are served from `reveal-themes/`, one folder down from the deck, so the hop
|
|
35
|
+
// back up is part of the path — reveal's own `./fonts/…` would otherwise look
|
|
36
|
+
// for them inside that folder.
|
|
37
|
+
const FONT_BASE = `../vendor/${VENDOR_MOUNTS['reveal.js']}/dist/theme/fonts/`;
|
|
27
38
|
|
|
28
39
|
const REMOTE_IMPORT = /@import\s+url\(\s*['"]?https?:[^)]*\)\s*;?[ \t]*\n?/gi;
|
|
29
40
|
const LOCAL_FONT_URL = /url\(\s*['"]?\.\/fonts\//gi;
|
|
@@ -55,3 +66,40 @@ export const readRevealTheme = async (name, { webfonts = false } = {}) => {
|
|
|
55
66
|
const css = (webfonts ? source : source.replace(REMOTE_IMPORT, '')).replace(LOCAL_FONT_URL, `url(${FONT_BASE}`);
|
|
56
67
|
return { css, fonts: fontDirs(source), dir: themeDir() };
|
|
57
68
|
};
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// This package's own named themes, as opposed to reveal's fifteen.
|
|
72
|
+
//
|
|
73
|
+
// The difference is what they dress. A reveal theme takes the slides and leaves
|
|
74
|
+
// the chrome to the bridge at the top of theme.base.css; these are written
|
|
75
|
+
// against that file's own tokens, so the whole page moves at once and nothing
|
|
76
|
+
// has to be bridged. They add no markup either: everything they style is the
|
|
77
|
+
// slide vocabulary theme.base.css already ships, which is what keeps a pack a
|
|
78
|
+
// skin rather than a second renderer.
|
|
79
|
+
//
|
|
80
|
+
// They live in runtime/themes/, so they are served by the runtime mount in dev
|
|
81
|
+
// and copied by the build's first step — a published deck carries the one it
|
|
82
|
+
// wears without either command knowing they exist.
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const deckThemeDir = () => join(dirname(fileURLToPath(import.meta.url)), '..', 'runtime', 'themes');
|
|
86
|
+
|
|
87
|
+
// Relative like every other URL the page holds: dist/ has to survive being
|
|
88
|
+
// published in a subfolder.
|
|
89
|
+
export const deckThemeUrl = (name) => `themes/${name}.css`;
|
|
90
|
+
|
|
91
|
+
export const listDeckThemes = async () =>
|
|
92
|
+
(await readdir(deckThemeDir()).catch(() => []))
|
|
93
|
+
.filter((file) => file.endsWith('.css'))
|
|
94
|
+
.map((file) => basename(file, '.css'))
|
|
95
|
+
.sort();
|
|
96
|
+
|
|
97
|
+
export const assertDeckTheme = async (name) => {
|
|
98
|
+
const themes = await listDeckThemes();
|
|
99
|
+
if (themes.includes(name)) return name;
|
|
100
|
+
throw new Error(
|
|
101
|
+
`unknown themePack: ${name}\n` +
|
|
102
|
+
` fb-slides ships: ${themes.join(', ')}\n` +
|
|
103
|
+
` drop the key for the base look, or put your own CSS in \`theme:\``,
|
|
104
|
+
);
|
|
105
|
+
};
|
package/package.json
CHANGED
package/runtime/annotate.js
CHANGED
|
@@ -76,6 +76,8 @@ const QUANTA = 24;
|
|
|
76
76
|
// nothing between Q and R (P, H/J/K/L, B, V, F, G, A, C, O are all elsewhere).
|
|
77
77
|
const PEN_KEY_CODE = 81; // Q
|
|
78
78
|
const ARROW_KEY_CODE = 87; // W
|
|
79
|
+
// The toolbar's own key, off the row: X puts the bar away, or brings it back.
|
|
80
|
+
const CLOSE_KEY_CODE = 88; // X
|
|
79
81
|
|
|
80
82
|
const PEN_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
81
83
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
@@ -87,6 +89,11 @@ const ARROW_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
|
|
|
87
89
|
<path d="M5 19L19 5" /><path d="M9.5 5H19v9.5" />
|
|
88
90
|
</svg>`;
|
|
89
91
|
|
|
92
|
+
const CLOSE_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
93
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
94
|
+
<path d="M6 6l12 12" /><path d="M18 6L6 18" />
|
|
95
|
+
</svg>`;
|
|
96
|
+
|
|
90
97
|
// Black text on a light chip, light text on a dark one — the armed pen button
|
|
91
98
|
// wears the ink's own colour, and two of the six are the extremes.
|
|
92
99
|
const readable = (hex) => {
|
|
@@ -144,6 +151,9 @@ const RevealAnnotate = () => ({
|
|
|
144
151
|
<button id="tool-arrow" class="deck-tool" type="button" aria-pressed="false"
|
|
145
152
|
title="Arrow — click the tail, then the tip (W)" aria-label="Arrow">${ARROW_ICON}<span
|
|
146
153
|
class="deck-key" aria-hidden="true">W</span></button>
|
|
154
|
+
<button id="tool-close" class="deck-tool deck-tools-close" type="button"
|
|
155
|
+
title="Put the toolbar away (X)" aria-label="Put the toolbar away">${CLOSE_ICON}<span
|
|
156
|
+
class="deck-key" aria-hidden="true">X</span></button>
|
|
147
157
|
<div id="deck-swatches" role="radiogroup" aria-label="Ink colour" hidden>
|
|
148
158
|
${COLOURS.map(
|
|
149
159
|
(colour, i) => `<button class="deck-swatch" type="button" role="radio"
|
|
@@ -156,9 +166,35 @@ const RevealAnnotate = () => ({
|
|
|
156
166
|
document.body.append(canvas, toolbar);
|
|
157
167
|
const penButton = toolbar.querySelector('#tool-pen');
|
|
158
168
|
const arrowButton = toolbar.querySelector('#tool-arrow');
|
|
169
|
+
const closeButton = toolbar.querySelector('#tool-close');
|
|
159
170
|
const swatches = toolbar.querySelector('#deck-swatches');
|
|
160
171
|
const ctx = canvas.getContext('2d');
|
|
161
172
|
|
|
173
|
+
// ---- showing and hiding ---------------------------------------------
|
|
174
|
+
// The bar sleeps above the top edge and comes down when the pointer goes
|
|
175
|
+
// for it, a tool arms, or focus lands on it — and then it *stays*. Leaving
|
|
176
|
+
// with the mouse, or disarming the tool that brought it down, does not put
|
|
177
|
+
// it away: only the X chip and its key do. A latch rather than :hover,
|
|
178
|
+
// because a presenter who has just picked a colour and moved off to draw
|
|
179
|
+
// should not have the bar slide out from under the next click.
|
|
180
|
+
const showTools = () => toolbar.classList.add('is-open');
|
|
181
|
+
const hideTools = () => {
|
|
182
|
+
toolbar.classList.remove('is-open');
|
|
183
|
+
// Focus left on the X would bring it straight back through `focusin`.
|
|
184
|
+
if (toolbar.contains(document.activeElement)) document.activeElement.blur();
|
|
185
|
+
};
|
|
186
|
+
const toggleTools = () => (toolbar.classList.contains('is-open') ? hideTools() : showTools());
|
|
187
|
+
// `pointerenter` fires for the wake strip too — a pseudo-element is hit
|
|
188
|
+
// tested as its owner — which is how parking the mouse along the top edge
|
|
189
|
+
// still wakes the bar.
|
|
190
|
+
toolbar.addEventListener('pointerenter', showTools);
|
|
191
|
+
toolbar.addEventListener('focusin', showTools);
|
|
192
|
+
closeButton.addEventListener('click', hideTools);
|
|
193
|
+
// The other plugins' tools arm through the same handshake, so one listener
|
|
194
|
+
// covers the spotlight and the pointer as well as the pen and the arrow.
|
|
195
|
+
document.addEventListener('deck:tool-armed', showTools);
|
|
196
|
+
deck.addKeyBinding({ keyCode: CLOSE_KEY_CODE, key: 'X', description: 'Show or hide the toolbar' }, toggleTools);
|
|
197
|
+
|
|
162
198
|
// ---- the canvas -----------------------------------------------------
|
|
163
199
|
|
|
164
200
|
let width = 0;
|
package/runtime/assets.js
CHANGED
|
@@ -167,6 +167,32 @@ export const createAssets = ({ paste, active }) => {
|
|
|
167
167
|
const wordInput = confirmBox.querySelector('.deck-assets-word');
|
|
168
168
|
const confirmButton = confirmBox.querySelector('.deck-assets-confirm-delete');
|
|
169
169
|
|
|
170
|
+
// ---- the image preview --------------------------------------------------
|
|
171
|
+
// A click on an image's name opens the image itself, over everything — the
|
|
172
|
+
// drawer is narrow and a thumbnail would say less than the file name does.
|
|
173
|
+
// A click anywhere on it, or Escape, puts it away. Every other kind of file
|
|
174
|
+
// goes to a new tab instead: the browser knows a clip and a PDF better.
|
|
175
|
+
|
|
176
|
+
const preview = document.createElement('div');
|
|
177
|
+
preview.className = 'deck-assets-preview';
|
|
178
|
+
preview.hidden = true;
|
|
179
|
+
preview.innerHTML = `<figure><img alt="" /><figcaption><code></code></figcaption></figure>`;
|
|
180
|
+
const previewImage = preview.querySelector('img');
|
|
181
|
+
|
|
182
|
+
const openPreview = (path) => {
|
|
183
|
+
previewImage.src = path;
|
|
184
|
+
preview.querySelector('figcaption code').textContent = path;
|
|
185
|
+
preview.hidden = false;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const closePreview = () => {
|
|
189
|
+
preview.hidden = true;
|
|
190
|
+
// A 20 MB screenshot should stop arriving the moment nobody is looking.
|
|
191
|
+
previewImage.removeAttribute('src');
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
preview.addEventListener('click', closePreview);
|
|
195
|
+
|
|
170
196
|
// ---- the full-page sheet ----------------------------------------------
|
|
171
197
|
|
|
172
198
|
const overlay = document.createElement('div');
|
|
@@ -266,6 +292,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
266
292
|
nameRow.hidden = true;
|
|
267
293
|
waiting.hidden = true;
|
|
268
294
|
closeConfirm();
|
|
295
|
+
closePreview();
|
|
269
296
|
};
|
|
270
297
|
|
|
271
298
|
// ---- the rows ---------------------------------------------------------
|
|
@@ -309,10 +336,20 @@ export const createAssets = ({ paste, active }) => {
|
|
|
309
336
|
`<button type="button" class="deck-assets-insert" title="Write it into the slide">${INSERT_ICON}</button>` +
|
|
310
337
|
`<button type="button" class="deck-assets-trash" title="Delete the file">${TRASH_ICON}</button>` +
|
|
311
338
|
`</span>`;
|
|
312
|
-
row.querySelector('code')
|
|
339
|
+
const name = row.querySelector('code');
|
|
340
|
+
name.textContent = file.path;
|
|
313
341
|
// The drawer is narrow and a path can be long: the row truncates, and
|
|
314
|
-
// hovering
|
|
315
|
-
|
|
342
|
+
// hovering the name says the whole thing. Clicking it shows the file: an
|
|
343
|
+
// image right here, over the deck; anything else in a new tab, where the
|
|
344
|
+
// browser plays the clip, renders the PDF, and offers to save the rest.
|
|
345
|
+
name.title = file.path;
|
|
346
|
+
name.classList.add('is-viewable');
|
|
347
|
+
if (IMAGE.test(file.path)) {
|
|
348
|
+
name.classList.add('is-image');
|
|
349
|
+
name.addEventListener('click', () => openPreview(file.path));
|
|
350
|
+
} else {
|
|
351
|
+
name.addEventListener('click', () => window.open(file.path, '_blank', 'noopener'));
|
|
352
|
+
}
|
|
316
353
|
row.querySelector('.deck-assets-size').textContent = bytes(file.size);
|
|
317
354
|
row.querySelector('.deck-assets-copy').addEventListener('click', (event) =>
|
|
318
355
|
copy(file.path, event.currentTarget),
|
|
@@ -535,7 +572,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
535
572
|
drop([...event.dataTransfer.files], target?.dataset.dir ?? chosen);
|
|
536
573
|
});
|
|
537
574
|
|
|
538
|
-
document.body.append(overlay);
|
|
575
|
+
document.body.append(overlay, preview);
|
|
539
576
|
|
|
540
577
|
return {
|
|
541
578
|
element,
|
|
@@ -546,6 +583,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
546
583
|
// One layer per press, innermost first — the drawer asks this before it
|
|
547
584
|
// decides whether Escape was meant for it.
|
|
548
585
|
escape: () => {
|
|
586
|
+
if (!preview.hidden) return closePreview(), true;
|
|
549
587
|
if (!confirmBox.hidden) return closeConfirm(), true;
|
|
550
588
|
if (!element.hidden) return close(), true;
|
|
551
589
|
return false;
|