fb-slides 0.10.0 → 0.11.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 +56 -0
- package/README.md +96 -5
- package/lib/build.mjs +4 -2
- package/lib/config.mjs +10 -3
- package/lib/dev.mjs +30 -9
- package/lib/edit.mjs +3 -1
- package/lib/render.mjs +16 -4
- package/lib/theme.mjs +53 -5
- package/package.json +1 -1
- package/runtime/annotate.js +36 -0
- package/runtime/deck.js +48 -3
- package/runtime/edit.js +45 -21
- package/runtime/snippets.js +43 -0
- package/runtime/theme.base.css +280 -14
- package/runtime/themes/custom-aurora.css +184 -0
- package/runtime/themes/custom-editoriale.css +185 -0
- package/runtime/themes.js +250 -0
- package/templates/starter/assets/author/JSDAY-26.jpg +0 -0
- package/templates/starter/assets/author/devfest-pisa-26.jpg +0 -0
- package/templates/starter/assets/author/devfestroma-25.jpg +0 -0
- package/templates/starter/assets/author/fb-mountain.jpg +0 -0
- package/templates/starter/assets/author/fb-mtb.jpeg +0 -0
- package/templates/starter/assets/author/gemini-masterclass-26.jpg +0 -0
- package/templates/starter/assets/author/webday-26.jpg +0 -0
- package/templates/starter/assets/jshd-sticker.png +0 -0
- package/templates/starter/decks/01-intro.md +4 -57
- package/templates/starter/decks/02-author.md +64 -0
- package/templates/starter/decks/02-demos.md +36 -0
- package/templates/starter/slides.config.js +26 -6
- package/templates/starter/assets/author/photo-1.svg +0 -11
- package/templates/starter/assets/author/photo-2.svg +0 -11
package/runtime/edit.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// RevealEdit — edit the slide you are looking at, save it back into its .md.
|
|
3
3
|
//
|
|
4
|
-
// A reveal.js plugin, dev-server only in effect
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// A reveal.js plugin, localhost and dev-server only in effect. At init it first
|
|
5
|
+
// looks at the address bar: anywhere but localhost — a published deck, a phone
|
|
6
|
+
// on the LAN — it stops right there and never asks. On localhost it probes the
|
|
7
|
+
// edit API that `fb-slides dev` mounts, and only an answer makes the button
|
|
8
|
+
// appear: `preview` and any other static server 404 the probe, and this plugin
|
|
9
|
+
// does nothing — which is how the same runtime ships in both without
|
|
10
|
+
// configuration.
|
|
8
11
|
//
|
|
9
12
|
// The drawer opens down the right edge with the slide still on stage, one
|
|
10
13
|
// textarea for the slide and one for the speaker notes. Typing re-renders the
|
|
@@ -56,6 +59,12 @@ const joinNotes = ({ content, label, notes }) =>
|
|
|
56
59
|
// one without a CORS preflight, which the dev server never answers.
|
|
57
60
|
const API_HEADERS = { 'x-fb-slides': '1' };
|
|
58
61
|
|
|
62
|
+
// Mirror of LOCAL_HOST in lib/edit.mjs: the edit API refuses any other Host,
|
|
63
|
+
// so asking from a published deck — or a phone on the LAN — could only ever
|
|
64
|
+
// cost a 404 in the console. location.hostname carries no port, `[::1]` keeps
|
|
65
|
+
// its brackets, and a file:// page has an empty one.
|
|
66
|
+
const LOCAL_HOST = /^(localhost|127\.0\.0\.1|\[::1\])$/;
|
|
67
|
+
|
|
59
68
|
const EDIT_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
|
60
69
|
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
61
70
|
<path d="M11 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-5" />
|
|
@@ -84,10 +93,14 @@ const RevealEdit = () => ({
|
|
|
84
93
|
id: 'edit',
|
|
85
94
|
|
|
86
95
|
init(deck) {
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
96
|
+
// Not on localhost: not even a question. The server would refuse the
|
|
97
|
+
// answer anyway, and a published deck should not log a 404 on every load.
|
|
98
|
+
if (!LOCAL_HOST.test(location.hostname)) return;
|
|
99
|
+
|
|
100
|
+
// Nothing is built until the API answers: under `preview` or any static
|
|
101
|
+
// server the fetch comes back as a 404 page, the JSON parse throws, and
|
|
102
|
+
// the deck never knows this plugin existed. Init itself stays synchronous
|
|
103
|
+
// so a slow or absent server cannot hold up the whole deck.
|
|
91
104
|
fetch('/api/editable', { headers: API_HEADERS, cache: 'no-cache' })
|
|
92
105
|
.then((response) => response.json())
|
|
93
106
|
.then((answer) => {
|
|
@@ -132,6 +145,12 @@ const build = (deck, assetsDir) => {
|
|
|
132
145
|
<button type="button" class="deck-edit-nav" data-go="1" title="Next slide" aria-label="Next slide">${NEXT_ICON}</button>
|
|
133
146
|
<button type="button" class="deck-edit-close" aria-label="Close">${CLOSE_ICON}</button>
|
|
134
147
|
</header>
|
|
148
|
+
<div class="deck-edit-actions">
|
|
149
|
+
<button type="button" class="deck-edit-add" title="Add a slide after this one">Add page</button>
|
|
150
|
+
<button type="button" class="deck-edit-delete" title="Delete this slide">Delete page</button>
|
|
151
|
+
<span class="deck-edit-status" role="status"></span>
|
|
152
|
+
<button type="button" class="deck-edit-save">Save</button>
|
|
153
|
+
</div>
|
|
135
154
|
<div class="deck-edit-row">
|
|
136
155
|
<label class="deck-edit-label" for="deck-edit-source">slide</label>
|
|
137
156
|
<span class="deck-edit-row-acts">
|
|
@@ -143,12 +162,6 @@ const build = (deck, assetsDir) => {
|
|
|
143
162
|
<textarea id="deck-edit-source" spellcheck="false"></textarea>
|
|
144
163
|
<label class="deck-edit-label" for="deck-edit-notes">speaker notes</label>
|
|
145
164
|
<textarea id="deck-edit-notes" spellcheck="false"></textarea>
|
|
146
|
-
<footer>
|
|
147
|
-
<button type="button" class="deck-edit-add" title="Add a slide after this one">Add page</button>
|
|
148
|
-
<button type="button" class="deck-edit-delete" title="Delete this slide">Delete page</button>
|
|
149
|
-
<span class="deck-edit-status" role="status"></span>
|
|
150
|
-
<button type="button" class="deck-edit-save">Save</button>
|
|
151
|
-
</footer>
|
|
152
165
|
<div class="deck-edit-confirm" hidden>
|
|
153
166
|
<p>Delete this slide from <code></code>?<br />There is no undo.</p>
|
|
154
167
|
<div>
|
|
@@ -303,10 +316,12 @@ const build = (deck, assetsDir) => {
|
|
|
303
316
|
deck.layout();
|
|
304
317
|
};
|
|
305
318
|
|
|
306
|
-
// `focus` names the box that gets the room
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
319
|
+
// `focus` names the box that gets the room — navigation and save-reloads
|
|
320
|
+
// pass the one the drawer already had, so following the deck never folds
|
|
321
|
+
// the notes away under a writing hand. `takeFocus` says whether that box
|
|
322
|
+
// also gets the cursor: opening by hand always does, following the deck
|
|
323
|
+
// only when the cursor was already in the drawer.
|
|
324
|
+
const open = async (focus = 'source', { takeFocus = true } = {}) => {
|
|
310
325
|
const slide = deck.getCurrentSlide();
|
|
311
326
|
const src = SRC(slide);
|
|
312
327
|
if (!src) return;
|
|
@@ -354,7 +369,7 @@ const build = (deck, assetsDir) => {
|
|
|
354
369
|
// stand this drawer down in turn when one of them arms (see below).
|
|
355
370
|
document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'edit' }));
|
|
356
371
|
setOpen(true);
|
|
357
|
-
(focus === 'notes' ? notesArea : sourceArea).focus();
|
|
372
|
+
if (takeFocus) (focus === 'notes' ? notesArea : sourceArea).focus();
|
|
358
373
|
};
|
|
359
374
|
|
|
360
375
|
const toggle = () => (editing ? close() : open());
|
|
@@ -442,6 +457,11 @@ const build = (deck, assetsDir) => {
|
|
|
442
457
|
// stage thing — and the drawer follows through `slidechanged` like any other
|
|
443
458
|
// navigation.
|
|
444
459
|
for (const nav of panel.querySelectorAll('.deck-edit-nav')) {
|
|
460
|
+
// The press itself would move the focus onto the button (Chrome does),
|
|
461
|
+
// and the drawer could no longer tell a writing hand from a browsing one
|
|
462
|
+
// when the new slide lands. Held back, the cursor stays where it was —
|
|
463
|
+
// in a box, or out on the deck.
|
|
464
|
+
nav.addEventListener('mousedown', (event) => event.preventDefault());
|
|
445
465
|
nav.addEventListener('click', () => {
|
|
446
466
|
if (Number(nav.dataset.go) < 0) deck.left({ skipFragments: true });
|
|
447
467
|
else deck.right({ skipFragments: true });
|
|
@@ -541,12 +561,16 @@ const build = (deck, assetsDir) => {
|
|
|
541
561
|
|
|
542
562
|
// The drawer follows the deck: landing on another slide loads that one,
|
|
543
563
|
// and whatever was typed on the old one goes the way of a cancel. The
|
|
544
|
-
//
|
|
564
|
+
// cursor only comes back to the box it was in — arrowing through the deck
|
|
565
|
+
// from outside the drawer must not pull it in. The overview is a different
|
|
566
|
+
// surface altogether, so there it does close.
|
|
545
567
|
deck.on('slidechanged', async () => {
|
|
546
568
|
if (!editing) return;
|
|
547
569
|
const focus = panel.dataset.focus;
|
|
570
|
+
const active = document.activeElement;
|
|
571
|
+
const takeFocus = active === sourceArea || active === notesArea;
|
|
548
572
|
discard();
|
|
549
|
-
await open(focus);
|
|
573
|
+
await open(focus, { takeFocus });
|
|
550
574
|
// The new slide would not load — a drawer left standing would lie about
|
|
551
575
|
// what it edits.
|
|
552
576
|
if (!editing) setOpen(false);
|
package/runtime/snippets.js
CHANGED
|
@@ -192,6 +192,49 @@ const BUILT_IN = [
|
|
|
192
192
|
keys: 'appear step reveal element attach',
|
|
193
193
|
body: '${content}\n<!-- .element: class="fragment" -->',
|
|
194
194
|
},
|
|
195
|
+
{
|
|
196
|
+
group: 'Text',
|
|
197
|
+
label: 'Eyebrow chips',
|
|
198
|
+
hint: 'the line above the title — pills, or a // rule, depending on the theme',
|
|
199
|
+
keys: 'tag label kicker pill badge topic',
|
|
200
|
+
top: true,
|
|
201
|
+
body: '<p class="chips"><span>${Angular}</span><span>Signals</span></p>',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
group: 'Text',
|
|
205
|
+
label: 'Rows',
|
|
206
|
+
hint: 'two or three things, each with a glyph and one sentence',
|
|
207
|
+
keys: 'cards list compare items icon',
|
|
208
|
+
body:
|
|
209
|
+
'<div class="rows">\n' +
|
|
210
|
+
' <div class="row">\n' +
|
|
211
|
+
' <span class="row-icon">${1}</span>\n' +
|
|
212
|
+
' <div><strong>Name</strong><p>What it is for.</p></div>\n' +
|
|
213
|
+
' </div>\n' +
|
|
214
|
+
' <div class="row">\n' +
|
|
215
|
+
' <span class="row-icon">2</span>\n' +
|
|
216
|
+
' <div><strong>Name</strong><p>What it is for.</p></div>\n' +
|
|
217
|
+
' </div>\n' +
|
|
218
|
+
'</div>',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
group: 'Text',
|
|
222
|
+
label: 'Call to action',
|
|
223
|
+
hint: 'the last slide — the first one is filled, both grow an arrow',
|
|
224
|
+
keys: 'cta end follow subscribe outro links',
|
|
225
|
+
body:
|
|
226
|
+
'<div class="rows rows--cta">\n' +
|
|
227
|
+
' <div class="row"><div><strong>${Save the post}</strong></div></div>\n' +
|
|
228
|
+
' <div class="row"><div><strong>Follow me</strong></div></div>\n' +
|
|
229
|
+
'</div>',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
group: 'Text',
|
|
233
|
+
label: 'Code, with its file name',
|
|
234
|
+
hint: 'the marker draws the header bar — the language comes from the extension',
|
|
235
|
+
keys: 'fence filename snippet header tab',
|
|
236
|
+
body: '<!-- file: ${cart.component.ts} -->\n```ts\nconst total = computed(() => 0);\n```',
|
|
237
|
+
},
|
|
195
238
|
{
|
|
196
239
|
group: 'Text',
|
|
197
240
|
label: 'Table',
|
package/runtime/theme.base.css
CHANGED
|
@@ -122,7 +122,14 @@ body,
|
|
|
122
122
|
-webkit-background-clip: text;
|
|
123
123
|
background-clip: text;
|
|
124
124
|
color: transparent;
|
|
125
|
-
|
|
125
|
+
/* The gradient only paints inside the element's box, and with
|
|
126
|
+
`background-clip: text` a glyph is invisible wherever it is not painted.
|
|
127
|
+
A pack that tightens line-height under a display serif (both shipped
|
|
128
|
+
packs do) leaves descenders and tall accents outside the line box, so
|
|
129
|
+
they were cut. Padding widens the painted box; the margins take the
|
|
130
|
+
same amount back so nothing on the slide moves. */
|
|
131
|
+
padding: 0.15em 0.1em 0.3em 0;
|
|
132
|
+
margin: -0.15em -0.1em -0.15em 0;
|
|
126
133
|
}
|
|
127
134
|
.reveal .slides section.deck-title h2 {
|
|
128
135
|
color: var(--muted);
|
|
@@ -266,6 +273,10 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
266
273
|
.reveal .slides section > table tbody tr:nth-child(even) { background: rgb(255 255 255 / 0.018); }
|
|
267
274
|
.reveal .slides section > table td:first-child { color: var(--muted); white-space: nowrap; }
|
|
268
275
|
.reveal .slides section > table th:first-child:empty { min-width: 8ch; }
|
|
276
|
+
/* A table written with an empty header row (`| | |`) is a two-column layout —
|
|
277
|
+
a term and what it means — not a table with nameless columns. Drawing the bar
|
|
278
|
+
anyway puts an empty grey strip across the slide, so it goes. */
|
|
279
|
+
.reveal .slides section > table thead:not(:has(th:not(:empty))) { display: none; }
|
|
269
280
|
|
|
270
281
|
/* --- fragments ----------------------------------------------------------- */
|
|
271
282
|
|
|
@@ -424,6 +435,163 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
424
435
|
}
|
|
425
436
|
.reveal .slides .box > :last-child { margin-bottom: 0; }
|
|
426
437
|
|
|
438
|
+
/* --- eyebrow chips ------------------------------------------------------- */
|
|
439
|
+
/* `<p class="chips"><span>Angular</span><span>Signals</span></p>` — the line
|
|
440
|
+
above the title that says which talk this is, and where in it you are. The
|
|
441
|
+
first chip is the loud one; the rest ride along quieter, because a row of
|
|
442
|
+
equally loud labels says nothing. One span is simply one pill.
|
|
443
|
+
|
|
444
|
+
A theme pack is free to make this a `// ANGULAR · SIGNALS` rule instead —
|
|
445
|
+
same markup, same meaning, different voice. */
|
|
446
|
+
.reveal .slides .chips {
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-wrap: wrap;
|
|
449
|
+
align-items: center;
|
|
450
|
+
gap: 0.45em;
|
|
451
|
+
margin: 0 0 1.1em;
|
|
452
|
+
font-size: 0.46em;
|
|
453
|
+
line-height: 1;
|
|
454
|
+
}
|
|
455
|
+
.reveal .slides .chips span {
|
|
456
|
+
padding: 0.62em 1em;
|
|
457
|
+
border: 1px solid var(--line);
|
|
458
|
+
border-radius: 999px;
|
|
459
|
+
background: color-mix(in srgb, var(--card) 60%, transparent);
|
|
460
|
+
color: var(--muted);
|
|
461
|
+
font-family: var(--mono);
|
|
462
|
+
letter-spacing: 0.01em;
|
|
463
|
+
white-space: nowrap;
|
|
464
|
+
}
|
|
465
|
+
.reveal .slides .chips span:first-child {
|
|
466
|
+
background: var(--accent);
|
|
467
|
+
border-color: var(--accent);
|
|
468
|
+
color: var(--bg);
|
|
469
|
+
font-weight: 650;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* --- rows ---------------------------------------------------------------- */
|
|
473
|
+
/* A list that reads as cards: one line each, an optional glyph in a tile at the
|
|
474
|
+
left, the name and one sentence about it. It is the shape a comparison slide
|
|
475
|
+
keeps wanting — two things, what each is for — and a <ul> cannot hold it
|
|
476
|
+
without turning into a table nobody can read from the back of the room.
|
|
477
|
+
|
|
478
|
+
`<div class="rows">`, then per item `<div class="row">` holding an optional
|
|
479
|
+
`<span class="row-icon">`, a `<strong>` and a `<p>`. */
|
|
480
|
+
.reveal .slides .rows {
|
|
481
|
+
display: flex;
|
|
482
|
+
flex-direction: column;
|
|
483
|
+
gap: 0.55em;
|
|
484
|
+
margin: 0 0 0.7em;
|
|
485
|
+
}
|
|
486
|
+
.reveal .slides .row {
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: flex-start;
|
|
489
|
+
gap: 0.75em;
|
|
490
|
+
padding: 0.75em 0.9em;
|
|
491
|
+
border: 1px solid var(--line);
|
|
492
|
+
border-radius: 14px;
|
|
493
|
+
background: color-mix(in srgb, var(--card) 70%, transparent);
|
|
494
|
+
}
|
|
495
|
+
.reveal .slides .row-icon {
|
|
496
|
+
flex: none;
|
|
497
|
+
display: grid;
|
|
498
|
+
place-items: center;
|
|
499
|
+
width: 1.9em;
|
|
500
|
+
height: 1.9em;
|
|
501
|
+
border-radius: 12px;
|
|
502
|
+
background: var(--accent);
|
|
503
|
+
color: var(--bg);
|
|
504
|
+
font-family: var(--mono);
|
|
505
|
+
font-size: 0.85em;
|
|
506
|
+
font-weight: 700;
|
|
507
|
+
line-height: 1;
|
|
508
|
+
}
|
|
509
|
+
/* The text column, whether or not it was wrapped in one. */
|
|
510
|
+
.reveal .slides .row > :not(.row-icon) { min-width: 0; }
|
|
511
|
+
.reveal .slides .row strong {
|
|
512
|
+
display: block;
|
|
513
|
+
margin-bottom: 0.1em;
|
|
514
|
+
font-size: 0.92em;
|
|
515
|
+
}
|
|
516
|
+
.reveal .slides .row p {
|
|
517
|
+
margin: 0;
|
|
518
|
+
color: var(--muted);
|
|
519
|
+
font-size: 0.7em;
|
|
520
|
+
line-height: 1.4;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* `<div class="rows rows--cta">` — the last slide's two ways out. Each row
|
|
524
|
+
grows an arrow at its right edge, and the first one is the one you actually
|
|
525
|
+
want pressed, so it is the one that is filled. Same rule as the chips: in a
|
|
526
|
+
row of equals nothing is being asked. */
|
|
527
|
+
.reveal .slides .rows--cta .row { align-items: center; }
|
|
528
|
+
.reveal .slides .rows--cta .row::after {
|
|
529
|
+
content: '\2192';
|
|
530
|
+
margin-left: auto;
|
|
531
|
+
padding-left: 0.7em;
|
|
532
|
+
font-size: 0.85em;
|
|
533
|
+
opacity: 0.7;
|
|
534
|
+
}
|
|
535
|
+
.reveal .slides .rows--cta .row:first-child {
|
|
536
|
+
background: var(--accent);
|
|
537
|
+
border-color: var(--accent);
|
|
538
|
+
color: var(--bg);
|
|
539
|
+
}
|
|
540
|
+
.reveal .slides .rows--cta .row:first-child p,
|
|
541
|
+
.reveal .slides .rows--cta .row:first-child::after {
|
|
542
|
+
color: color-mix(in srgb, var(--bg) 72%, var(--accent));
|
|
543
|
+
opacity: 1;
|
|
544
|
+
}
|
|
545
|
+
/* `.reveal strong` is white, which on a filled accent row is the one colour it
|
|
546
|
+
must not be: the label has to read against the fill, not against the deck. */
|
|
547
|
+
.reveal .slides .rows--cta .row:first-child strong { color: inherit; }
|
|
548
|
+
|
|
549
|
+
/* --- a code block that says which file it is ----------------------------- */
|
|
550
|
+
/* `<!-- file: cart.component.ts -->` on the line above a fence (deck.js lifts
|
|
551
|
+
it onto the <pre>) draws a header bar: the name on the left, the language on
|
|
552
|
+
the right. The language is not written anywhere — it is the `language-*`
|
|
553
|
+
class the highlighter already applied, uppercased.
|
|
554
|
+
|
|
555
|
+
The bar is the <pre>'s own ::before rather than an element, so the markup a
|
|
556
|
+
deck carries stays a plain fenced code block and Marp still renders it. */
|
|
557
|
+
.reveal pre[data-file] { padding-top: 2.1em; }
|
|
558
|
+
.reveal pre[data-file]::before {
|
|
559
|
+
content: attr(data-file);
|
|
560
|
+
position: absolute;
|
|
561
|
+
top: 0;
|
|
562
|
+
left: 0;
|
|
563
|
+
right: 0;
|
|
564
|
+
height: 2.1em;
|
|
565
|
+
display: flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
padding: 0 1.2em;
|
|
568
|
+
box-sizing: border-box;
|
|
569
|
+
border-bottom: 1px solid var(--line);
|
|
570
|
+
background: color-mix(in srgb, var(--card) 70%, #14181f);
|
|
571
|
+
color: var(--muted);
|
|
572
|
+
font-family: var(--mono);
|
|
573
|
+
font-size: 0.85em;
|
|
574
|
+
/* Above the stack of absolutely positioned <code> steps, which start at the
|
|
575
|
+
top of the block and would otherwise slide under the bar. */
|
|
576
|
+
z-index: 1;
|
|
577
|
+
}
|
|
578
|
+
.reveal pre[data-lang]::after {
|
|
579
|
+
content: attr(data-lang);
|
|
580
|
+
position: absolute;
|
|
581
|
+
top: 0;
|
|
582
|
+
right: 1.2em;
|
|
583
|
+
height: 2.1em;
|
|
584
|
+
display: flex;
|
|
585
|
+
align-items: center;
|
|
586
|
+
color: var(--accent);
|
|
587
|
+
font-family: var(--mono);
|
|
588
|
+
font-size: 0.72em;
|
|
589
|
+
letter-spacing: 0.08em;
|
|
590
|
+
z-index: 2;
|
|
591
|
+
}
|
|
592
|
+
/* The header sits in the <pre>'s padding; the code keeps its own. */
|
|
593
|
+
.reveal pre[data-file] code { border-radius: 0 0 10px 10px; }
|
|
594
|
+
|
|
427
595
|
/* --- the author slide ---------------------------------------------------- */
|
|
428
596
|
/* `<!-- .slide: class="author-slide" -->` turns a slide into the speaker page:
|
|
429
597
|
photos filling the left edge to edge, the bio centred on the right. Who you
|
|
@@ -630,7 +798,9 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
630
798
|
|
|
631
799
|
/* Hidden until it is wanted: on stage the slide is the point, so the toolbar
|
|
632
800
|
sleeps just above the top edge of the screen. Parking the mouse along that
|
|
633
|
-
edge — or arming a tool, or tabbing into it — slides it back down
|
|
801
|
+
edge — or arming a tool, or tabbing into it — slides it back down, and down
|
|
802
|
+
it stays: `is-open` is a latch annotate.js sets, and only the X chip (or the
|
|
803
|
+
X key) lifts it. Neither the mouse wandering off nor a tool disarming does. */
|
|
634
804
|
#deck-tools {
|
|
635
805
|
position: fixed;
|
|
636
806
|
top: 14px;
|
|
@@ -686,19 +856,13 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
686
856
|
0%, 100% { transform: translateX(-50%) translateY(0); }
|
|
687
857
|
50% { transform: translateX(-50%) translateY(3px); }
|
|
688
858
|
}
|
|
689
|
-
#deck-tools
|
|
690
|
-
#deck-tools:focus-within,
|
|
691
|
-
#deck-tools.is-armed {
|
|
859
|
+
#deck-tools.is-open {
|
|
692
860
|
transform: translateX(-50%) translateY(0);
|
|
693
861
|
}
|
|
694
862
|
/* Once the toolbar is out, the chevron has done its job — the strip stays as
|
|
695
|
-
a silent
|
|
696
|
-
#deck-tools
|
|
697
|
-
#deck-tools
|
|
698
|
-
#deck-tools.is-armed::before { cursor: default; }
|
|
699
|
-
#deck-tools:hover::after,
|
|
700
|
-
#deck-tools:focus-within::after,
|
|
701
|
-
#deck-tools.is-armed::after { opacity: 0; }
|
|
863
|
+
a silent surface. */
|
|
864
|
+
#deck-tools.is-open::before { cursor: default; }
|
|
865
|
+
#deck-tools.is-open::after { opacity: 0; }
|
|
702
866
|
|
|
703
867
|
.deck-tool {
|
|
704
868
|
position: relative;
|
|
@@ -739,6 +903,21 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
739
903
|
background: color-mix(in srgb, var(--text) 10%, transparent);
|
|
740
904
|
color: var(--text);
|
|
741
905
|
}
|
|
906
|
+
/* The X: last on the strip whatever order the plugins hung their chips in,
|
|
907
|
+
and set a little apart from the tools it puts away. */
|
|
908
|
+
.deck-tools-close {
|
|
909
|
+
order: 1;
|
|
910
|
+
margin-left: 6px;
|
|
911
|
+
}
|
|
912
|
+
.deck-tools-close::before {
|
|
913
|
+
content: '';
|
|
914
|
+
position: absolute;
|
|
915
|
+
left: -5px;
|
|
916
|
+
top: 8px;
|
|
917
|
+
bottom: 8px;
|
|
918
|
+
width: 1px;
|
|
919
|
+
background: var(--line);
|
|
920
|
+
}
|
|
742
921
|
/* An armed tool wears the colour it draws in. annotate.js owns the pen's
|
|
743
922
|
palette — it is a list the toolbar enumerates, not a theme token — and sets
|
|
744
923
|
`--pen` and its readable foreground on #deck-tools as the colour changes. */
|
|
@@ -791,6 +970,90 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
|
|
|
791
970
|
0 0 14px color-mix(in srgb, var(--ink) 65%, transparent);
|
|
792
971
|
}
|
|
793
972
|
|
|
973
|
+
/* --- the theme switcher (themes.js, dev only) ---------------------------- */
|
|
974
|
+
/* Hung below the toolbar like the swatches, for the same reason: the toolbar
|
|
975
|
+
comes down from the top edge, so down is where the room is. It exists only
|
|
976
|
+
under `fb-slides dev` — a built deck is rendered without the list this reads,
|
|
977
|
+
and the plugin never gets as far as creating any of it. */
|
|
978
|
+
#deck-themes {
|
|
979
|
+
position: absolute;
|
|
980
|
+
top: calc(100% + 9px);
|
|
981
|
+
left: 50%;
|
|
982
|
+
transform: translateX(-50%);
|
|
983
|
+
min-width: 200px;
|
|
984
|
+
padding: 6px;
|
|
985
|
+
/* Reveal ships fifteen themes and the packs sit above them: the list is
|
|
986
|
+
taller than a toolbar menu should be, so it scrolls rather than running off
|
|
987
|
+
the bottom of a laptop screen. */
|
|
988
|
+
max-height: min(60vh, 420px);
|
|
989
|
+
overflow-y: auto;
|
|
990
|
+
overscroll-behavior: contain;
|
|
991
|
+
background: color-mix(in srgb, var(--card) 92%, transparent);
|
|
992
|
+
border: 1px solid var(--line);
|
|
993
|
+
border-radius: 12px;
|
|
994
|
+
backdrop-filter: blur(6px);
|
|
995
|
+
box-shadow: 0 14px 40px rgb(0 0 0 / 0.4);
|
|
996
|
+
scrollbar-width: thin;
|
|
997
|
+
scrollbar-color: var(--outline-bar) transparent;
|
|
998
|
+
}
|
|
999
|
+
#deck-themes::-webkit-scrollbar { width: 5px; }
|
|
1000
|
+
#deck-themes::-webkit-scrollbar-track { background: transparent; }
|
|
1001
|
+
#deck-themes::-webkit-scrollbar-thumb { background: var(--outline-bar); border-radius: 999px; }
|
|
1002
|
+
#deck-themes::-webkit-scrollbar-thumb:hover { background: var(--outline-bar-hover); }
|
|
1003
|
+
|
|
1004
|
+
/* The two kinds, told apart. Not sticky, unlike the outline's deck headers:
|
|
1005
|
+
there are only two of them, and two sticky headings at the same offset pile
|
|
1006
|
+
up on each other the moment the list is scrolled. */
|
|
1007
|
+
#deck-themes > h2 {
|
|
1008
|
+
margin: 12px 0 4px;
|
|
1009
|
+
padding: 6px 10px 5px;
|
|
1010
|
+
font: 600 9.5px/1 var(--sans);
|
|
1011
|
+
letter-spacing: 0.12em;
|
|
1012
|
+
text-transform: uppercase;
|
|
1013
|
+
color: var(--muted);
|
|
1014
|
+
background: var(--bg-2);
|
|
1015
|
+
border: 0;
|
|
1016
|
+
}
|
|
1017
|
+
#deck-themes[hidden] { display: none; }
|
|
1018
|
+
|
|
1019
|
+
/* Open, not armed — the pressed look belongs to the tools that draw. */
|
|
1020
|
+
#tool-theme[aria-expanded='true'] {
|
|
1021
|
+
background: color-mix(in srgb, var(--text) 12%, transparent);
|
|
1022
|
+
color: var(--text);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.deck-theme-row {
|
|
1026
|
+
display: block;
|
|
1027
|
+
width: 100%;
|
|
1028
|
+
padding: 7px 10px;
|
|
1029
|
+
border: 0;
|
|
1030
|
+
border-radius: 8px;
|
|
1031
|
+
background: transparent;
|
|
1032
|
+
color: var(--muted);
|
|
1033
|
+
font: 400 12.5px/1.2 var(--mono);
|
|
1034
|
+
text-align: left;
|
|
1035
|
+
cursor: pointer;
|
|
1036
|
+
transition: background 0.14s ease, color 0.14s ease;
|
|
1037
|
+
}
|
|
1038
|
+
.deck-theme-row:hover { background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--text); }
|
|
1039
|
+
.deck-theme-row[aria-selected='true'] {
|
|
1040
|
+
background: color-mix(in srgb, var(--accent) 22%, transparent);
|
|
1041
|
+
color: var(--text);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/* What it takes to keep the choice: the switcher previews, slides.config.js
|
|
1045
|
+
decides. Saying so here is cheaper than being asked. */
|
|
1046
|
+
.deck-theme-note {
|
|
1047
|
+
position: sticky;
|
|
1048
|
+
bottom: -6px;
|
|
1049
|
+
margin: 8px 4px 0;
|
|
1050
|
+
padding: 5px 0 2px;
|
|
1051
|
+
background: var(--bg-2);
|
|
1052
|
+
color: var(--muted);
|
|
1053
|
+
font: 400 10.5px/1.35 var(--mono);
|
|
1054
|
+
opacity: 0.75;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
794
1057
|
/* --- the outline sidebar (outline.js) ------------------------------------ */
|
|
795
1058
|
|
|
796
1059
|
/* The panel does not float over the deck, it takes room from it: the slide
|
|
@@ -1321,11 +1584,14 @@ body.edit-open #deck-edit { transform: none; }
|
|
|
1321
1584
|
.deck-edit-ink .hljs-comment,
|
|
1322
1585
|
.deck-edit-ink .hljs-meta { color: var(--muted); font-weight: 400; }
|
|
1323
1586
|
|
|
1324
|
-
|
|
1587
|
+
/* The file operations sit in a bar under the header, over the boxes: what a
|
|
1588
|
+
save or a delete acts on is the whole slide, not the box the cursor is in. */
|
|
1589
|
+
.deck-edit-actions {
|
|
1325
1590
|
display: flex;
|
|
1326
1591
|
align-items: center;
|
|
1327
1592
|
gap: 10px;
|
|
1328
|
-
padding:
|
|
1593
|
+
padding: 10px 16px;
|
|
1594
|
+
border-bottom: 1px solid var(--line);
|
|
1329
1595
|
}
|
|
1330
1596
|
.deck-edit-status {
|
|
1331
1597
|
flex: 1;
|