fb-slides 0.5.1 → 0.6.3
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 +118 -0
- package/README.md +99 -1
- package/lib/config.mjs +4 -0
- package/lib/render.mjs +1 -0
- package/package.json +2 -1
- package/runtime/annotate.js +11 -1
- package/runtime/deck.js +117 -1
- package/runtime/edit.js +76 -14
- package/runtime/outline.js +24 -5
- package/runtime/pointer.js +9 -1
- package/runtime/snippets.js +319 -0
- package/runtime/spotlight.js +9 -1
- package/runtime/theme.base.css +397 -30
- package/templates/starter/assets/author/photo-1.svg +11 -0
- package/templates/starter/assets/author/photo-2.svg +11 -0
- package/templates/starter/decks/01-intro.md +22 -0
- package/templates/starter/decks/02-demos.md +44 -6
- package/templates/starter/theme.css +0 -11
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What changed in each release, newest first. A deck picks the engine up from the
|
|
4
|
+
package, so `npm update fb-slides` is how a talk written months ago gets all of
|
|
5
|
+
this — which makes it worth being able to see what arrived.
|
|
6
|
+
|
|
7
|
+
Entries are written as the work lands, under **Unreleased**; the release commit
|
|
8
|
+
that stamps the version renames that heading to the version and its date.
|
|
9
|
+
|
|
10
|
+
## 0.6.3 — 2026-09-05
|
|
11
|
+
|
|
12
|
+
- **Disabled slides.** `<!-- disabled -->` on a line of its own keeps a slide in
|
|
13
|
+
the file without keeping it in the talk: the arrows walk past it, it holds no
|
|
14
|
+
slide number, and the count and the progress bar are computed as if it were
|
|
15
|
+
not there. It is still listed in the navigator and in reveal's overview,
|
|
16
|
+
dimmed and marked *off* — clicking it there opens it, which is the point of
|
|
17
|
+
keeping it.
|
|
18
|
+
|
|
19
|
+
## 0.6.2 — 2026-09-05
|
|
20
|
+
|
|
21
|
+
- **Fixed:** the toolbar could close itself. One tool arming told the others to
|
|
22
|
+
stand down, and the bar read that handshake as "nothing is armed" while a tool
|
|
23
|
+
still was. The armed state is now recomputed from the buttons themselves.
|
|
24
|
+
|
|
25
|
+
## 0.6.1 — 2026-09-05
|
|
26
|
+
|
|
27
|
+
- **The toolbar moved to the top edge**, and sleeps just above it: a chevron says
|
|
28
|
+
it is there, and the bar comes down when the pointer goes for it. The slides
|
|
29
|
+
keep the bottom of the screen.
|
|
30
|
+
|
|
31
|
+
## 0.6.0 — 2026-09-05
|
|
32
|
+
|
|
33
|
+
- **A vocabulary of slide layouts** in the base theme, so a deck stops writing
|
|
34
|
+
`style=""`: `.cols`/`.col` (two columns, with the `min-width: 0` a code block
|
|
35
|
+
falls over without), `.frame` (an embed that shares the slide), `.caption`,
|
|
36
|
+
`.box`, and `.author-slide` — the speaker page, photos down the left edge and
|
|
37
|
+
the bio on the right. Each fixes its numbers in CSS variables, so a project
|
|
38
|
+
retunes one from its own `theme.css`. Images and clips are now centred and
|
|
39
|
+
lose the white card reveal's core CSS frames every image with.
|
|
40
|
+
- **The speaker page's photos cross-fade** for however many there are: the
|
|
41
|
+
keyframes are written at load from the two timing tokens, instead of a fixed
|
|
42
|
+
table that stopped at eight.
|
|
43
|
+
- **A block picker in the edit drawer.** The `+` over the slide box — or `/` on
|
|
44
|
+
an empty line — writes the markup nobody remembers: the demo marker, a framed
|
|
45
|
+
embed, a video with the attributes that make it behave, two columns, a fenced
|
|
46
|
+
block, a mermaid graph, the speaker page. What was selected takes the place of
|
|
47
|
+
the first placeholder, so picking *Fragment* wraps the paragraph you had
|
|
48
|
+
highlighted. `snippets:` in `slides.config.js` adds a project's own.
|
|
49
|
+
- **The drawer takes half the page** on the chevron where its title used to be,
|
|
50
|
+
and stays that way across the reload every save ends in.
|
|
51
|
+
- **Add page / Delete page** say what they act on — the slide, not the text in
|
|
52
|
+
the box.
|
|
53
|
+
- **The starter** comes with the speaker page already written, second, against
|
|
54
|
+
two placeholder photos, and its demos deck now shows a framed embed next to
|
|
55
|
+
the two full-bleed ones.
|
|
56
|
+
|
|
57
|
+
## 0.5.1 — 2026-09-05
|
|
58
|
+
|
|
59
|
+
- The slide number under the arrows recedes: no pill, thinner ink, smaller type.
|
|
60
|
+
|
|
61
|
+
## 0.5.0 — 2026-09-05
|
|
62
|
+
|
|
63
|
+
- **The edit drawer.** `T` opens the slide you are looking at down the right
|
|
64
|
+
edge — its markdown in one box, its speaker notes in the other. Typing
|
|
65
|
+
re-renders the real slide in place, and **Save** writes the text back into that
|
|
66
|
+
slide's own lines of the `.md`, leaving the rest of the file alone. **Add** and
|
|
67
|
+
**Delete** work on the file too. Dev server only: a built deck never shows it.
|
|
68
|
+
- Docs: how to make a demo a real application.
|
|
69
|
+
|
|
70
|
+
## 0.4.3 — 2026-09-05
|
|
71
|
+
|
|
72
|
+
- **Fixed:** the spotlight left a ghost hole behind once its veil had faded.
|
|
73
|
+
|
|
74
|
+
## 0.4.2 — 2026-09-05
|
|
75
|
+
|
|
76
|
+
- The four tools moved onto the **QWER row**, in toolbar order, and each chip
|
|
77
|
+
wears its own key along its bottom edge.
|
|
78
|
+
|
|
79
|
+
## 0.4.1 — 2026-09-03
|
|
80
|
+
|
|
81
|
+
- Packaging fixes.
|
|
82
|
+
|
|
83
|
+
## 0.4.0 — 2026-09-03
|
|
84
|
+
|
|
85
|
+
- **The pointer**: the cursor swapped for a glowing halo, a centre-seeking dart,
|
|
86
|
+
or itself.
|
|
87
|
+
- **The arrow**: click the tail, then the tip — the pen's colours, the pen's
|
|
88
|
+
fade, and a head large enough to read from the back row.
|
|
89
|
+
- The toolbar moved to the bottom centre, its popovers opening upwards.
|
|
90
|
+
|
|
91
|
+
## 0.3.0 — 2026-09-03
|
|
92
|
+
|
|
93
|
+
- **The pen** holds its ink for two seconds before it fades, and the number keys
|
|
94
|
+
pick its colour.
|
|
95
|
+
|
|
96
|
+
## 0.2.0 — 2026-09-03
|
|
97
|
+
|
|
98
|
+
- **`revealTheme:`** — wear one of reveal.js's own fifteen themes, served from
|
|
99
|
+
the deck rather than a CDN. The chrome follows it through the theme's own
|
|
100
|
+
variables.
|
|
101
|
+
|
|
102
|
+
## 0.1.2 — 2026-09-03
|
|
103
|
+
|
|
104
|
+
- A hairline scrollbar for the navigator.
|
|
105
|
+
- `npm run starter` runs the starter template in place.
|
|
106
|
+
|
|
107
|
+
## 0.1.1 — 2026-09-03
|
|
108
|
+
|
|
109
|
+
- **The vertical navigator** (`V`): the deck as a readable list down the left
|
|
110
|
+
edge, with a list/preview toggle and the talk's own structure as its depth.
|
|
111
|
+
- **The spotlight**, and a shortcuts registry behind the help overlay.
|
|
112
|
+
|
|
113
|
+
## 0.1.0 — 2026-09-03
|
|
114
|
+
|
|
115
|
+
First release. Markdown files in `decks/` become a reveal.js deck with no build
|
|
116
|
+
step: live demo embeds that run in the slide, mermaid diagrams, stepped code
|
|
117
|
+
highlighting, speaker notes, an annotation pen, a zero-config dev server, and a
|
|
118
|
+
`build` that writes a folder you can publish anywhere.
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ cd my-talk && npm install && npm run dev
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
The engine is the package; a talk is only its own content. Bump the version and every
|
|
13
|
-
deck you have written gets the new features.
|
|
13
|
+
deck you have written gets the new features — [CHANGELOG.md](CHANGELOG.md) says which.
|
|
14
14
|
|
|
15
15
|
## The shape of a talk
|
|
16
16
|
|
|
@@ -127,6 +127,87 @@ A `Note:` block at the end of a slide never shows on screen, only in the speaker
|
|
|
127
127
|
Note: a bad description is the most common reason a tool never gets used.
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### Disabled slides
|
|
131
|
+
|
|
132
|
+
A slide whose source carries `<!-- disabled -->` on a line of its own stays in the
|
|
133
|
+
file and in the navigator, but steps out of the talk:
|
|
134
|
+
|
|
135
|
+
```markdown
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
<!-- disabled -->
|
|
139
|
+
|
|
140
|
+
## The version of this I cut on the train
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The arrows walk past it in whichever direction they were going, it holds no slide
|
|
146
|
+
number, and the count and the progress bar are computed as if it were not there.
|
|
147
|
+
It is still listed in the navigator (`V`) and in reveal's overview, marked **off** —
|
|
148
|
+
and clicking it there is how you look at it, which is the point of keeping it.
|
|
149
|
+
Looking at one, the corner reads `off` where the number would be.
|
|
150
|
+
|
|
151
|
+
Half a talk is the slides you decided against but are not ready to delete. This is
|
|
152
|
+
where they live, next to the ones that made it.
|
|
153
|
+
|
|
154
|
+
### Blocks
|
|
155
|
+
|
|
156
|
+
Six classes cover the layouts a talk keeps needing, so a slide asks for one by name
|
|
157
|
+
instead of carrying a paragraph of inline styles:
|
|
158
|
+
|
|
159
|
+
| | |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| `.cols` / `.col` | two columns — with the `min-width: 0` a code block falls over without |
|
|
162
|
+
| `.frame` | an embed that shares the slide instead of taking it whole |
|
|
163
|
+
| `.caption` | a quiet line under a figure |
|
|
164
|
+
| `.box` | a bordered block: the point the slide comes back to |
|
|
165
|
+
| `.author-slide` | the speaker page — photos down the left edge, the bio on the right |
|
|
166
|
+
| `<img>`, `<video>` | centred, unframed, and never wider than the slide |
|
|
167
|
+
|
|
168
|
+
````markdown
|
|
169
|
+
<div class="cols">
|
|
170
|
+
<div class="col">
|
|
171
|
+
|
|
172
|
+
**In the widget**
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
app.openLink({ url: 'https://example.com/' });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
</div>
|
|
179
|
+
<div class="col">
|
|
180
|
+
|
|
181
|
+
The blank lines matter: Markdown inside a block-level tag is only parsed
|
|
182
|
+
when there is one on each side of it.
|
|
183
|
+
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
````
|
|
187
|
+
|
|
188
|
+
Each of them fixes its numbers in CSS variables — `--cols-gap`, `--frame-w`,
|
|
189
|
+
`--frame-h`, `--author-photo`, `--author-hold` — so a project retunes one from its own
|
|
190
|
+
`theme.css` without restating the rule.
|
|
191
|
+
|
|
192
|
+
The speaker page is markup you write and the theme lays out. Give `.author-photo` as
|
|
193
|
+
many `<img>` as you like: they cross-fade in the order they are written, on a loop that
|
|
194
|
+
restarts every time the slide comes up.
|
|
195
|
+
|
|
196
|
+
```markdown
|
|
197
|
+
<!-- .slide: class="author-slide" -->
|
|
198
|
+
|
|
199
|
+
<div class="author-photo">
|
|
200
|
+
<img src="assets/author/on-stage.jpg" alt="" />
|
|
201
|
+
<img src="assets/author/portrait.jpg" alt="" />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="author-bio">
|
|
205
|
+
<h1>Your name</h1>
|
|
206
|
+
<ul><li>What you do</li></ul>
|
|
207
|
+
<p class="author-meta">The stack · you · work · with</p>
|
|
208
|
+
</div>
|
|
209
|
+
```
|
|
210
|
+
|
|
130
211
|
### Editing in the browser
|
|
131
212
|
|
|
132
213
|
On the dev server, `T` (or the last button on the toolbar) opens the slide you
|
|
@@ -140,6 +221,22 @@ right after the current one and opens it; **Delete** removes the slide from its
|
|
|
140
221
|
file, after asking. Clicking into the notes box trades the room with the slide
|
|
141
222
|
box, so both are comfortable to write in.
|
|
142
223
|
|
|
224
|
+
The `+` over the slide box — or `/` typed on an empty line — opens the block
|
|
225
|
+
picker: the demo marker, a framed embed, a video with the attributes that make
|
|
226
|
+
it behave, two columns, a fenced block, the speaker page. The block lands where
|
|
227
|
+
the cursor was, with its first placeholder selected so it is ready to type over;
|
|
228
|
+
text already selected takes that placeholder's place, so picking **Fragment**
|
|
229
|
+
wraps the paragraph that was highlighted. A block that only works at the top of
|
|
230
|
+
a slide, like the demo marker, goes there whatever the cursor was doing.
|
|
231
|
+
|
|
232
|
+
`snippets:` in `slides.config.js` adds a project's own blocks to the list:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
snippets: [
|
|
236
|
+
{ label: 'Pricing table', hint: 'the three tiers', body: '<div class="tiers">${…}</div>' },
|
|
237
|
+
],
|
|
238
|
+
```
|
|
239
|
+
|
|
143
240
|
The drawer exists only under `fb-slides dev`: a built deck is static files and
|
|
144
241
|
never shows the button.
|
|
145
242
|
|
|
@@ -194,6 +291,7 @@ export default {
|
|
|
194
291
|
outDir: 'dist',
|
|
195
292
|
exclude: ['dist'], // extra path segments the build skips
|
|
196
293
|
fragmentLists: true, // bullet lists reveal one item at a time
|
|
294
|
+
snippets: [], // your own blocks in the edit drawer's picker
|
|
197
295
|
reveal: { transition: 'fade' },// passed to Reveal.initialize()
|
|
198
296
|
mermaid: {}, // passed to mermaid.initialize()
|
|
199
297
|
};
|
package/lib/config.mjs
CHANGED
|
@@ -80,6 +80,10 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
80
80
|
reveal: user.reveal ?? {},
|
|
81
81
|
// Bullet lists of 3+ items appear one item at a time.
|
|
82
82
|
fragmentLists: user.fragmentLists ?? true,
|
|
83
|
+
// The project's own entries in the edit drawer's block picker, after the
|
|
84
|
+
// built-in ones: `{ label, hint, body, top }`, where `${…}` in the body
|
|
85
|
+
// marks what has to be typed over.
|
|
86
|
+
snippets: user.snippets ?? [],
|
|
83
87
|
// The build output, at any depth: a framework demo's own dist/ is its dev
|
|
84
88
|
// server's business, and the deck ships that demo as sources.
|
|
85
89
|
exclude: [...ALWAYS_EXCLUDED, overrides.outDir ?? user.outDir ?? 'dist', ...(user.exclude ?? [])],
|
package/lib/render.mjs
CHANGED
|
@@ -46,6 +46,7 @@ export const renderIndex = async (config, runtimeDir) => {
|
|
|
46
46
|
demos: config.urls.demos,
|
|
47
47
|
reveal: config.reveal,
|
|
48
48
|
fragmentLists: config.fragmentLists,
|
|
49
|
+
snippets: config.snippets,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
// The chrome reads this to take its colours from the reveal theme instead of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fb-slides",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Markdown-driven reveal.js decks: live demo embeds, annotation, mermaid, and a zero-config dev server",
|
|
6
6
|
"keywords": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"lib",
|
|
31
31
|
"runtime",
|
|
32
32
|
"templates",
|
|
33
|
+
"CHANGELOG.md",
|
|
33
34
|
"README.md",
|
|
34
35
|
"LICENSE"
|
|
35
36
|
],
|
package/runtime/annotate.js
CHANGED
|
@@ -477,7 +477,17 @@ const RevealAnnotate = () => ({
|
|
|
477
477
|
for (const [name, button] of Object.entries(toolButtons)) {
|
|
478
478
|
button.setAttribute('aria-pressed', String(name === tool));
|
|
479
479
|
}
|
|
480
|
-
|
|
480
|
+
// `is-armed` is shared with the other tool plugins, and the arming
|
|
481
|
+
// handshake fires mid-switch: the newcomer sets the class, then its
|
|
482
|
+
// event runs this module's stand-down — so a plain toggle with this
|
|
483
|
+
// module's own boolean would wipe it with a tool still armed. It is
|
|
484
|
+
// recomputed from the buttons instead, which every module keeps
|
|
485
|
+
// truthful. The edit chip stays out of it: its pressed state is the
|
|
486
|
+
// drawer, not an armed tool.
|
|
487
|
+
toolbar.classList.toggle(
|
|
488
|
+
'is-armed',
|
|
489
|
+
!!toolbar.querySelector('.deck-tool[aria-pressed="true"]:not(#tool-edit)'),
|
|
490
|
+
);
|
|
481
491
|
// The colours are only worth showing while there is a tool to apply them
|
|
482
492
|
// to, which makes arming one the gesture that opens them.
|
|
483
493
|
swatches.hidden = tool === null;
|
package/runtime/deck.js
CHANGED
|
@@ -61,6 +61,13 @@ if (!DECKS.length) console.error(`[deck] no .md found in ${DECKS_DIR}`);
|
|
|
61
61
|
const DEMO_MARKER = /^<!--\s*demo:\s*(\S+?)\s*-->/;
|
|
62
62
|
const DEMOS_DIR = CFG.demos ?? 'demo/';
|
|
63
63
|
|
|
64
|
+
// A slide whose source carries `<!-- disabled -->` stays in the file and in the
|
|
65
|
+
// navigator, but steps out of the talk: the arrows walk past it and it is not
|
|
66
|
+
// counted. The line is stripped before rendering — what is left is an ordinary
|
|
67
|
+
// slide, demo marker included, for the day it is turned back on.
|
|
68
|
+
const DISABLED_MARKER = /^[ \t]*<!--[ \t]*disabled[ \t]*-->[ \t]*\r?\n?/im;
|
|
69
|
+
const OFF = 'slide-off';
|
|
70
|
+
|
|
64
71
|
// Bullet lists reveal themselves one item at a time. `?nofrag` turns it off for
|
|
65
72
|
// one visit; `fragmentLists: false` in the config turns it off for the project.
|
|
66
73
|
const AUTO_FRAGMENT = CFG.fragmentLists !== false && !new URLSearchParams(location.search).has('nofrag');
|
|
@@ -135,9 +142,18 @@ for (const file of DECKS) {
|
|
|
135
142
|
// `section:` names the deck; without one the file name has to do.
|
|
136
143
|
const deck = { file, label: front.section ?? file.replace(/^\d+-|\.md$/g, '') };
|
|
137
144
|
|
|
138
|
-
for (const [index,
|
|
145
|
+
for (const [index, raw] of splitSlides(stripFrontMatter(markdown)).entries()) {
|
|
146
|
+
const disabled = DISABLED_MARKER.test(raw);
|
|
147
|
+
const source = disabled ? raw.replace(DISABLED_MARKER, '') : raw;
|
|
139
148
|
const marker = source.match(DEMO_MARKER);
|
|
140
149
|
const section = marker ? demoSection(marker[1], deck) : markdownSection(source, deck);
|
|
150
|
+
if (disabled) {
|
|
151
|
+
section.classList.add(OFF);
|
|
152
|
+
// Reveal's own answer to "in the deck, out of the count": it leaves an
|
|
153
|
+
// uncounted slide out of `getSlides()`, the slide number and the progress
|
|
154
|
+
// bar, while keeping it reachable by index.
|
|
155
|
+
section.dataset.visibility = 'uncounted';
|
|
156
|
+
}
|
|
141
157
|
// Where the slide came from — the file and its index within it, counted by
|
|
142
158
|
// the same split as above. The edit drawer saves back through these; the
|
|
143
159
|
// markdown plugin forwards data-* attributes when it rewrites the section.
|
|
@@ -197,6 +213,54 @@ const nudge = (slide) => {
|
|
|
197
213
|
Reveal.on('ready', ({ currentSlide }) => nudge(currentSlide));
|
|
198
214
|
Reveal.on('slidechanged', ({ currentSlide }) => nudge(currentSlide));
|
|
199
215
|
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Walking past the disabled slides. Reveal already leaves them out of the
|
|
218
|
+
// count; the talk itself steps over them here, in whichever direction it was
|
|
219
|
+
// already going. Two ways in are deliberate and let through: picking a slide by
|
|
220
|
+
// hand — the navigator, reveal's overview — and landing on one at load, which
|
|
221
|
+
// is what the edit drawer does on every save.
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const sections = () => [...slidesEl.querySelectorAll(':scope > section')];
|
|
225
|
+
|
|
226
|
+
let jumping = false;
|
|
227
|
+
// Cleared on the next turn of the loop: `slidechanged` fires inside the call
|
|
228
|
+
// that follows, so a click that lands on the slide already showing — no event
|
|
229
|
+
// at all — cannot leave the flag standing for the next press of an arrow.
|
|
230
|
+
const jump = () => {
|
|
231
|
+
jumping = true;
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
jumping = false;
|
|
234
|
+
}, 0);
|
|
235
|
+
};
|
|
236
|
+
document.addEventListener('deck:jump', jump);
|
|
237
|
+
slidesEl.addEventListener('click', () => Reveal.isOverview() && jump(), true);
|
|
238
|
+
|
|
239
|
+
let cameFrom = Reveal.getIndices().h;
|
|
240
|
+
|
|
241
|
+
Reveal.on('slidechanged', ({ indexh, currentSlide }) => {
|
|
242
|
+
const deliberate = jumping;
|
|
243
|
+
jumping = false;
|
|
244
|
+
if (deliberate || !currentSlide?.classList.contains(OFF)) {
|
|
245
|
+
cameFrom = indexh;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const slides = sections();
|
|
250
|
+
const step = indexh < cameFrom ? -1 : 1;
|
|
251
|
+
const seek = (start, direction) => {
|
|
252
|
+
let i = start;
|
|
253
|
+
while (slides[i]?.classList.contains(OFF)) i += direction;
|
|
254
|
+
return slides[i] ? i : null;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Off the end — the deck ends on disabled slides — turn round rather than
|
|
258
|
+
// strand the talk on one it is not meant to show.
|
|
259
|
+
const target = seek(indexh + step, step) ?? seek(indexh - step, -step);
|
|
260
|
+
if (target === null) cameFrom = indexh; // every slide is disabled: stay put
|
|
261
|
+
else Reveal.slide(target);
|
|
262
|
+
});
|
|
263
|
+
|
|
200
264
|
// ---------------------------------------------------------------------------
|
|
201
265
|
// Post-processing — everything below runs on markdown the plugin has rendered.
|
|
202
266
|
// ---------------------------------------------------------------------------
|
|
@@ -294,6 +358,58 @@ for (const pre of document.querySelectorAll('.slides pre')) {
|
|
|
294
358
|
if (lines > 14) pre.classList.add('pre--dense');
|
|
295
359
|
}
|
|
296
360
|
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// The speaker page's photos cross-fade in DOM order (`.author-slide` in
|
|
363
|
+
// theme.base.css). Where each fade lands depends on how many photos there are
|
|
364
|
+
// and on the two timing tokens a project can retune — so the keyframes each
|
|
365
|
+
// strip needs are written here, instead of shipping a set for every count the
|
|
366
|
+
// theme might ever be handed.
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
const seconds = (value) => {
|
|
370
|
+
const time = Number.parseFloat(value);
|
|
371
|
+
if (!Number.isFinite(time)) return 0;
|
|
372
|
+
return value.trim().endsWith('ms') ? time / 1000 : time;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const strips = document.querySelectorAll('.slides .author-photo');
|
|
376
|
+
if (strips.length) {
|
|
377
|
+
const sheet = document.createElement('style');
|
|
378
|
+
document.head.append(sheet);
|
|
379
|
+
const written = new Set();
|
|
380
|
+
|
|
381
|
+
for (const strip of strips) {
|
|
382
|
+
const photos = [...strip.querySelectorAll('img')];
|
|
383
|
+
photos.forEach((photo, i) => photo.style.setProperty('--i', i));
|
|
384
|
+
// One photo has nothing to fade to, and the CSS already leaves it showing.
|
|
385
|
+
if (photos.length < 2) continue;
|
|
386
|
+
|
|
387
|
+
const css = getComputedStyle(strip);
|
|
388
|
+
const hold = seconds(css.getPropertyValue('--author-hold'));
|
|
389
|
+
const fade = seconds(css.getPropertyValue('--author-fade'));
|
|
390
|
+
const slot = hold + fade;
|
|
391
|
+
if (!slot) continue;
|
|
392
|
+
|
|
393
|
+
const total = photos.length * slot;
|
|
394
|
+
const at = (time) => `${((time / total) * 100).toFixed(3)}%`;
|
|
395
|
+
// Named for what it draws: two strips of the same length and timing — the
|
|
396
|
+
// ordinary case, since both come from the same tokens — share one set.
|
|
397
|
+
const name = `author-fade-${photos.length}-${Math.round(hold * 1000)}-${Math.round(fade * 1000)}`;
|
|
398
|
+
if (!written.has(name)) {
|
|
399
|
+
written.add(name);
|
|
400
|
+
sheet.sheet.insertRule(
|
|
401
|
+
`@keyframes ${name} {` +
|
|
402
|
+
`0% { opacity: 0 }` +
|
|
403
|
+
`${at(fade)}, ${at(slot)} { opacity: 1 }` +
|
|
404
|
+
`${at(slot + fade)}, 100% { opacity: 0 }` +
|
|
405
|
+
`}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
strip.style.setProperty('--author-n', photos.length);
|
|
409
|
+
strip.style.setProperty('--author-anim', name);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
297
413
|
Reveal.sync();
|
|
298
414
|
|
|
299
415
|
// Which deck are we in? Shown in the corner, hidden on dividers.
|
package/runtime/edit.js
CHANGED
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
// slide's own range of the .md (the server owns that arithmetic) and reloads:
|
|
14
14
|
// the URL hash puts the deck back on this slide, and the full pipeline —
|
|
15
15
|
// mermaid, fragments, highlight steps — runs over the saved source.
|
|
16
|
+
//
|
|
17
|
+
// The `+` over the slide box — or `/` on an empty line — opens the block
|
|
18
|
+
// picker, which writes the markup nobody should have to remember (snippets.js).
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
|
|
21
|
+
import { createSnippets, PLUS_ICON } from './snippets.js';
|
|
22
|
+
|
|
18
23
|
// Where the slide came from, stamped on every section by deck.js: the .md file
|
|
19
24
|
// and the slide's index within it, counted the way the server counts.
|
|
20
25
|
const SRC = (slide) => {
|
|
@@ -116,19 +121,22 @@ const build = (deck) => {
|
|
|
116
121
|
panel.id = 'deck-edit';
|
|
117
122
|
panel.innerHTML = `
|
|
118
123
|
<header>
|
|
119
|
-
<
|
|
124
|
+
<button type="button" class="deck-edit-wide" aria-pressed="false"></button>
|
|
120
125
|
<code class="deck-edit-file"></code>
|
|
121
126
|
<button type="button" class="deck-edit-nav" data-go="-1" title="Previous slide" aria-label="Previous slide">${PREV_ICON}</button>
|
|
122
127
|
<button type="button" class="deck-edit-nav" data-go="1" title="Next slide" aria-label="Next slide">${NEXT_ICON}</button>
|
|
123
128
|
<button type="button" class="deck-edit-close" aria-label="Close">${CLOSE_ICON}</button>
|
|
124
129
|
</header>
|
|
125
|
-
<
|
|
130
|
+
<div class="deck-edit-row">
|
|
131
|
+
<label class="deck-edit-label" for="deck-edit-source">slide</label>
|
|
132
|
+
<button type="button" class="deck-edit-snip" title="Insert a block (/)" aria-label="Insert a block">${PLUS_ICON}</button>
|
|
133
|
+
</div>
|
|
126
134
|
<textarea id="deck-edit-source" spellcheck="false"></textarea>
|
|
127
135
|
<label class="deck-edit-label" for="deck-edit-notes">speaker notes</label>
|
|
128
136
|
<textarea id="deck-edit-notes" spellcheck="false"></textarea>
|
|
129
137
|
<footer>
|
|
130
|
-
<button type="button" class="deck-edit-add" title="Add a slide after this one">Add</button>
|
|
131
|
-
<button type="button" class="deck-edit-delete" title="Delete this slide">Delete</button>
|
|
138
|
+
<button type="button" class="deck-edit-add" title="Add a slide after this one">Add page</button>
|
|
139
|
+
<button type="button" class="deck-edit-delete" title="Delete this slide">Delete page</button>
|
|
132
140
|
<span class="deck-edit-status" role="status"></span>
|
|
133
141
|
<button type="button" class="deck-edit-save">Save</button>
|
|
134
142
|
</footer>
|
|
@@ -144,6 +152,7 @@ const build = (deck) => {
|
|
|
144
152
|
panel.inert = true;
|
|
145
153
|
|
|
146
154
|
const fileLabel = panel.querySelector('.deck-edit-file');
|
|
155
|
+
const wideButton = panel.querySelector('.deck-edit-wide');
|
|
147
156
|
const sourceArea = panel.querySelector('#deck-edit-source');
|
|
148
157
|
const notesArea = panel.querySelector('#deck-edit-notes');
|
|
149
158
|
const status = panel.querySelector('.deck-edit-status');
|
|
@@ -190,6 +199,13 @@ const build = (deck) => {
|
|
|
190
199
|
frame ||= requestAnimationFrame(paint);
|
|
191
200
|
};
|
|
192
201
|
|
|
202
|
+
// ---- the block picker ---------------------------------------------------
|
|
203
|
+
// Over the drawer, like the delete question: the deck keeps the stage, and
|
|
204
|
+
// what the picker writes goes through the same preview typing does.
|
|
205
|
+
|
|
206
|
+
const snippets = createSnippets({ area: sourceArea, paint: schedulePaint });
|
|
207
|
+
panel.append(snippets.element);
|
|
208
|
+
|
|
193
209
|
// ---- open and close ---------------------------------------------------
|
|
194
210
|
// The deck is narrowed by CSS while the drawer is out (see theme.base.css);
|
|
195
211
|
// reveal only rescales when told to, so layout() is driven for as long as
|
|
@@ -204,6 +220,29 @@ const build = (deck) => {
|
|
|
204
220
|
requestAnimationFrame(step);
|
|
205
221
|
};
|
|
206
222
|
|
|
223
|
+
// Half the page for a slide that is mostly markup, the narrow drawer for one
|
|
224
|
+
// that is mostly words. The width is a token, so saying it on the body is
|
|
225
|
+
// enough — the deck, the badge, the signature and the toolbar all step aside
|
|
226
|
+
// through the same `calc()` they already use. It survives the reload a save
|
|
227
|
+
// ends in, which is the only way it could be worth setting at all.
|
|
228
|
+
const WIDE_KEY = 'fb-slides.edit-wide';
|
|
229
|
+
|
|
230
|
+
const setWide = (wide) => {
|
|
231
|
+
document.body.classList.toggle('edit-wide', wide);
|
|
232
|
+
wideButton.setAttribute('aria-pressed', String(wide));
|
|
233
|
+
wideButton.innerHTML = wide ? NEXT_ICON : PREV_ICON;
|
|
234
|
+
const label = wide ? 'Narrow the drawer' : 'Widen the drawer to half the page';
|
|
235
|
+
wideButton.title = label;
|
|
236
|
+
wideButton.setAttribute('aria-label', label);
|
|
237
|
+
try {
|
|
238
|
+
if (wide) sessionStorage.setItem(WIDE_KEY, '1');
|
|
239
|
+
else sessionStorage.removeItem(WIDE_KEY);
|
|
240
|
+
} catch {
|
|
241
|
+
/* walled-off storage only costs the width after a reload */
|
|
242
|
+
}
|
|
243
|
+
reflow();
|
|
244
|
+
};
|
|
245
|
+
|
|
207
246
|
const setOpen = (open) => {
|
|
208
247
|
document.body.classList.toggle('edit-open', open);
|
|
209
248
|
button.setAttribute('aria-pressed', String(open));
|
|
@@ -216,6 +255,7 @@ const build = (deck) => {
|
|
|
216
255
|
const discard = () => {
|
|
217
256
|
cancelAnimationFrame(frame);
|
|
218
257
|
frame = 0;
|
|
258
|
+
snippets.close({ restore: false });
|
|
219
259
|
confirmBox.hidden = true;
|
|
220
260
|
editing.slide.innerHTML = editing.html;
|
|
221
261
|
// The original fragments are back; reveal counts them again.
|
|
@@ -230,7 +270,10 @@ const build = (deck) => {
|
|
|
230
270
|
deck.layout();
|
|
231
271
|
};
|
|
232
272
|
|
|
233
|
-
|
|
273
|
+
// `focus` names the box that gets the room and the cursor — navigation and
|
|
274
|
+
// save-reloads pass the one the drawer already had, so following the deck
|
|
275
|
+
// never folds the notes away under a writing hand.
|
|
276
|
+
const open = async (focus = 'source') => {
|
|
234
277
|
const slide = deck.getCurrentSlide();
|
|
235
278
|
const src = SRC(slide);
|
|
236
279
|
if (!src) return;
|
|
@@ -266,15 +309,16 @@ const build = (deck) => {
|
|
|
266
309
|
fileLabel.textContent = src.file;
|
|
267
310
|
sourceArea.value = content;
|
|
268
311
|
notesArea.value = notes;
|
|
312
|
+
snippets.reset();
|
|
269
313
|
status.textContent = '';
|
|
270
314
|
saveButton.disabled = false;
|
|
271
|
-
panel.dataset.focus =
|
|
315
|
+
panel.dataset.focus = focus;
|
|
272
316
|
|
|
273
317
|
// One pointer, one tool: the pen, spotlight and pointer stand down — and
|
|
274
318
|
// stand this drawer down in turn when one of them arms (see below).
|
|
275
319
|
document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'edit' }));
|
|
276
320
|
setOpen(true);
|
|
277
|
-
sourceArea.focus();
|
|
321
|
+
(focus === 'notes' ? notesArea : sourceArea).focus();
|
|
278
322
|
};
|
|
279
323
|
|
|
280
324
|
const toggle = () => (editing ? close() : open());
|
|
@@ -287,9 +331,11 @@ const build = (deck) => {
|
|
|
287
331
|
|
|
288
332
|
const REOPEN_KEY = 'fb-slides.edit-reopen';
|
|
289
333
|
|
|
334
|
+
// The flag's value is the box that was open, so the reload hands the room
|
|
335
|
+
// (and the cursor) back to it.
|
|
290
336
|
const reloadEditing = () => {
|
|
291
337
|
try {
|
|
292
|
-
sessionStorage.setItem(REOPEN_KEY, '
|
|
338
|
+
sessionStorage.setItem(REOPEN_KEY, panel.dataset.focus === 'notes' ? 'notes' : 'source');
|
|
293
339
|
} catch {
|
|
294
340
|
/* walled-off storage only costs the reopen */
|
|
295
341
|
}
|
|
@@ -348,6 +394,10 @@ const build = (deck) => {
|
|
|
348
394
|
|
|
349
395
|
button.addEventListener('click', toggle);
|
|
350
396
|
panel.querySelector('.deck-edit-close').addEventListener('click', close);
|
|
397
|
+
wideButton.addEventListener('click', () =>
|
|
398
|
+
setWide(!document.body.classList.contains('edit-wide')),
|
|
399
|
+
);
|
|
400
|
+
panel.querySelector('.deck-edit-snip').addEventListener('click', () => snippets.open());
|
|
351
401
|
saveButton.addEventListener('click', save);
|
|
352
402
|
sourceArea.addEventListener('input', schedulePaint);
|
|
353
403
|
|
|
@@ -400,8 +450,10 @@ const build = (deck) => {
|
|
|
400
450
|
'keydown',
|
|
401
451
|
(event) => {
|
|
402
452
|
if (event.key !== 'Escape' || !editing) return;
|
|
403
|
-
// One layer at a time:
|
|
404
|
-
|
|
453
|
+
// One layer at a time, innermost first: the picker, then a pending
|
|
454
|
+
// delete question, then the drawer itself.
|
|
455
|
+
if (snippets.isOpen()) snippets.close();
|
|
456
|
+
else if (confirmBox.hidden) close();
|
|
405
457
|
else confirmBox.hidden = true;
|
|
406
458
|
event.stopPropagation();
|
|
407
459
|
event.preventDefault();
|
|
@@ -416,13 +468,22 @@ const build = (deck) => {
|
|
|
416
468
|
if (event.detail !== 'edit') close();
|
|
417
469
|
});
|
|
418
470
|
|
|
471
|
+
let wideAtStart = false;
|
|
472
|
+
try {
|
|
473
|
+
wideAtStart = Boolean(sessionStorage.getItem(WIDE_KEY));
|
|
474
|
+
} catch {
|
|
475
|
+
/* walled-off storage: the drawer opens at its usual width */
|
|
476
|
+
}
|
|
477
|
+
setWide(wideAtStart);
|
|
478
|
+
|
|
419
479
|
// A write reloaded the page with the drawer out: pick the editing back up
|
|
420
480
|
// on whatever slide the hash landed on.
|
|
421
481
|
try {
|
|
422
|
-
|
|
482
|
+
const reopen = sessionStorage.getItem(REOPEN_KEY);
|
|
483
|
+
if (reopen) {
|
|
423
484
|
sessionStorage.removeItem(REOPEN_KEY);
|
|
424
|
-
if (deck.isReady?.()) open();
|
|
425
|
-
else deck.on('ready', open);
|
|
485
|
+
if (deck.isReady?.()) open(reopen);
|
|
486
|
+
else deck.on('ready', () => open(reopen));
|
|
426
487
|
}
|
|
427
488
|
} catch {
|
|
428
489
|
/* walled-off storage: the drawer just stays closed */
|
|
@@ -433,8 +494,9 @@ const build = (deck) => {
|
|
|
433
494
|
// overview is a different surface altogether, so there it does close.
|
|
434
495
|
deck.on('slidechanged', async () => {
|
|
435
496
|
if (!editing) return;
|
|
497
|
+
const focus = panel.dataset.focus;
|
|
436
498
|
discard();
|
|
437
|
-
await open();
|
|
499
|
+
await open(focus);
|
|
438
500
|
// The new slide would not load — a drawer left standing would lie about
|
|
439
501
|
// what it edits.
|
|
440
502
|
if (!editing) setOpen(false);
|