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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/bin/fb-slides.mjs +111 -0
  4. package/lib/build.mjs +98 -0
  5. package/lib/config.mjs +90 -0
  6. package/lib/create.mjs +59 -0
  7. package/lib/decks.mjs +13 -0
  8. package/lib/dev.mjs +94 -0
  9. package/lib/render.mjs +52 -0
  10. package/lib/server.mjs +168 -0
  11. package/lib/vendor.mjs +32 -0
  12. package/package.json +51 -0
  13. package/runtime/annotate.js +458 -0
  14. package/runtime/deck.js +299 -0
  15. package/runtime/index.html +37 -0
  16. package/runtime/outline.js +354 -0
  17. package/runtime/shortcuts.js +53 -0
  18. package/runtime/spotlight.js +293 -0
  19. package/runtime/theme.base.css +879 -0
  20. package/templates/starter/README.md +27 -0
  21. package/templates/starter/_gitignore +4 -0
  22. package/templates/starter/_package.json +14 -0
  23. package/templates/starter/assets/.gitkeep +0 -0
  24. package/templates/starter/decks/01-intro.md +44 -0
  25. package/templates/starter/decks/02-demos.md +34 -0
  26. package/templates/starter/demo/angular-hello/README.md +15 -0
  27. package/templates/starter/demo/angular-hello/_package.json +24 -0
  28. package/templates/starter/demo/angular-hello/angular.json +34 -0
  29. package/templates/starter/demo/angular-hello/src/index.html +12 -0
  30. package/templates/starter/demo/angular-hello/src/main.ts +18 -0
  31. package/templates/starter/demo/angular-hello/src/styles.css +22 -0
  32. package/templates/starter/demo/angular-hello/tsconfig.app.json +5 -0
  33. package/templates/starter/demo/angular-hello/tsconfig.json +17 -0
  34. package/templates/starter/demo/counter/index.html +34 -0
  35. package/templates/starter/slides.config.js +34 -0
  36. 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;