fb-slides 0.6.2 → 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 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,30 @@ 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
+
130
154
  ### Blocks
131
155
 
132
156
  Six classes cover the layouts a talk keeps needing, so a slide asks for one by name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fb-slides",
3
- "version": "0.6.2",
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/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, source] of splitSlides(stripFrontMatter(markdown)).entries()) {
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
  // ---------------------------------------------------------------------------
@@ -85,8 +85,17 @@ const rowsFor = (deck) => {
85
85
  const rows = [];
86
86
  let section = null;
87
87
  let dividerSeen = false;
88
+ let number = 0;
88
89
 
89
- for (const [index, slide] of deck.getSlides().entries()) {
90
+ // Not `deck.getSlides()`: reveal leaves the disabled slides out of that list,
91
+ // and a slide that is out of the talk is exactly what someone opens the
92
+ // navigator to reach. They are listed, marked, and numbered by what they are
93
+ // — nothing, since the deck is counted as if they were not there.
94
+ const slides = [...document.querySelectorAll('.reveal .slides section')].filter(
95
+ (slide) => !slide.classList.contains('stack'),
96
+ );
97
+
98
+ for (const slide of slides) {
90
99
  const stack = slide.parentElement.matches('section') ? slide.parentElement : null;
91
100
  const owner = stack ?? slide;
92
101
  const label = owner.dataset.deck ?? '';
@@ -105,8 +114,10 @@ const rowsFor = (deck) => {
105
114
  const vertical = Boolean(stack) && slide !== stack.firstElementChild;
106
115
  const depth = vertical ? base + 1 : base;
107
116
 
108
- slide.dataset.outlineNumber = index + 1;
109
- rows.push({ kind: 'slide', slide, depth, number: index + 1, divider, vertical });
117
+ const off = slide.classList.contains('slide-off');
118
+ if (!off) number += 1;
119
+ slide.dataset.outlineNumber = off ? '' : number;
120
+ rows.push({ kind: 'slide', slide, depth, number: off ? null : number, divider, vertical, off });
110
121
  }
111
122
  return rows;
112
123
  };
@@ -241,13 +252,19 @@ const RevealOutline = () => ({
241
252
  item.dataset.depth = Math.min(row.depth, 2);
242
253
  if (row.divider) item.classList.add('is-divider');
243
254
  if (row.vertical) item.classList.add('is-vertical');
255
+ if (row.off) item.classList.add('is-off');
256
+ // A disabled slide has no number to show, so the corner says what it
257
+ // has instead of one.
258
+ const number = row.off
259
+ ? '<span class="deck-outline-n is-off">off</span>'
260
+ : `<span class="deck-outline-n">${row.number}</span>`;
244
261
  if (view === 'preview') {
245
- item.innerHTML = `<span class="deck-outline-n">${row.number}</span>`;
262
+ item.innerHTML = number;
246
263
  item.prepend(thumbFor(row.slide));
247
264
  // No room for the title in the row; it moves to the tooltip.
248
265
  item.title = titleOf(row.slide);
249
266
  } else {
250
- item.innerHTML = `<span class="deck-outline-title"></span><span class="deck-outline-n">${row.number}</span>`;
267
+ item.innerHTML = `<span class="deck-outline-title"></span>${number}`;
251
268
  item.querySelector('.deck-outline-title').textContent = titleOf(row.slide);
252
269
  }
253
270
  return item;
@@ -303,6 +320,8 @@ const RevealOutline = () => ({
303
320
  const item = event.target.closest('.deck-outline-item');
304
321
  if (!item) return;
305
322
  const { h, v } = deck.getIndices(rows[item.dataset.row].slide);
323
+ // Picked by hand: a disabled slide is shown rather than walked past.
324
+ document.dispatchEvent(new CustomEvent('deck:jump'));
306
325
  deck.slide(h, v);
307
326
  // Clicked with the mouse, the button would keep the focus and swallow the
308
327
  // next Space as a re-click. Activated from the keyboard (`detail === 0`)
@@ -105,6 +105,12 @@ const BUILT_IN = [
105
105
  ' <p class="author-meta">❤️ Life outside work — <strong>yoursite.dev</strong></p>\n' +
106
106
  '</div>',
107
107
  },
108
+ {
109
+ label: 'Disabled',
110
+ hint: 'keep the slide, leave it out of the talk',
111
+ top: true,
112
+ body: '<!-- disabled -->',
113
+ },
108
114
  {
109
115
  label: 'Slide class',
110
116
  hint: 'hand this slide a class of your own',
@@ -979,6 +979,24 @@ body.outline-open #deck-outline { transform: none; }
979
979
  opacity: 0.55;
980
980
  }
981
981
 
982
+ /* A disabled slide — `<!-- disabled -->` in its source. It is listed, because
983
+ the navigator is how it is reached at all, but it reads as set aside: the
984
+ talk walks past it and it holds no number. Still a button, still hovers,
985
+ still opens — clicking one is how you look at it. */
986
+ .deck-outline-item.is-off .deck-outline-title { opacity: 0.4; }
987
+ .deck-outline-item.is-off .deck-outline-thumb { opacity: 0.32; }
988
+ .deck-outline-item.is-off:hover .deck-outline-title,
989
+ .deck-outline-item.is-off:hover .deck-outline-thumb { opacity: 0.75; }
990
+ .deck-outline-n.is-off {
991
+ padding: 2px 4px;
992
+ border: 1px solid var(--line);
993
+ border-radius: 4px;
994
+ font: 600 8.5px/1 var(--sans);
995
+ letter-spacing: 0.1em;
996
+ text-transform: uppercase;
997
+ opacity: 1;
998
+ }
999
+
982
1000
  /* --- the outline's preview mode ------------------------------------------ */
983
1001
 
984
1002
  /* Same rows, but each one is the slide itself. Depth indents carry over from
@@ -1400,6 +1418,36 @@ body.edit-open #deck-edit { transform: none; }
1400
1418
  0 0 16px color-mix(in srgb, var(--accent) 55%, transparent);
1401
1419
  }
1402
1420
 
1421
+ /* A disabled slide in reveal's own overview, where the grid is the other way
1422
+ of picking one by hand. Dimmed, not hidden: it is still there to click. */
1423
+ /* `filter`, not `opacity`: reveal pins every slide in the overview to
1424
+ `opacity: 1 !important`, and this says the same thing without the shouting
1425
+ match. */
1426
+ .reveal.overview .slides section.slide-off { filter: grayscale(1) opacity(0.35); }
1427
+ .reveal.overview .slides section.slide-off:hover { filter: grayscale(0.4) opacity(0.8); }
1428
+
1429
+ /* Looking at one, the count and the bar stand down — there is no number to be
1430
+ at, this slide is not in the talk — and the word takes the number's place,
1431
+ which is the shortest way to say why it went. */
1432
+ .reveal:has(.slides section.slide-off.present) .slide-number,
1433
+ .reveal:has(.slides section.slide-off.present) .progress { visibility: hidden; }
1434
+ .reveal:has(.slides section.slide-off.present)::after {
1435
+ content: 'off';
1436
+ position: absolute;
1437
+ z-index: 4;
1438
+ left: calc(var(--r-controls-spacing, 12px) + 50px);
1439
+ bottom: 12px;
1440
+ transform: translateX(-50%);
1441
+ padding: 3px 6px;
1442
+ border: 1px solid var(--line);
1443
+ border-radius: 5px;
1444
+ font: 600 9px/1 var(--sans);
1445
+ letter-spacing: 0.12em;
1446
+ text-transform: uppercase;
1447
+ color: var(--muted);
1448
+ pointer-events: none;
1449
+ }
1450
+
1403
1451
  /* --- the pointer (pointer.js) -------------------------------------------- */
1404
1452
 
1405
1453
  /* The real cursor goes away everywhere while the pointer is armed — the
@@ -30,6 +30,17 @@ Not every demo wants the whole slide. `.frame` sizes one so the heading stays.
30
30
 
31
31
  ---
32
32
 
33
+ <!-- disabled -->
34
+
35
+ ## A slide kept out of the talk
36
+
37
+ `<!-- disabled -->` on its own line: still in the file, still in the navigator —
38
+ marked **off** there — but the arrows walk past it and it is not counted.
39
+
40
+ Click it in the navigator (`V`) to look at it anyway.
41
+
42
+ ---
43
+
33
44
  ## The mechanism, in one picture
34
45
 
35
46
  ```mermaid