fb-slides 0.6.2 → 0.7.0-rc.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 +134 -0
- package/README.md +39 -1
- package/bin/fb-slides.mjs +3 -1
- package/docs/source-code.md +133 -0
- package/lib/config.mjs +26 -0
- package/lib/dev.mjs +88 -1
- package/lib/render.mjs +6 -1
- package/package.json +2 -1
- package/runtime/deck.js +99 -4
- package/runtime/outline.js +24 -5
- package/runtime/snippets.js +6 -0
- package/runtime/theme.base.css +50 -0
- package/templates/starter/decks/02-demos.md +12 -0
- package/templates/starter/slides.config.js +11 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
## Unreleased
|
|
11
|
+
|
|
12
|
+
- **A `source ↗` button on demo slides.** `editor: true` in `slides.config.js`
|
|
13
|
+
starts `code serve-web` — the web server built into VS Code — alongside the
|
|
14
|
+
deck, and every demo slide gets a link that opens its folder in a real editor
|
|
15
|
+
in a new tab: file tree, search, terminal, extensions. Nothing is installed and
|
|
16
|
+
nothing is embedded in the slide. The folder is worked out from the marker for
|
|
17
|
+
a folder demo, and for one embedded by URL from the `servers:` entry serving
|
|
18
|
+
that address — which is the first thing `url:` has ever been used for.
|
|
19
|
+
`<!-- demo: … | src: demo/cart -->` says it outright when nothing else knows.
|
|
20
|
+
The editor's port — the deck's plus 100 — is checked before it starts, because
|
|
21
|
+
`serve-web` has no `--strictPort` and two decks are enough to collide. A built
|
|
22
|
+
deck never carries the button: the link is a localhost address and a path on
|
|
23
|
+
your disk. `--no-editor` turns it off for one run.
|
|
24
|
+
[docs/source-code.md](docs/source-code.md)
|
|
25
|
+
|
|
26
|
+
## 0.6.3 — 2026-09-05
|
|
27
|
+
|
|
28
|
+
- **Disabled slides.** `<!-- disabled -->` on a line of its own keeps a slide in
|
|
29
|
+
the file without keeping it in the talk: the arrows walk past it, it holds no
|
|
30
|
+
slide number, and the count and the progress bar are computed as if it were
|
|
31
|
+
not there. It is still listed in the navigator and in reveal's overview,
|
|
32
|
+
dimmed and marked *off* — clicking it there opens it, which is the point of
|
|
33
|
+
keeping it.
|
|
34
|
+
|
|
35
|
+
## 0.6.2 — 2026-09-05
|
|
36
|
+
|
|
37
|
+
- **Fixed:** the toolbar could close itself. One tool arming told the others to
|
|
38
|
+
stand down, and the bar read that handshake as "nothing is armed" while a tool
|
|
39
|
+
still was. The armed state is now recomputed from the buttons themselves.
|
|
40
|
+
|
|
41
|
+
## 0.6.1 — 2026-09-05
|
|
42
|
+
|
|
43
|
+
- **The toolbar moved to the top edge**, and sleeps just above it: a chevron says
|
|
44
|
+
it is there, and the bar comes down when the pointer goes for it. The slides
|
|
45
|
+
keep the bottom of the screen.
|
|
46
|
+
|
|
47
|
+
## 0.6.0 — 2026-09-05
|
|
48
|
+
|
|
49
|
+
- **A vocabulary of slide layouts** in the base theme, so a deck stops writing
|
|
50
|
+
`style=""`: `.cols`/`.col` (two columns, with the `min-width: 0` a code block
|
|
51
|
+
falls over without), `.frame` (an embed that shares the slide), `.caption`,
|
|
52
|
+
`.box`, and `.author-slide` — the speaker page, photos down the left edge and
|
|
53
|
+
the bio on the right. Each fixes its numbers in CSS variables, so a project
|
|
54
|
+
retunes one from its own `theme.css`. Images and clips are now centred and
|
|
55
|
+
lose the white card reveal's core CSS frames every image with.
|
|
56
|
+
- **The speaker page's photos cross-fade** for however many there are: the
|
|
57
|
+
keyframes are written at load from the two timing tokens, instead of a fixed
|
|
58
|
+
table that stopped at eight.
|
|
59
|
+
- **A block picker in the edit drawer.** The `+` over the slide box — or `/` on
|
|
60
|
+
an empty line — writes the markup nobody remembers: the demo marker, a framed
|
|
61
|
+
embed, a video with the attributes that make it behave, two columns, a fenced
|
|
62
|
+
block, a mermaid graph, the speaker page. What was selected takes the place of
|
|
63
|
+
the first placeholder, so picking *Fragment* wraps the paragraph you had
|
|
64
|
+
highlighted. `snippets:` in `slides.config.js` adds a project's own.
|
|
65
|
+
- **The drawer takes half the page** on the chevron where its title used to be,
|
|
66
|
+
and stays that way across the reload every save ends in.
|
|
67
|
+
- **Add page / Delete page** say what they act on — the slide, not the text in
|
|
68
|
+
the box.
|
|
69
|
+
- **The starter** comes with the speaker page already written, second, against
|
|
70
|
+
two placeholder photos, and its demos deck now shows a framed embed next to
|
|
71
|
+
the two full-bleed ones.
|
|
72
|
+
|
|
73
|
+
## 0.5.1 — 2026-09-05
|
|
74
|
+
|
|
75
|
+
- The slide number under the arrows recedes: no pill, thinner ink, smaller type.
|
|
76
|
+
|
|
77
|
+
## 0.5.0 — 2026-09-05
|
|
78
|
+
|
|
79
|
+
- **The edit drawer.** `T` opens the slide you are looking at down the right
|
|
80
|
+
edge — its markdown in one box, its speaker notes in the other. Typing
|
|
81
|
+
re-renders the real slide in place, and **Save** writes the text back into that
|
|
82
|
+
slide's own lines of the `.md`, leaving the rest of the file alone. **Add** and
|
|
83
|
+
**Delete** work on the file too. Dev server only: a built deck never shows it.
|
|
84
|
+
- Docs: how to make a demo a real application.
|
|
85
|
+
|
|
86
|
+
## 0.4.3 — 2026-09-05
|
|
87
|
+
|
|
88
|
+
- **Fixed:** the spotlight left a ghost hole behind once its veil had faded.
|
|
89
|
+
|
|
90
|
+
## 0.4.2 — 2026-09-05
|
|
91
|
+
|
|
92
|
+
- The four tools moved onto the **QWER row**, in toolbar order, and each chip
|
|
93
|
+
wears its own key along its bottom edge.
|
|
94
|
+
|
|
95
|
+
## 0.4.1 — 2026-09-03
|
|
96
|
+
|
|
97
|
+
- Packaging fixes.
|
|
98
|
+
|
|
99
|
+
## 0.4.0 — 2026-09-03
|
|
100
|
+
|
|
101
|
+
- **The pointer**: the cursor swapped for a glowing halo, a centre-seeking dart,
|
|
102
|
+
or itself.
|
|
103
|
+
- **The arrow**: click the tail, then the tip — the pen's colours, the pen's
|
|
104
|
+
fade, and a head large enough to read from the back row.
|
|
105
|
+
- The toolbar moved to the bottom centre, its popovers opening upwards.
|
|
106
|
+
|
|
107
|
+
## 0.3.0 — 2026-09-03
|
|
108
|
+
|
|
109
|
+
- **The pen** holds its ink for two seconds before it fades, and the number keys
|
|
110
|
+
pick its colour.
|
|
111
|
+
|
|
112
|
+
## 0.2.0 — 2026-09-03
|
|
113
|
+
|
|
114
|
+
- **`revealTheme:`** — wear one of reveal.js's own fifteen themes, served from
|
|
115
|
+
the deck rather than a CDN. The chrome follows it through the theme's own
|
|
116
|
+
variables.
|
|
117
|
+
|
|
118
|
+
## 0.1.2 — 2026-09-03
|
|
119
|
+
|
|
120
|
+
- A hairline scrollbar for the navigator.
|
|
121
|
+
- `npm run starter` runs the starter template in place.
|
|
122
|
+
|
|
123
|
+
## 0.1.1 — 2026-09-03
|
|
124
|
+
|
|
125
|
+
- **The vertical navigator** (`V`): the deck as a readable list down the left
|
|
126
|
+
edge, with a list/preview toggle and the talk's own structure as its depth.
|
|
127
|
+
- **The spotlight**, and a shortcuts registry behind the help overlay.
|
|
128
|
+
|
|
129
|
+
## 0.1.0 — 2026-09-03
|
|
130
|
+
|
|
131
|
+
First release. Markdown files in `decks/` become a reveal.js deck with no build
|
|
132
|
+
step: live demo embeds that run in the slide, mermaid diagrams, stepped code
|
|
133
|
+
highlighting, speaker notes, an annotation pen, a zero-config dev server, and a
|
|
134
|
+
`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
|
|
|
@@ -101,6 +101,14 @@ A demo can also be a whole application with its own dev server — Angular, or a
|
|
|
101
101
|
Vite — copied into `demo/` as it is. It is embedded by **URL**, not by folder name:
|
|
102
102
|
[docs/framework-demos.md](docs/framework-demos.md).
|
|
103
103
|
|
|
104
|
+
### The source button
|
|
105
|
+
|
|
106
|
+
`editor: true` puts a `source ↗` link on every demo slide, opening that demo's folder in
|
|
107
|
+
**real VS Code** in a new tab — `code serve-web`, the web server built into the editor you
|
|
108
|
+
already have, started alongside the deck. Nothing is installed and nothing is embedded, and
|
|
109
|
+
a built deck never carries it. Run `dev` once at your desk first: VS Code downloads its
|
|
110
|
+
server half on the very first use. [docs/source-code.md](docs/source-code.md).
|
|
111
|
+
|
|
104
112
|
### Stepped code highlighting
|
|
105
113
|
|
|
106
114
|
The line ranges live in the fence's info string — reveal reveals one group per click:
|
|
@@ -127,6 +135,30 @@ A `Note:` block at the end of a slide never shows on screen, only in the speaker
|
|
|
127
135
|
Note: a bad description is the most common reason a tool never gets used.
|
|
128
136
|
```
|
|
129
137
|
|
|
138
|
+
### Disabled slides
|
|
139
|
+
|
|
140
|
+
A slide whose source carries `<!-- disabled -->` on a line of its own stays in the
|
|
141
|
+
file and in the navigator, but steps out of the talk:
|
|
142
|
+
|
|
143
|
+
```markdown
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
<!-- disabled -->
|
|
147
|
+
|
|
148
|
+
## The version of this I cut on the train
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The arrows walk past it in whichever direction they were going, it holds no slide
|
|
154
|
+
number, and the count and the progress bar are computed as if it were not there.
|
|
155
|
+
It is still listed in the navigator (`V`) and in reveal's overview, marked **off** —
|
|
156
|
+
and clicking it there is how you look at it, which is the point of keeping it.
|
|
157
|
+
Looking at one, the corner reads `off` where the number would be.
|
|
158
|
+
|
|
159
|
+
Half a talk is the slides you decided against but are not ready to delete. This is
|
|
160
|
+
where they live, next to the ones that made it.
|
|
161
|
+
|
|
130
162
|
### Blocks
|
|
131
163
|
|
|
132
164
|
Six classes cover the layouts a talk keeps needing, so a slide asks for one by name
|
|
@@ -262,6 +294,8 @@ export default {
|
|
|
262
294
|
{ name: 'angular demo', cwd: 'demo/app', command: 'npm', args: ['start', '--', '--port', '4200'] },
|
|
263
295
|
],
|
|
264
296
|
|
|
297
|
+
editor: true, // a `source ↗` button on every demo slide
|
|
298
|
+
|
|
265
299
|
port: 4000,
|
|
266
300
|
open: true,
|
|
267
301
|
outDir: 'dist',
|
|
@@ -280,6 +314,10 @@ Copying a real Angular or Vite app into `demo/` and wiring it up — including t
|
|
|
280
314
|
Vite needs so a busy port fails instead of moving:
|
|
281
315
|
[docs/framework-demos.md](docs/framework-demos.md).
|
|
282
316
|
|
|
317
|
+
`editor: true` is a side process of the same kind, and fails the same way — a missing
|
|
318
|
+
`code` in PATH, a port already taken, and you get a warning and demo slides without their
|
|
319
|
+
`source ↗` button: [docs/source-code.md](docs/source-code.md).
|
|
320
|
+
|
|
283
321
|
## Theming
|
|
284
322
|
|
|
285
323
|
`theme.css` in the project is loaded **after** the package's base theme, so it overrides
|
package/bin/fb-slides.mjs
CHANGED
|
@@ -34,7 +34,8 @@ const HELP = `
|
|
|
34
34
|
${self.name} v${self.version}
|
|
35
35
|
|
|
36
36
|
fb-slides create <dir> scaffold a new deck
|
|
37
|
-
fb-slides dev serve the deck and open it [--port n] [--no-open]
|
|
37
|
+
fb-slides dev serve the deck and open it [--port n] [--no-open]
|
|
38
|
+
[--no-servers] [--no-editor]
|
|
38
39
|
fb-slides build build dist/ [--out dir] [--watch]
|
|
39
40
|
fb-slides preview build, then serve the result [--port n]
|
|
40
41
|
|
|
@@ -46,6 +47,7 @@ const overrides = () => ({
|
|
|
46
47
|
outDir: option('out'),
|
|
47
48
|
open: flag('no-open') ? false : undefined,
|
|
48
49
|
servers: flag('no-servers') ? false : undefined,
|
|
50
|
+
editor: flag('no-editor') ? false : undefined,
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
const run = async () => {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# The source button
|
|
2
|
+
|
|
3
|
+
A demo slide shows the app running. `source ↗`, next to `open in a tab ↗`, shows
|
|
4
|
+
what it is made of: the demo's folder, open in **VS Code** in a new tab.
|
|
5
|
+
|
|
6
|
+
Not an editor written for this package, and not a viewer that resembles one. It
|
|
7
|
+
is `code serve-web` — the web server built into the VS Code on the machine
|
|
8
|
+
running the talk — pointed at the demo's folder. The file tree, the search, the
|
|
9
|
+
terminal, the extensions, the keybindings: the editor, in a tab.
|
|
10
|
+
|
|
11
|
+
## Turning it on
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// slides.config.js
|
|
15
|
+
export default {
|
|
16
|
+
editor: true,
|
|
17
|
+
};
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is the whole configuration. `dev` starts it alongside the deck, on the
|
|
21
|
+
deck's port plus 100, and every demo slide grows the link.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
$ npm run dev
|
|
25
|
+
|
|
26
|
+
↑ code serve-web → http://localhost:4100/
|
|
27
|
+
|
|
28
|
+
My Talk
|
|
29
|
+
→ http://localhost:4000/
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Both defaults move if they have to:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
editor: { port: 5000, command: 'code-insiders' },
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`command` is a name in PATH. `code-insiders` and `cursor` answer `serve-web`
|
|
39
|
+
too; whatever you name has to be a VS Code CLI, because that subcommand is what
|
|
40
|
+
this uses.
|
|
41
|
+
|
|
42
|
+
## Run it once before you need it
|
|
43
|
+
|
|
44
|
+
**The first run downloads about 100 MB** — the server half of VS Code, into
|
|
45
|
+
`~/.vscode/cli`. It happens once per machine and it is cached from then on, but
|
|
46
|
+
a minute of silent downloading is not something to meet in front of a room.
|
|
47
|
+
|
|
48
|
+
Start `dev` once at your desk after turning the key on. That is the whole
|
|
49
|
+
precaution, and it is why `editor:` is off in a fresh deck rather than on.
|
|
50
|
+
|
|
51
|
+
If `code` is not in PATH at all, the deck says so and carries on without the
|
|
52
|
+
button:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
⚠ code is not in PATH — the demo slides get no source button.
|
|
56
|
+
In VS Code: Shell Command: Install 'code' command in PATH.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## When the port is taken
|
|
60
|
+
|
|
61
|
+
The default is the deck's port plus 100, and **two decks are enough to collide**:
|
|
62
|
+
one on 4000 puts its editor on 4100, which is a perfectly ordinary port for the
|
|
63
|
+
second deck to be on.
|
|
64
|
+
|
|
65
|
+
`code serve-web` does not fail on a busy port the way the deck server does — a
|
|
66
|
+
process holding the IPv6 side leaves the IPv4 side free, both bind, and
|
|
67
|
+
`localhost` in the browser then lands on whichever the resolver picked. That is
|
|
68
|
+
the failure you find out about on stage. So the port is checked before the
|
|
69
|
+
editor is started, and a taken one is a message rather than a wrong tab:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
⚠ port 4100 is in use — no source button.
|
|
73
|
+
Free it, or give the editor another one: `editor: { port: n }`.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Where a slide's code is found
|
|
77
|
+
|
|
78
|
+
The button has to be told which folder a demo comes from. Three of the four
|
|
79
|
+
answers need nothing from you:
|
|
80
|
+
|
|
81
|
+
| the slide says | the folder |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `<!-- demo: counter -->` | `demo/counter` — a name is a folder in the demos dir |
|
|
84
|
+
| `<!-- demo: ../shared/cart/ -->` | that path, as written |
|
|
85
|
+
| `<!-- demo: http://localhost:4200/ -->` | the `cwd` of whichever `servers:` entry has that `url` |
|
|
86
|
+
| `<!-- demo: https://example.com/ \| src: demo/cart -->` | `demo/cart` |
|
|
87
|
+
|
|
88
|
+
The third row is the one worth knowing about. A framework demo is embedded by
|
|
89
|
+
**URL** — the slide has no idea there is a folder anywhere. `servers:` does:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
{
|
|
93
|
+
name: 'checkout',
|
|
94
|
+
cwd: 'demo/checkout', // ← the sources
|
|
95
|
+
command: 'npm',
|
|
96
|
+
args: ['start', '--', '--port', '4200'],
|
|
97
|
+
url: 'http://localhost:4200/', // ← what the slide points at
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
That pairing used to be a log line and nothing else. It is now also how the
|
|
102
|
+
button finds the code behind a running app.
|
|
103
|
+
|
|
104
|
+
The fourth row, `| src:`, is the escape hatch for what is left: a demo deployed
|
|
105
|
+
somewhere, or embedded from a server the config does not declare. It wins over
|
|
106
|
+
everything else when it is there.
|
|
107
|
+
|
|
108
|
+
A demo whose folder is unknown simply has no button. Nothing breaks, and the
|
|
109
|
+
rest of the slide is unchanged.
|
|
110
|
+
|
|
111
|
+
## What it does not do
|
|
112
|
+
|
|
113
|
+
- **It is not in the built deck.** `build` never writes it out: the link is a
|
|
114
|
+
`localhost` address and an absolute path on your disk, and neither means
|
|
115
|
+
anything to someone reading the deck on the web. This is a presenting tool,
|
|
116
|
+
like the demos that need `servers:` running.
|
|
117
|
+
- **It does not open in the slide.** A new tab, deliberately — an editor is
|
|
118
|
+
heavy, and a deck that never mounts one has nothing to tear down when the
|
|
119
|
+
slide moves on.
|
|
120
|
+
- **`--no-editor` turns it off** for one run. It is deliberately not part of
|
|
121
|
+
`--no-servers`: that flag is for skipping a demo's own dev server, and a demo
|
|
122
|
+
that is not running is exactly when you want to open its code. The URL → folder
|
|
123
|
+
table above is read from `servers:` as written in the config, so it still
|
|
124
|
+
works when none of those servers were started.
|
|
125
|
+
|
|
126
|
+
## It is your real filesystem
|
|
127
|
+
|
|
128
|
+
The editor opens the actual folder, with write access, as an editor does. Saving
|
|
129
|
+
a file in that tab changes the file on disk.
|
|
130
|
+
|
|
131
|
+
That is the feature, if you mean to edit live: change a component in the tab,
|
|
132
|
+
switch back to the slide, watch the demo's own dev server pick it up. It is
|
|
133
|
+
worth knowing about all the same, because the same is true of a stray keystroke.
|
package/lib/config.mjs
CHANGED
|
@@ -21,6 +21,19 @@ const AUTO_STATIC = ['assets', 'demo', 'public', 'images', 'img'];
|
|
|
21
21
|
// Never served, never published — heavy, private, or generated.
|
|
22
22
|
export const ALWAYS_EXCLUDED = ['node_modules', '.git', '.angular', '.next', '.cache', '.DS_Store'];
|
|
23
23
|
|
|
24
|
+
// `editor: true` turns the source button on with the defaults; `{ port, command }`
|
|
25
|
+
// changes them; leaving the key out is no button at all. The port follows the
|
|
26
|
+
// deck's so two talks open at once do not fight over it, and the command is a
|
|
27
|
+
// name in PATH — `code-insiders` and `cursor` answer `serve-web` too.
|
|
28
|
+
const editorSpec = (value, deckPort) => {
|
|
29
|
+
if (!value) return null;
|
|
30
|
+
const user = value === true ? {} : value;
|
|
31
|
+
return {
|
|
32
|
+
command: user.command ?? 'code',
|
|
33
|
+
port: Number(user.port ?? deckPort + 100),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
24
37
|
export const findConfigFile = (root) => CONFIG_FILES.map((name) => join(root, name)).find(existsSync) ?? null;
|
|
25
38
|
|
|
26
39
|
const importConfig = async (file) => {
|
|
@@ -93,6 +106,19 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
93
106
|
// silently wears the wrong clothes.
|
|
94
107
|
if (config.revealTheme) await assertRevealTheme(config.revealTheme);
|
|
95
108
|
|
|
109
|
+
// `--no-editor` is its own flag rather than a part of `--no-servers`: that one
|
|
110
|
+
// is for skipping a demo's dev server, and a demo that is not running is
|
|
111
|
+
// exactly when you want to be able to open its code.
|
|
112
|
+
config.editor = overrides.editor === false ? null : editorSpec(user.editor, config.port);
|
|
113
|
+
// The folder behind each demo embedded by URL, taken from `servers:` as the
|
|
114
|
+
// project wrote it rather than from the list `dev` will start: `--no-servers`
|
|
115
|
+
// empties that one, and a demo that is not running is exactly when its code is
|
|
116
|
+
// worth opening. `url:` had never been used for anything until this.
|
|
117
|
+
config.demoSources = Object.fromEntries(
|
|
118
|
+
(user.servers ?? [])
|
|
119
|
+
.filter((spec) => spec.url && spec.cwd)
|
|
120
|
+
.map((spec) => [String(spec.url).replace(/\/$/, ''), spec.cwd]),
|
|
121
|
+
);
|
|
96
122
|
config.decksPath = resolve(root, config.decksDir);
|
|
97
123
|
config.outPath = resolve(root, config.outDir);
|
|
98
124
|
// What the browser asks for, as opposed to where it is on disk.
|
package/lib/dev.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
+
import { connect } from 'node:net';
|
|
10
11
|
import { existsSync } from 'node:fs';
|
|
11
12
|
import { join, resolve } from 'node:path';
|
|
12
13
|
|
|
@@ -54,6 +55,84 @@ const startSideServer = (spec, root, children) => {
|
|
|
54
55
|
});
|
|
55
56
|
};
|
|
56
57
|
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// The source-code editor. `code serve-web` is VS Code's own web server, shipped
|
|
60
|
+
// inside the editor the presenter already has: no dependency is added here, and
|
|
61
|
+
// what opens in the tab is the real thing rather than a viewer pretending to be
|
|
62
|
+
// one. It is a side process like any other — it dies with the deck, and its
|
|
63
|
+
// failure is never the deck's failure, only a slide without its button.
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
// `serve-web` has no --strictPort, and it does not fail on a taken port the way
|
|
67
|
+
// the deck server does: another process holding the IPv6 side leaves the IPv4
|
|
68
|
+
// side free, both bind, and `localhost` in the browser then lands on whichever
|
|
69
|
+
// the resolver picked. Two fb-slides decks are enough to arrange that — one on
|
|
70
|
+
// 4000 puts its editor on 4100, which is a perfectly ordinary port for the
|
|
71
|
+
// second deck. So ask first, and say which port to use instead.
|
|
72
|
+
const portInUse = (port) =>
|
|
73
|
+
Promise.all(
|
|
74
|
+
['127.0.0.1', '::1'].map(
|
|
75
|
+
(host) =>
|
|
76
|
+
new Promise((done) => {
|
|
77
|
+
const probe = connect({ host, port });
|
|
78
|
+
const settle = (taken) => {
|
|
79
|
+
probe.destroy();
|
|
80
|
+
done(taken);
|
|
81
|
+
};
|
|
82
|
+
probe.setTimeout(400, () => settle(false));
|
|
83
|
+
probe.on('connect', () => settle(true));
|
|
84
|
+
probe.on('error', () => settle(false));
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
).then((answers) => answers.some(Boolean));
|
|
88
|
+
|
|
89
|
+
const hasCommand = (command) =>
|
|
90
|
+
new Promise((done) => {
|
|
91
|
+
const probe = spawn(command, ['--version'], { stdio: 'ignore' });
|
|
92
|
+
probe.on('error', () => done(false));
|
|
93
|
+
probe.on('exit', (code) => done(code === 0));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const startEditor = async (spec, children) => {
|
|
97
|
+
if (!(await hasCommand(spec.command))) {
|
|
98
|
+
console.warn(` ⚠ ${spec.command} is not in PATH — the demo slides get no source button.`);
|
|
99
|
+
console.warn(` In VS Code: Shell Command: Install 'code' command in PATH.`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (await portInUse(spec.port)) {
|
|
104
|
+
console.warn(` ⚠ port ${spec.port} is in use — no source button.`);
|
|
105
|
+
console.warn(` Free it, or give the editor another one: \`editor: { port: n }\`.`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const url = `http://localhost:${spec.port}/`;
|
|
110
|
+
// First run downloads the server half of VS Code into ~/.vscode/cli, which is
|
|
111
|
+
// a hundred megabytes and a minute — hence `stdio: 'inherit'`, so it happens
|
|
112
|
+
// in front of whoever started the deck rather than silently before a talk.
|
|
113
|
+
console.log(` ↑ ${spec.command} serve-web → ${url}`);
|
|
114
|
+
const child = spawn(
|
|
115
|
+
spec.command,
|
|
116
|
+
[
|
|
117
|
+
'serve-web',
|
|
118
|
+
'--port',
|
|
119
|
+
String(spec.port),
|
|
120
|
+
// No token in the URL: the slide has to be able to link straight to a
|
|
121
|
+
// folder, and this listens on localhost only.
|
|
122
|
+
'--without-connection-token',
|
|
123
|
+
'--accept-server-license-terms',
|
|
124
|
+
'--disable-telemetry',
|
|
125
|
+
],
|
|
126
|
+
{ stdio: 'inherit' },
|
|
127
|
+
);
|
|
128
|
+
children.push(child);
|
|
129
|
+
child.on('error', (error) => console.warn(` ⚠ ${spec.command}: ${error.message}`));
|
|
130
|
+
child.on('exit', (code) => {
|
|
131
|
+
if (code) console.warn(`\n ⚠ ${spec.command} serve-web stopped (exit ${code}) — no source button.\n`);
|
|
132
|
+
});
|
|
133
|
+
return url;
|
|
134
|
+
};
|
|
135
|
+
|
|
57
136
|
// What the page is built from, read again on every request. A config with a
|
|
58
137
|
// syntax error in it — the state it is in halfway through an edit — leaves the
|
|
59
138
|
// last good one standing rather than serving a broken deck.
|
|
@@ -73,6 +152,7 @@ const reader = (config, reload) => {
|
|
|
73
152
|
export const dev = async (config, runtimeDir, reload) => {
|
|
74
153
|
assertUsable(config);
|
|
75
154
|
const current = reader(config, reload);
|
|
155
|
+
let editorUrl = null;
|
|
76
156
|
|
|
77
157
|
const mounts = [
|
|
78
158
|
// The project first: a file next to the decks shadows the one this package
|
|
@@ -92,7 +172,13 @@ export const dev = async (config, runtimeDir, reload) => {
|
|
|
92
172
|
// the theme or the signature and reloading is enough. What was fixed when
|
|
93
173
|
// the server came up — the mounts, the port, the side processes — still
|
|
94
174
|
// needs a restart.
|
|
95
|
-
index: async () =>
|
|
175
|
+
index: async () => {
|
|
176
|
+
const now = await current();
|
|
177
|
+
// Read at request time, not at wiring time: the editor is started below,
|
|
178
|
+
// after the port is bound, and the first page load comes later still.
|
|
179
|
+
const editor = editorUrl ? { url: editorUrl, root: config.root, sources: now.demoSources } : null;
|
|
180
|
+
return renderIndex(now, runtimeDir, editor ? { editor } : {});
|
|
181
|
+
},
|
|
96
182
|
// Editing a slide in the browser saves it back into its .md — dev only,
|
|
97
183
|
// which is what makes the edit button appear at all (the runtime probes
|
|
98
184
|
// /api/editable). Build and preview never mount this.
|
|
@@ -111,6 +197,7 @@ export const dev = async (config, runtimeDir, reload) => {
|
|
|
111
197
|
const url = `http://localhost:${config.port}/`;
|
|
112
198
|
const children = [];
|
|
113
199
|
for (const spec of config.servers) startSideServer(spec, config.root, children);
|
|
200
|
+
if (config.editor) editorUrl = await startEditor(config.editor, children);
|
|
114
201
|
|
|
115
202
|
console.log(`\n ${config.title}\n → ${url}\n`);
|
|
116
203
|
if (config.open) setTimeout(() => openBrowser(url), 400);
|
package/lib/render.mjs
CHANGED
|
@@ -38,7 +38,11 @@ const headHtml = (config) => {
|
|
|
38
38
|
return tags.join('\n ');
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// `extra` is what only one command knows. `dev` puts the source-code editor
|
|
42
|
+
// there — a localhost URL and absolute paths, both of which are true for the
|
|
43
|
+
// machine running the talk and nowhere else, so `build` never passes it and a
|
|
44
|
+
// published deck has no button to press.
|
|
45
|
+
export const renderIndex = async (config, runtimeDir, extra = {}) => {
|
|
42
46
|
const template = await readFile(join(runtimeDir, 'index.html'), 'utf8');
|
|
43
47
|
|
|
44
48
|
const runtime = {
|
|
@@ -47,6 +51,7 @@ export const renderIndex = async (config, runtimeDir) => {
|
|
|
47
51
|
reveal: config.reveal,
|
|
48
52
|
fragmentLists: config.fragmentLists,
|
|
49
53
|
snippets: config.snippets,
|
|
54
|
+
...extra,
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
// 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.7.0-rc.0",
|
|
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
|
@@ -58,9 +58,43 @@ if (!DECKS.length) console.error(`[deck] no .md found in ${DECKS_DIR}`);
|
|
|
58
58
|
// running in an iframe. The marker lives in the Markdown, so the position of a
|
|
59
59
|
// demo is decided there — like any other slide. The value is a folder inside the
|
|
60
60
|
// project's demos dir, a ./ or ../ path, or a full http(s) URL for anything external.
|
|
61
|
-
|
|
61
|
+
//
|
|
62
|
+
// `| src: demo/cart` after it names the folder that code comes from, for the
|
|
63
|
+
// source button. It is only ever needed for a URL whose server the config does
|
|
64
|
+
// not declare — every other form already says where its own sources are.
|
|
65
|
+
const DEMO_MARKER = /^<!--\s*demo:\s*(\S+?)(?:\s*\|\s*src:\s*(\S+?))?\s*-->/;
|
|
62
66
|
const DEMOS_DIR = CFG.demos ?? 'demo/';
|
|
63
67
|
|
|
68
|
+
// `dev` fills this in when `editor:` is on: the address VS Code's own web server
|
|
69
|
+
// is answering at, the project's absolute path, and the folder behind each demo
|
|
70
|
+
// URL. A built deck never carries it — both halves are true of one machine only —
|
|
71
|
+
// so the button exists while presenting and nowhere else.
|
|
72
|
+
const EDITOR = CFG.editor ?? null;
|
|
73
|
+
|
|
74
|
+
// Where a demo's code sits, relative to the project root. A bare name is a
|
|
75
|
+
// folder in the demos dir, a ./ or ../ path is itself, and a URL is whichever
|
|
76
|
+
// `servers:` entry serves it — unless the slide said outright with `| src:`.
|
|
77
|
+
const sourceOf = (target, src, isUrl) => {
|
|
78
|
+
if (src) return src;
|
|
79
|
+
if (isUrl) return EDITOR?.sources?.[target.replace(/\/$/, '')] ?? null;
|
|
80
|
+
return /^[./]/.test(target) ? target : `${DEMOS_DIR}${target}`;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// VS Code for the Web opens whatever `?folder=` names, and it wants a path on
|
|
84
|
+
// disk — which the browser has no way of knowing, so the server sent the root.
|
|
85
|
+
const editorHref = (relative) => {
|
|
86
|
+
if (!EDITOR?.url || !relative) return null;
|
|
87
|
+
const folder = `${EDITOR.root}/${relative.replace(/^\.\//, '').replace(/\/+$/, '')}`;
|
|
88
|
+
return `${EDITOR.url}?folder=${encodeURIComponent(folder)}`;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// A slide whose source carries `<!-- disabled -->` stays in the file and in the
|
|
92
|
+
// navigator, but steps out of the talk: the arrows walk past it and it is not
|
|
93
|
+
// counted. The line is stripped before rendering — what is left is an ordinary
|
|
94
|
+
// slide, demo marker included, for the day it is turned back on.
|
|
95
|
+
const DISABLED_MARKER = /^[ \t]*<!--[ \t]*disabled[ \t]*-->[ \t]*\r?\n?/im;
|
|
96
|
+
const OFF = 'slide-off';
|
|
97
|
+
|
|
64
98
|
// Bullet lists reveal themselves one item at a time. `?nofrag` turns it off for
|
|
65
99
|
// one visit; `fragmentLists: false` in the config turns it off for the project.
|
|
66
100
|
const AUTO_FRAGMENT = CFG.fragmentLists !== false && !new URLSearchParams(location.search).has('nofrag');
|
|
@@ -100,7 +134,7 @@ const markdownSection = (source, deck) => {
|
|
|
100
134
|
};
|
|
101
135
|
|
|
102
136
|
// The real page, running, inside the slide.
|
|
103
|
-
const demoSection = (target, deck) => {
|
|
137
|
+
const demoSection = (target, src, deck) => {
|
|
104
138
|
const isUrl = /^https?:\/\//.test(target);
|
|
105
139
|
const name = isUrl ? target : target.replace(/\/$/, '');
|
|
106
140
|
// Bare names are folders in the demos dir; a ./ or ../ path and a full URL are
|
|
@@ -108,6 +142,9 @@ const demoSection = (target, deck) => {
|
|
|
108
142
|
const path = isUrl || /^[./]/.test(name) ? name : `${DEMOS_DIR}${name}/`;
|
|
109
143
|
// A whole URL in the header would drown the slide: show just the host.
|
|
110
144
|
const label = isUrl ? new URL(name).host : name;
|
|
145
|
+
// A new tab on purpose: the editor is heavy, and a deck that never mounts it
|
|
146
|
+
// has nothing of it to tear down when the slide moves on.
|
|
147
|
+
const source = editorHref(sourceOf(name, src, isUrl));
|
|
111
148
|
const section = document.createElement('section');
|
|
112
149
|
section.className = 'demo-slide';
|
|
113
150
|
section.dataset.deck = deck.label;
|
|
@@ -115,6 +152,7 @@ const demoSection = (target, deck) => {
|
|
|
115
152
|
<header>
|
|
116
153
|
<span class="tag">live</span>
|
|
117
154
|
<code>${label}</code>
|
|
155
|
+
${source ? `<a href="${source}" target="_blank" rel="noreferrer">source ↗</a>` : ''}
|
|
118
156
|
<a href="${path}" target="_blank" rel="noreferrer">open in a tab ↗</a>
|
|
119
157
|
</header>
|
|
120
158
|
<iframe data-src="${path}" title="${name}" loading="lazy"></iframe>
|
|
@@ -135,9 +173,18 @@ for (const file of DECKS) {
|
|
|
135
173
|
// `section:` names the deck; without one the file name has to do.
|
|
136
174
|
const deck = { file, label: front.section ?? file.replace(/^\d+-|\.md$/g, '') };
|
|
137
175
|
|
|
138
|
-
for (const [index,
|
|
176
|
+
for (const [index, raw] of splitSlides(stripFrontMatter(markdown)).entries()) {
|
|
177
|
+
const disabled = DISABLED_MARKER.test(raw);
|
|
178
|
+
const source = disabled ? raw.replace(DISABLED_MARKER, '') : raw;
|
|
139
179
|
const marker = source.match(DEMO_MARKER);
|
|
140
|
-
const section = marker ? demoSection(marker[1], deck) : markdownSection(source, deck);
|
|
180
|
+
const section = marker ? demoSection(marker[1], marker[2], deck) : markdownSection(source, deck);
|
|
181
|
+
if (disabled) {
|
|
182
|
+
section.classList.add(OFF);
|
|
183
|
+
// Reveal's own answer to "in the deck, out of the count": it leaves an
|
|
184
|
+
// uncounted slide out of `getSlides()`, the slide number and the progress
|
|
185
|
+
// bar, while keeping it reachable by index.
|
|
186
|
+
section.dataset.visibility = 'uncounted';
|
|
187
|
+
}
|
|
141
188
|
// Where the slide came from — the file and its index within it, counted by
|
|
142
189
|
// the same split as above. The edit drawer saves back through these; the
|
|
143
190
|
// markdown plugin forwards data-* attributes when it rewrites the section.
|
|
@@ -197,6 +244,54 @@ const nudge = (slide) => {
|
|
|
197
244
|
Reveal.on('ready', ({ currentSlide }) => nudge(currentSlide));
|
|
198
245
|
Reveal.on('slidechanged', ({ currentSlide }) => nudge(currentSlide));
|
|
199
246
|
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// Walking past the disabled slides. Reveal already leaves them out of the
|
|
249
|
+
// count; the talk itself steps over them here, in whichever direction it was
|
|
250
|
+
// already going. Two ways in are deliberate and let through: picking a slide by
|
|
251
|
+
// hand — the navigator, reveal's overview — and landing on one at load, which
|
|
252
|
+
// is what the edit drawer does on every save.
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
const sections = () => [...slidesEl.querySelectorAll(':scope > section')];
|
|
256
|
+
|
|
257
|
+
let jumping = false;
|
|
258
|
+
// Cleared on the next turn of the loop: `slidechanged` fires inside the call
|
|
259
|
+
// that follows, so a click that lands on the slide already showing — no event
|
|
260
|
+
// at all — cannot leave the flag standing for the next press of an arrow.
|
|
261
|
+
const jump = () => {
|
|
262
|
+
jumping = true;
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
jumping = false;
|
|
265
|
+
}, 0);
|
|
266
|
+
};
|
|
267
|
+
document.addEventListener('deck:jump', jump);
|
|
268
|
+
slidesEl.addEventListener('click', () => Reveal.isOverview() && jump(), true);
|
|
269
|
+
|
|
270
|
+
let cameFrom = Reveal.getIndices().h;
|
|
271
|
+
|
|
272
|
+
Reveal.on('slidechanged', ({ indexh, currentSlide }) => {
|
|
273
|
+
const deliberate = jumping;
|
|
274
|
+
jumping = false;
|
|
275
|
+
if (deliberate || !currentSlide?.classList.contains(OFF)) {
|
|
276
|
+
cameFrom = indexh;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const slides = sections();
|
|
281
|
+
const step = indexh < cameFrom ? -1 : 1;
|
|
282
|
+
const seek = (start, direction) => {
|
|
283
|
+
let i = start;
|
|
284
|
+
while (slides[i]?.classList.contains(OFF)) i += direction;
|
|
285
|
+
return slides[i] ? i : null;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Off the end — the deck ends on disabled slides — turn round rather than
|
|
289
|
+
// strand the talk on one it is not meant to show.
|
|
290
|
+
const target = seek(indexh + step, step) ?? seek(indexh - step, -step);
|
|
291
|
+
if (target === null) cameFrom = indexh; // every slide is disabled: stay put
|
|
292
|
+
else Reveal.slide(target);
|
|
293
|
+
});
|
|
294
|
+
|
|
200
295
|
// ---------------------------------------------------------------------------
|
|
201
296
|
// Post-processing — everything below runs on markdown the plugin has rendered.
|
|
202
297
|
// ---------------------------------------------------------------------------
|
package/runtime/outline.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
109
|
-
|
|
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 =
|
|
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
|
|
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`)
|
package/runtime/snippets.js
CHANGED
|
@@ -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',
|
package/runtime/theme.base.css
CHANGED
|
@@ -335,6 +335,8 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
335
335
|
white-space: nowrap;
|
|
336
336
|
}
|
|
337
337
|
.reveal .slides section.demo-slide header a { margin-left: auto; font-size: 0.9em; }
|
|
338
|
+
/* The first link is the one that pushes right; the rest ride along on the gap. */
|
|
339
|
+
.reveal .slides section.demo-slide header a ~ a { margin-left: 0; }
|
|
338
340
|
.reveal .slides section.demo-slide .tag {
|
|
339
341
|
font: 600 0.8em/1 var(--sans);
|
|
340
342
|
letter-spacing: 0.08em;
|
|
@@ -979,6 +981,24 @@ body.outline-open #deck-outline { transform: none; }
|
|
|
979
981
|
opacity: 0.55;
|
|
980
982
|
}
|
|
981
983
|
|
|
984
|
+
/* A disabled slide — `<!-- disabled -->` in its source. It is listed, because
|
|
985
|
+
the navigator is how it is reached at all, but it reads as set aside: the
|
|
986
|
+
talk walks past it and it holds no number. Still a button, still hovers,
|
|
987
|
+
still opens — clicking one is how you look at it. */
|
|
988
|
+
.deck-outline-item.is-off .deck-outline-title { opacity: 0.4; }
|
|
989
|
+
.deck-outline-item.is-off .deck-outline-thumb { opacity: 0.32; }
|
|
990
|
+
.deck-outline-item.is-off:hover .deck-outline-title,
|
|
991
|
+
.deck-outline-item.is-off:hover .deck-outline-thumb { opacity: 0.75; }
|
|
992
|
+
.deck-outline-n.is-off {
|
|
993
|
+
padding: 2px 4px;
|
|
994
|
+
border: 1px solid var(--line);
|
|
995
|
+
border-radius: 4px;
|
|
996
|
+
font: 600 8.5px/1 var(--sans);
|
|
997
|
+
letter-spacing: 0.1em;
|
|
998
|
+
text-transform: uppercase;
|
|
999
|
+
opacity: 1;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
982
1002
|
/* --- the outline's preview mode ------------------------------------------ */
|
|
983
1003
|
|
|
984
1004
|
/* Same rows, but each one is the slide itself. Depth indents carry over from
|
|
@@ -1400,6 +1420,36 @@ body.edit-open #deck-edit { transform: none; }
|
|
|
1400
1420
|
0 0 16px color-mix(in srgb, var(--accent) 55%, transparent);
|
|
1401
1421
|
}
|
|
1402
1422
|
|
|
1423
|
+
/* A disabled slide in reveal's own overview, where the grid is the other way
|
|
1424
|
+
of picking one by hand. Dimmed, not hidden: it is still there to click. */
|
|
1425
|
+
/* `filter`, not `opacity`: reveal pins every slide in the overview to
|
|
1426
|
+
`opacity: 1 !important`, and this says the same thing without the shouting
|
|
1427
|
+
match. */
|
|
1428
|
+
.reveal.overview .slides section.slide-off { filter: grayscale(1) opacity(0.35); }
|
|
1429
|
+
.reveal.overview .slides section.slide-off:hover { filter: grayscale(0.4) opacity(0.8); }
|
|
1430
|
+
|
|
1431
|
+
/* Looking at one, the count and the bar stand down — there is no number to be
|
|
1432
|
+
at, this slide is not in the talk — and the word takes the number's place,
|
|
1433
|
+
which is the shortest way to say why it went. */
|
|
1434
|
+
.reveal:has(.slides section.slide-off.present) .slide-number,
|
|
1435
|
+
.reveal:has(.slides section.slide-off.present) .progress { visibility: hidden; }
|
|
1436
|
+
.reveal:has(.slides section.slide-off.present)::after {
|
|
1437
|
+
content: 'off';
|
|
1438
|
+
position: absolute;
|
|
1439
|
+
z-index: 4;
|
|
1440
|
+
left: calc(var(--r-controls-spacing, 12px) + 50px);
|
|
1441
|
+
bottom: 12px;
|
|
1442
|
+
transform: translateX(-50%);
|
|
1443
|
+
padding: 3px 6px;
|
|
1444
|
+
border: 1px solid var(--line);
|
|
1445
|
+
border-radius: 5px;
|
|
1446
|
+
font: 600 9px/1 var(--sans);
|
|
1447
|
+
letter-spacing: 0.12em;
|
|
1448
|
+
text-transform: uppercase;
|
|
1449
|
+
color: var(--muted);
|
|
1450
|
+
pointer-events: none;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1403
1453
|
/* --- the pointer (pointer.js) -------------------------------------------- */
|
|
1404
1454
|
|
|
1405
1455
|
/* 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
|
|
@@ -67,5 +78,6 @@ Note what never happens: the model never touches the DOM, the network, or your s
|
|
|
67
78
|
- what follows it is the fallback for GitHub and Marp
|
|
68
79
|
- `demo/angular-hello` is started by `servers:` in `slides.config.js`
|
|
69
80
|
- the `+` in the edit drawer writes all three, and `/` opens the same list
|
|
81
|
+
- `editor: true` in the config adds `source ↗`: the demo's folder in real VS Code
|
|
70
82
|
|
|
71
83
|
Note: the iframe is reloaded every time you arrive on the slide, so a demo is always in its initial state.
|
|
@@ -38,6 +38,17 @@ export default {
|
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
|
|
41
|
+
// A `source ↗` button on every demo slide, opening that demo's folder in real
|
|
42
|
+
// VS Code in a new tab. It is `code serve-web` — the web server built into the
|
|
43
|
+
// editor you already have — started alongside the deck, so nothing is installed
|
|
44
|
+
// and nothing is embedded. Off by default because the very first run downloads
|
|
45
|
+
// VS Code's server half (~100 MB) and you do not want to discover that on
|
|
46
|
+
// stage: uncomment, run `dev` once at your desk, and it is cached from then on.
|
|
47
|
+
// `{ port: 4100, command: 'code' }` to change either — the default port is the
|
|
48
|
+
// deck's plus 100. `--no-editor` turns it off for one run, and a
|
|
49
|
+
// built deck never has it: the link is a localhost address and a path on disk.
|
|
50
|
+
editor: true,
|
|
51
|
+
|
|
41
52
|
// The corner signature. Uncomment to show it.
|
|
42
53
|
// signature: { name: 'fabiobiondi.dev', url: 'https://www.fabiobiondi.dev', logo: 'assets/logo.png' },
|
|
43
54
|
|