fb-slides 0.1.2
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/LICENSE +21 -0
- package/README.md +225 -0
- package/bin/fb-slides.mjs +111 -0
- package/lib/build.mjs +98 -0
- package/lib/config.mjs +90 -0
- package/lib/create.mjs +59 -0
- package/lib/decks.mjs +13 -0
- package/lib/dev.mjs +94 -0
- package/lib/render.mjs +52 -0
- package/lib/server.mjs +168 -0
- package/lib/vendor.mjs +32 -0
- package/package.json +51 -0
- package/runtime/annotate.js +458 -0
- package/runtime/deck.js +299 -0
- package/runtime/index.html +37 -0
- package/runtime/outline.js +354 -0
- package/runtime/shortcuts.js +53 -0
- package/runtime/spotlight.js +293 -0
- package/runtime/theme.base.css +879 -0
- package/templates/starter/README.md +27 -0
- package/templates/starter/_gitignore +4 -0
- package/templates/starter/_package.json +14 -0
- package/templates/starter/assets/.gitkeep +0 -0
- package/templates/starter/decks/01-intro.md +44 -0
- package/templates/starter/decks/02-demos.md +34 -0
- package/templates/starter/demo/angular-hello/README.md +15 -0
- package/templates/starter/demo/angular-hello/_package.json +24 -0
- package/templates/starter/demo/angular-hello/angular.json +34 -0
- package/templates/starter/demo/angular-hello/src/index.html +12 -0
- package/templates/starter/demo/angular-hello/src/main.ts +18 -0
- package/templates/starter/demo/angular-hello/src/styles.css +22 -0
- package/templates/starter/demo/angular-hello/tsconfig.app.json +5 -0
- package/templates/starter/demo/angular-hello/tsconfig.json +17 -0
- package/templates/starter/demo/counter/index.html +34 -0
- package/templates/starter/slides.config.js +34 -0
- package/templates/starter/theme.css +14 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// RevealSpotlight — light up one part of the slide, put the rest behind glass.
|
|
3
|
+
//
|
|
4
|
+
// A reveal.js plugin, built like annotate.js: `init()` gets the deck, the file
|
|
5
|
+
// owns its chrome and key bindings, and index.html stays a page of slides.
|
|
6
|
+
//
|
|
7
|
+
// The whole effect is one full-viewport div with a backdrop-filter, wearing a
|
|
8
|
+
// `clip-path: path(evenodd, …)` whose outer subpath is the viewport and whose
|
|
9
|
+
// inner one is the spotlit rectangle. Even-odd punches the rectangle out as a
|
|
10
|
+
// hole, so the blur and the dimming apply everywhere except inside it — one
|
|
11
|
+
// element, no seams, and the browser does the compositing. The rectangle lives
|
|
12
|
+
// in screen pixels like the pen's ink, clear of the CSS transform reveal
|
|
13
|
+
// scales slides with.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
import { listShortcutAfter } from './shortcuts.js';
|
|
17
|
+
|
|
18
|
+
const RADIUS = 10; // px, the hole's corner rounding
|
|
19
|
+
const MIN_DRAG = 6; // px in either axis before a press counts as a drag
|
|
20
|
+
const EDGE = 10; // px inside the rectangle's edge that resizes rather than moves
|
|
21
|
+
const MIN_SIZE = 24; // px, the smallest a resize can squeeze the light to
|
|
22
|
+
|
|
23
|
+
// The resize cursors, by the zone's name. The middle ('') is not listed: there
|
|
24
|
+
// the inline style is cleared and the stylesheet's grab/grabbing pair applies.
|
|
25
|
+
const CURSORS = {
|
|
26
|
+
n: 'ns-resize',
|
|
27
|
+
s: 'ns-resize',
|
|
28
|
+
e: 'ew-resize',
|
|
29
|
+
w: 'ew-resize',
|
|
30
|
+
nw: 'nwse-resize',
|
|
31
|
+
se: 'nwse-resize',
|
|
32
|
+
ne: 'nesw-resize',
|
|
33
|
+
sw: 'nesw-resize',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const SPOT_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
37
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
38
|
+
<path d="M4 8V6a2 2 0 0 1 2-2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" />
|
|
39
|
+
<path d="M20 16v2a2 2 0 0 1-2 2h-2" /><path d="M8 20H6a2 2 0 0 1-2-2v-2" />
|
|
40
|
+
<rect x="9" y="9.5" width="6" height="5" rx="1" />
|
|
41
|
+
</svg>`;
|
|
42
|
+
|
|
43
|
+
// The evenodd path: the viewport, then the hole as a rounded rectangle. The
|
|
44
|
+
// radius is clamped so a sliver of a rectangle still closes into a valid path.
|
|
45
|
+
const holePath = (rect, width, height) => {
|
|
46
|
+
const { x, y, w, h } = rect;
|
|
47
|
+
const r = Math.min(RADIUS, w / 2, h / 2);
|
|
48
|
+
return `path(evenodd, "M0 0H${width}V${height}H0Z \
|
|
49
|
+
M${x + r} ${y}h${w - 2 * r}a${r} ${r} 0 0 1 ${r} ${r}v${h - 2 * r}\
|
|
50
|
+
a${r} ${r} 0 0 1 -${r} ${r}h${2 * r - w}a${r} ${r} 0 0 1 -${r} -${r}\
|
|
51
|
+
v${2 * r - h}a${r} ${r} 0 0 1 ${r} -${r}Z")`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const RevealSpotlight = () => ({
|
|
55
|
+
id: 'spotlight',
|
|
56
|
+
|
|
57
|
+
init(deck) {
|
|
58
|
+
// ---- chrome ---------------------------------------------------------
|
|
59
|
+
// Fixed overlays on <body>, outside `.reveal` and its transforms. The veil
|
|
60
|
+
// sits under the ink canvas, so the pen can still draw inside the light.
|
|
61
|
+
const veil = document.createElement('div');
|
|
62
|
+
veil.id = 'deck-spot';
|
|
63
|
+
veil.setAttribute('aria-hidden', 'true');
|
|
64
|
+
|
|
65
|
+
// clip-path clips the veil's own border too, so the hole's edge is a
|
|
66
|
+
// second element laid over it — a thin ring that traces the rectangle.
|
|
67
|
+
// It earns its keep twice over: clipping cuts the hole out of the veil's
|
|
68
|
+
// hit-testing along with its paint, so pointer events inside the light
|
|
69
|
+
// would fall through to the slide — the ring, sized to the rectangle, is
|
|
70
|
+
// what catches them, and dragging it moves the spotlight.
|
|
71
|
+
const ring = document.createElement('div');
|
|
72
|
+
ring.id = 'deck-spot-ring';
|
|
73
|
+
ring.setAttribute('aria-hidden', 'true');
|
|
74
|
+
|
|
75
|
+
document.body.append(veil, ring);
|
|
76
|
+
|
|
77
|
+
// The toolbar is annotate's; this plugin registers after it in deck.js and
|
|
78
|
+
// hangs its button there. A deck running without the pen still gets one.
|
|
79
|
+
let toolbar = document.querySelector('#deck-tools');
|
|
80
|
+
if (!toolbar) {
|
|
81
|
+
toolbar = document.createElement('div');
|
|
82
|
+
toolbar.id = 'deck-tools';
|
|
83
|
+
toolbar.setAttribute('role', 'toolbar');
|
|
84
|
+
toolbar.setAttribute('aria-label', 'Slide tools');
|
|
85
|
+
document.body.append(toolbar);
|
|
86
|
+
}
|
|
87
|
+
const button = document.createElement('button');
|
|
88
|
+
button.id = 'tool-spot';
|
|
89
|
+
button.className = 'deck-tool';
|
|
90
|
+
button.type = 'button';
|
|
91
|
+
button.setAttribute('aria-pressed', 'false');
|
|
92
|
+
button.title = 'Spotlight — drag to light up part of the slide (Shift)';
|
|
93
|
+
button.setAttribute('aria-label', 'Spotlight');
|
|
94
|
+
button.innerHTML = SPOT_ICON;
|
|
95
|
+
toolbar.append(button);
|
|
96
|
+
|
|
97
|
+
// ---- state ----------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
let armed = false;
|
|
100
|
+
let rect = null; // {x, y, w, h} in screen px, or null when the veil is clear
|
|
101
|
+
let press = null; // where the current drag started, or null
|
|
102
|
+
let grab = null; // the pointer's offset into a rectangle being moved, or null
|
|
103
|
+
|
|
104
|
+
const layout = () => {
|
|
105
|
+
if (!rect) return;
|
|
106
|
+
veil.style.clipPath = holePath(rect, window.innerWidth, window.innerHeight);
|
|
107
|
+
ring.style.left = `${rect.x}px`;
|
|
108
|
+
ring.style.top = `${rect.y}px`;
|
|
109
|
+
ring.style.width = `${rect.w}px`;
|
|
110
|
+
ring.style.height = `${rect.h}px`;
|
|
111
|
+
ring.style.borderRadius = `${Math.min(RADIUS, rect.w / 2, rect.h / 2)}px`;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const show = () => {
|
|
115
|
+
layout();
|
|
116
|
+
veil.classList.add('has-hole');
|
|
117
|
+
ring.classList.add('has-hole', 'is-live');
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// The clip-path stays as it was, so the hole holds its place while the
|
|
121
|
+
// veil fades out around it.
|
|
122
|
+
const clear = () => {
|
|
123
|
+
rect = null;
|
|
124
|
+
press = null;
|
|
125
|
+
grab = null;
|
|
126
|
+
veil.classList.remove('has-hole');
|
|
127
|
+
ring.classList.remove('has-hole', 'is-live');
|
|
128
|
+
ring.style.cursor = '';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const setSpot = (on) => {
|
|
132
|
+
armed = on;
|
|
133
|
+
button.setAttribute('aria-pressed', String(on));
|
|
134
|
+
toolbar.classList.toggle('is-armed', on);
|
|
135
|
+
// Only an armed veil takes the pointer; the rest of the time clicks fall
|
|
136
|
+
// through to the slide underneath — same contract as the pen's canvas.
|
|
137
|
+
veil.classList.toggle('is-live', on);
|
|
138
|
+
if (on) document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'spotlight' }));
|
|
139
|
+
else clear();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Two tools, one pointer: arming either disarms the other. The handshake
|
|
143
|
+
// is a DOM event rather than an import either way round, so each plugin
|
|
144
|
+
// works alone.
|
|
145
|
+
document.addEventListener('deck:tool-armed', (event) => {
|
|
146
|
+
if (event.detail !== 'spotlight' && armed) setSpot(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ---- the drag -------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
veil.addEventListener('pointerdown', (event) => {
|
|
152
|
+
if (!armed || event.button !== 0) return;
|
|
153
|
+
veil.setPointerCapture(event.pointerId);
|
|
154
|
+
press = { id: event.pointerId, x: event.clientX, y: event.clientY, dragging: false };
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
veil.addEventListener('pointermove', (event) => {
|
|
158
|
+
if (!press || event.pointerId !== press.id) return;
|
|
159
|
+
const w = Math.abs(event.clientX - press.x);
|
|
160
|
+
const h = Math.abs(event.clientY - press.y);
|
|
161
|
+
// Below the threshold it is still a click; past it the veil comes down
|
|
162
|
+
// and the rectangle follows the pointer.
|
|
163
|
+
if (!press.dragging && w < MIN_DRAG && h < MIN_DRAG) return;
|
|
164
|
+
press.dragging = true;
|
|
165
|
+
rect = {
|
|
166
|
+
x: Math.min(press.x, event.clientX),
|
|
167
|
+
y: Math.min(press.y, event.clientY),
|
|
168
|
+
w: Math.max(w, 1),
|
|
169
|
+
h: Math.max(h, 1),
|
|
170
|
+
};
|
|
171
|
+
show();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const release = (event) => {
|
|
175
|
+
if (!press || event.pointerId !== press.id) return;
|
|
176
|
+
// A plain click switches the light back off; a drag leaves its rectangle.
|
|
177
|
+
if (!press.dragging) clear();
|
|
178
|
+
press = null;
|
|
179
|
+
};
|
|
180
|
+
veil.addEventListener('pointerup', release);
|
|
181
|
+
veil.addEventListener('pointercancel', release);
|
|
182
|
+
|
|
183
|
+
// ---- the move and the resize ----------------------------------------
|
|
184
|
+
// Grabbing the light and drawing it never race: the ring owns the inside
|
|
185
|
+
// of the rectangle, the veil owns everything around it. Within the ring, a
|
|
186
|
+
// band along the edge resizes and the middle moves — the zone is named by
|
|
187
|
+
// the edges the pointer is near ('n', 'se', …; '' is the middle), and the
|
|
188
|
+
// same name picks the arrow cursor and says which edges follow the drag.
|
|
189
|
+
|
|
190
|
+
const zoneAt = (event) => {
|
|
191
|
+
const x = event.clientX - rect.x;
|
|
192
|
+
const y = event.clientY - rect.y;
|
|
193
|
+
// A small rectangle keeps a third of itself in the middle, or it could
|
|
194
|
+
// only ever be resized, never picked up.
|
|
195
|
+
const band = Math.min(EDGE, rect.w / 3, rect.h / 3);
|
|
196
|
+
return (
|
|
197
|
+
(y < band ? 'n' : y > rect.h - band ? 's' : '') +
|
|
198
|
+
(x < band ? 'w' : x > rect.w - band ? 'e' : '')
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
ring.addEventListener('pointerdown', (event) => {
|
|
203
|
+
if (!armed || !rect || event.button !== 0) return;
|
|
204
|
+
ring.setPointerCapture(event.pointerId);
|
|
205
|
+
grab = {
|
|
206
|
+
id: event.pointerId,
|
|
207
|
+
zone: zoneAt(event),
|
|
208
|
+
dx: event.clientX - rect.x,
|
|
209
|
+
dy: event.clientY - rect.y,
|
|
210
|
+
from: { ...rect },
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
ring.addEventListener('pointermove', (event) => {
|
|
215
|
+
if (!grab || event.pointerId !== grab.id) {
|
|
216
|
+
// Just hovering: wear the zone's cursor. The middle clears the inline
|
|
217
|
+
// style so the stylesheet's grab/grabbing pair takes over.
|
|
218
|
+
if (!grab && rect) ring.style.cursor = CURSORS[zoneAt(event)] ?? '';
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!grab.zone) {
|
|
223
|
+
rect.x = event.clientX - grab.dx;
|
|
224
|
+
rect.y = event.clientY - grab.dy;
|
|
225
|
+
layout();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Each named edge follows the pointer while its opposite stays anchored
|
|
230
|
+
// where the drag found it, clamped so the light never collapses.
|
|
231
|
+
const { from } = grab;
|
|
232
|
+
if (grab.zone.includes('w')) {
|
|
233
|
+
rect.x = Math.min(event.clientX, from.x + from.w - MIN_SIZE);
|
|
234
|
+
rect.w = from.x + from.w - rect.x;
|
|
235
|
+
}
|
|
236
|
+
if (grab.zone.includes('e')) rect.w = Math.max(event.clientX - from.x, MIN_SIZE);
|
|
237
|
+
if (grab.zone.includes('n')) {
|
|
238
|
+
rect.y = Math.min(event.clientY, from.y + from.h - MIN_SIZE);
|
|
239
|
+
rect.h = from.y + from.h - rect.y;
|
|
240
|
+
}
|
|
241
|
+
if (grab.zone.includes('s')) rect.h = Math.max(event.clientY - from.y, MIN_SIZE);
|
|
242
|
+
layout();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const drop = (event) => {
|
|
246
|
+
if (grab && event.pointerId === grab.id) grab = null;
|
|
247
|
+
};
|
|
248
|
+
ring.addEventListener('pointerup', drop);
|
|
249
|
+
ring.addEventListener('pointercancel', drop);
|
|
250
|
+
|
|
251
|
+
button.addEventListener('click', () => setSpot(!armed));
|
|
252
|
+
|
|
253
|
+
// The shortcut is a tap of Shift on its own — not through addKeyBinding,
|
|
254
|
+
// which knows nothing of keyup. Toggling on the keydown would fire on
|
|
255
|
+
// every Shift+Space and Shift+arrow reveal already answers, so the toggle
|
|
256
|
+
// waits for the release, and stands down the moment any other key shows
|
|
257
|
+
// Shift was there as a modifier.
|
|
258
|
+
let shiftTap = false;
|
|
259
|
+
document.addEventListener('keydown', (event) => {
|
|
260
|
+
shiftTap = event.key === 'Shift' && !event.repeat;
|
|
261
|
+
});
|
|
262
|
+
document.addEventListener('keyup', (event) => {
|
|
263
|
+
if (event.key === 'Shift' && shiftTap) setSpot(!armed);
|
|
264
|
+
});
|
|
265
|
+
// Hand-rolled keys are invisible to reveal's help overlay; shortcuts.js
|
|
266
|
+
// writes the row the binding above cannot.
|
|
267
|
+
deck.on('ready', () =>
|
|
268
|
+
listShortcutAfter(deck, { key: 'SHIFT', action: 'Toggle the spotlight', after: /overview/i }),
|
|
269
|
+
);
|
|
270
|
+
// Escape is reveal's overview toggle; take it back only while armed, in the
|
|
271
|
+
// capture phase, exactly as the pen does.
|
|
272
|
+
document.addEventListener(
|
|
273
|
+
'keydown',
|
|
274
|
+
(event) => {
|
|
275
|
+
if (event.key !== 'Escape' || !armed) return;
|
|
276
|
+
setSpot(false);
|
|
277
|
+
event.stopPropagation();
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
},
|
|
280
|
+
true,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// A spotlight aims at one slide's content; the next slide starts dark-free.
|
|
284
|
+
deck.on('slidechanged', clear);
|
|
285
|
+
deck.on('overviewshown', () => setSpot(false));
|
|
286
|
+
|
|
287
|
+
// The rectangle is screen pixels: on resize it keeps its place on screen,
|
|
288
|
+
// only the outer path has to learn the new viewport.
|
|
289
|
+
window.addEventListener('resize', layout);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export default RevealSpotlight;
|