fb-slides 0.5.0 → 0.6.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/README.md +74 -0
- package/lib/config.mjs +4 -0
- package/lib/render.mjs +1 -0
- package/package.json +1 -1
- package/runtime/annotate.js +11 -1
- package/runtime/deck.js +52 -0
- package/runtime/edit.js +76 -14
- package/runtime/pointer.js +9 -1
- package/runtime/snippets.js +313 -0
- package/runtime/spotlight.js +9 -1
- package/runtime/theme.base.css +361 -31
- package/templates/starter/assets/author/photo-1.svg +11 -0
- package/templates/starter/assets/author/photo-2.svg +11 -0
- package/templates/starter/decks/01-intro.md +22 -0
- package/templates/starter/decks/02-demos.md +38 -1
package/README.md
CHANGED
|
@@ -127,6 +127,63 @@ A `Note:` block at the end of a slide never shows on screen, only in the speaker
|
|
|
127
127
|
Note: a bad description is the most common reason a tool never gets used.
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### Blocks
|
|
131
|
+
|
|
132
|
+
Six classes cover the layouts a talk keeps needing, so a slide asks for one by name
|
|
133
|
+
instead of carrying a paragraph of inline styles:
|
|
134
|
+
|
|
135
|
+
| | |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| `.cols` / `.col` | two columns — with the `min-width: 0` a code block falls over without |
|
|
138
|
+
| `.frame` | an embed that shares the slide instead of taking it whole |
|
|
139
|
+
| `.caption` | a quiet line under a figure |
|
|
140
|
+
| `.box` | a bordered block: the point the slide comes back to |
|
|
141
|
+
| `.author-slide` | the speaker page — photos down the left edge, the bio on the right |
|
|
142
|
+
| `<img>`, `<video>` | centred, unframed, and never wider than the slide |
|
|
143
|
+
|
|
144
|
+
````markdown
|
|
145
|
+
<div class="cols">
|
|
146
|
+
<div class="col">
|
|
147
|
+
|
|
148
|
+
**In the widget**
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
app.openLink({ url: 'https://example.com/' });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
</div>
|
|
155
|
+
<div class="col">
|
|
156
|
+
|
|
157
|
+
The blank lines matter: Markdown inside a block-level tag is only parsed
|
|
158
|
+
when there is one on each side of it.
|
|
159
|
+
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
````
|
|
163
|
+
|
|
164
|
+
Each of them fixes its numbers in CSS variables — `--cols-gap`, `--frame-w`,
|
|
165
|
+
`--frame-h`, `--author-photo`, `--author-hold` — so a project retunes one from its own
|
|
166
|
+
`theme.css` without restating the rule.
|
|
167
|
+
|
|
168
|
+
The speaker page is markup you write and the theme lays out. Give `.author-photo` as
|
|
169
|
+
many `<img>` as you like: they cross-fade in the order they are written, on a loop that
|
|
170
|
+
restarts every time the slide comes up.
|
|
171
|
+
|
|
172
|
+
```markdown
|
|
173
|
+
<!-- .slide: class="author-slide" -->
|
|
174
|
+
|
|
175
|
+
<div class="author-photo">
|
|
176
|
+
<img src="assets/author/on-stage.jpg" alt="" />
|
|
177
|
+
<img src="assets/author/portrait.jpg" alt="" />
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class="author-bio">
|
|
181
|
+
<h1>Your name</h1>
|
|
182
|
+
<ul><li>What you do</li></ul>
|
|
183
|
+
<p class="author-meta">The stack · you · work · with</p>
|
|
184
|
+
</div>
|
|
185
|
+
```
|
|
186
|
+
|
|
130
187
|
### Editing in the browser
|
|
131
188
|
|
|
132
189
|
On the dev server, `T` (or the last button on the toolbar) opens the slide you
|
|
@@ -140,6 +197,22 @@ right after the current one and opens it; **Delete** removes the slide from its
|
|
|
140
197
|
file, after asking. Clicking into the notes box trades the room with the slide
|
|
141
198
|
box, so both are comfortable to write in.
|
|
142
199
|
|
|
200
|
+
The `+` over the slide box — or `/` typed on an empty line — opens the block
|
|
201
|
+
picker: the demo marker, a framed embed, a video with the attributes that make
|
|
202
|
+
it behave, two columns, a fenced block, the speaker page. The block lands where
|
|
203
|
+
the cursor was, with its first placeholder selected so it is ready to type over;
|
|
204
|
+
text already selected takes that placeholder's place, so picking **Fragment**
|
|
205
|
+
wraps the paragraph that was highlighted. A block that only works at the top of
|
|
206
|
+
a slide, like the demo marker, goes there whatever the cursor was doing.
|
|
207
|
+
|
|
208
|
+
`snippets:` in `slides.config.js` adds a project's own blocks to the list:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
snippets: [
|
|
212
|
+
{ label: 'Pricing table', hint: 'the three tiers', body: '<div class="tiers">${…}</div>' },
|
|
213
|
+
],
|
|
214
|
+
```
|
|
215
|
+
|
|
143
216
|
The drawer exists only under `fb-slides dev`: a built deck is static files and
|
|
144
217
|
never shows the button.
|
|
145
218
|
|
|
@@ -194,6 +267,7 @@ export default {
|
|
|
194
267
|
outDir: 'dist',
|
|
195
268
|
exclude: ['dist'], // extra path segments the build skips
|
|
196
269
|
fragmentLists: true, // bullet lists reveal one item at a time
|
|
270
|
+
snippets: [], // your own blocks in the edit drawer's picker
|
|
197
271
|
reveal: { transition: 'fade' },// passed to Reveal.initialize()
|
|
198
272
|
mermaid: {}, // passed to mermaid.initialize()
|
|
199
273
|
};
|
package/lib/config.mjs
CHANGED
|
@@ -80,6 +80,10 @@ export const loadConfig = async (root = process.cwd(), overrides = {}) => {
|
|
|
80
80
|
reveal: user.reveal ?? {},
|
|
81
81
|
// Bullet lists of 3+ items appear one item at a time.
|
|
82
82
|
fragmentLists: user.fragmentLists ?? true,
|
|
83
|
+
// The project's own entries in the edit drawer's block picker, after the
|
|
84
|
+
// built-in ones: `{ label, hint, body, top }`, where `${…}` in the body
|
|
85
|
+
// marks what has to be typed over.
|
|
86
|
+
snippets: user.snippets ?? [],
|
|
83
87
|
// The build output, at any depth: a framework demo's own dist/ is its dev
|
|
84
88
|
// server's business, and the deck ships that demo as sources.
|
|
85
89
|
exclude: [...ALWAYS_EXCLUDED, overrides.outDir ?? user.outDir ?? 'dist', ...(user.exclude ?? [])],
|
package/lib/render.mjs
CHANGED
|
@@ -46,6 +46,7 @@ export const renderIndex = async (config, runtimeDir) => {
|
|
|
46
46
|
demos: config.urls.demos,
|
|
47
47
|
reveal: config.reveal,
|
|
48
48
|
fragmentLists: config.fragmentLists,
|
|
49
|
+
snippets: config.snippets,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
// The chrome reads this to take its colours from the reveal theme instead of
|
package/package.json
CHANGED
package/runtime/annotate.js
CHANGED
|
@@ -477,7 +477,17 @@ const RevealAnnotate = () => ({
|
|
|
477
477
|
for (const [name, button] of Object.entries(toolButtons)) {
|
|
478
478
|
button.setAttribute('aria-pressed', String(name === tool));
|
|
479
479
|
}
|
|
480
|
-
|
|
480
|
+
// `is-armed` is shared with the other tool plugins, and the arming
|
|
481
|
+
// handshake fires mid-switch: the newcomer sets the class, then its
|
|
482
|
+
// event runs this module's stand-down — so a plain toggle with this
|
|
483
|
+
// module's own boolean would wipe it with a tool still armed. It is
|
|
484
|
+
// recomputed from the buttons instead, which every module keeps
|
|
485
|
+
// truthful. The edit chip stays out of it: its pressed state is the
|
|
486
|
+
// drawer, not an armed tool.
|
|
487
|
+
toolbar.classList.toggle(
|
|
488
|
+
'is-armed',
|
|
489
|
+
!!toolbar.querySelector('.deck-tool[aria-pressed="true"]:not(#tool-edit)'),
|
|
490
|
+
);
|
|
481
491
|
// The colours are only worth showing while there is a tool to apply them
|
|
482
492
|
// to, which makes arming one the gesture that opens them.
|
|
483
493
|
swatches.hidden = tool === null;
|
package/runtime/deck.js
CHANGED
|
@@ -294,6 +294,58 @@ for (const pre of document.querySelectorAll('.slides pre')) {
|
|
|
294
294
|
if (lines > 14) pre.classList.add('pre--dense');
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// The speaker page's photos cross-fade in DOM order (`.author-slide` in
|
|
299
|
+
// theme.base.css). Where each fade lands depends on how many photos there are
|
|
300
|
+
// and on the two timing tokens a project can retune — so the keyframes each
|
|
301
|
+
// strip needs are written here, instead of shipping a set for every count the
|
|
302
|
+
// theme might ever be handed.
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
const seconds = (value) => {
|
|
306
|
+
const time = Number.parseFloat(value);
|
|
307
|
+
if (!Number.isFinite(time)) return 0;
|
|
308
|
+
return value.trim().endsWith('ms') ? time / 1000 : time;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const strips = document.querySelectorAll('.slides .author-photo');
|
|
312
|
+
if (strips.length) {
|
|
313
|
+
const sheet = document.createElement('style');
|
|
314
|
+
document.head.append(sheet);
|
|
315
|
+
const written = new Set();
|
|
316
|
+
|
|
317
|
+
for (const strip of strips) {
|
|
318
|
+
const photos = [...strip.querySelectorAll('img')];
|
|
319
|
+
photos.forEach((photo, i) => photo.style.setProperty('--i', i));
|
|
320
|
+
// One photo has nothing to fade to, and the CSS already leaves it showing.
|
|
321
|
+
if (photos.length < 2) continue;
|
|
322
|
+
|
|
323
|
+
const css = getComputedStyle(strip);
|
|
324
|
+
const hold = seconds(css.getPropertyValue('--author-hold'));
|
|
325
|
+
const fade = seconds(css.getPropertyValue('--author-fade'));
|
|
326
|
+
const slot = hold + fade;
|
|
327
|
+
if (!slot) continue;
|
|
328
|
+
|
|
329
|
+
const total = photos.length * slot;
|
|
330
|
+
const at = (time) => `${((time / total) * 100).toFixed(3)}%`;
|
|
331
|
+
// Named for what it draws: two strips of the same length and timing — the
|
|
332
|
+
// ordinary case, since both come from the same tokens — share one set.
|
|
333
|
+
const name = `author-fade-${photos.length}-${Math.round(hold * 1000)}-${Math.round(fade * 1000)}`;
|
|
334
|
+
if (!written.has(name)) {
|
|
335
|
+
written.add(name);
|
|
336
|
+
sheet.sheet.insertRule(
|
|
337
|
+
`@keyframes ${name} {` +
|
|
338
|
+
`0% { opacity: 0 }` +
|
|
339
|
+
`${at(fade)}, ${at(slot)} { opacity: 1 }` +
|
|
340
|
+
`${at(slot + fade)}, 100% { opacity: 0 }` +
|
|
341
|
+
`}`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
strip.style.setProperty('--author-n', photos.length);
|
|
345
|
+
strip.style.setProperty('--author-anim', name);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
297
349
|
Reveal.sync();
|
|
298
350
|
|
|
299
351
|
// Which deck are we in? Shown in the corner, hidden on dividers.
|
package/runtime/edit.js
CHANGED
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
// slide's own range of the .md (the server owns that arithmetic) and reloads:
|
|
14
14
|
// the URL hash puts the deck back on this slide, and the full pipeline —
|
|
15
15
|
// mermaid, fragments, highlight steps — runs over the saved source.
|
|
16
|
+
//
|
|
17
|
+
// The `+` over the slide box — or `/` on an empty line — opens the block
|
|
18
|
+
// picker, which writes the markup nobody should have to remember (snippets.js).
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
|
|
21
|
+
import { createSnippets, PLUS_ICON } from './snippets.js';
|
|
22
|
+
|
|
18
23
|
// Where the slide came from, stamped on every section by deck.js: the .md file
|
|
19
24
|
// and the slide's index within it, counted the way the server counts.
|
|
20
25
|
const SRC = (slide) => {
|
|
@@ -116,19 +121,22 @@ const build = (deck) => {
|
|
|
116
121
|
panel.id = 'deck-edit';
|
|
117
122
|
panel.innerHTML = `
|
|
118
123
|
<header>
|
|
119
|
-
<
|
|
124
|
+
<button type="button" class="deck-edit-wide" aria-pressed="false"></button>
|
|
120
125
|
<code class="deck-edit-file"></code>
|
|
121
126
|
<button type="button" class="deck-edit-nav" data-go="-1" title="Previous slide" aria-label="Previous slide">${PREV_ICON}</button>
|
|
122
127
|
<button type="button" class="deck-edit-nav" data-go="1" title="Next slide" aria-label="Next slide">${NEXT_ICON}</button>
|
|
123
128
|
<button type="button" class="deck-edit-close" aria-label="Close">${CLOSE_ICON}</button>
|
|
124
129
|
</header>
|
|
125
|
-
<
|
|
130
|
+
<div class="deck-edit-row">
|
|
131
|
+
<label class="deck-edit-label" for="deck-edit-source">slide</label>
|
|
132
|
+
<button type="button" class="deck-edit-snip" title="Insert a block (/)" aria-label="Insert a block">${PLUS_ICON}</button>
|
|
133
|
+
</div>
|
|
126
134
|
<textarea id="deck-edit-source" spellcheck="false"></textarea>
|
|
127
135
|
<label class="deck-edit-label" for="deck-edit-notes">speaker notes</label>
|
|
128
136
|
<textarea id="deck-edit-notes" spellcheck="false"></textarea>
|
|
129
137
|
<footer>
|
|
130
|
-
<button type="button" class="deck-edit-add" title="Add a slide after this one">Add</button>
|
|
131
|
-
<button type="button" class="deck-edit-delete" title="Delete this slide">Delete</button>
|
|
138
|
+
<button type="button" class="deck-edit-add" title="Add a slide after this one">Add page</button>
|
|
139
|
+
<button type="button" class="deck-edit-delete" title="Delete this slide">Delete page</button>
|
|
132
140
|
<span class="deck-edit-status" role="status"></span>
|
|
133
141
|
<button type="button" class="deck-edit-save">Save</button>
|
|
134
142
|
</footer>
|
|
@@ -144,6 +152,7 @@ const build = (deck) => {
|
|
|
144
152
|
panel.inert = true;
|
|
145
153
|
|
|
146
154
|
const fileLabel = panel.querySelector('.deck-edit-file');
|
|
155
|
+
const wideButton = panel.querySelector('.deck-edit-wide');
|
|
147
156
|
const sourceArea = panel.querySelector('#deck-edit-source');
|
|
148
157
|
const notesArea = panel.querySelector('#deck-edit-notes');
|
|
149
158
|
const status = panel.querySelector('.deck-edit-status');
|
|
@@ -190,6 +199,13 @@ const build = (deck) => {
|
|
|
190
199
|
frame ||= requestAnimationFrame(paint);
|
|
191
200
|
};
|
|
192
201
|
|
|
202
|
+
// ---- the block picker ---------------------------------------------------
|
|
203
|
+
// Over the drawer, like the delete question: the deck keeps the stage, and
|
|
204
|
+
// what the picker writes goes through the same preview typing does.
|
|
205
|
+
|
|
206
|
+
const snippets = createSnippets({ area: sourceArea, paint: schedulePaint });
|
|
207
|
+
panel.append(snippets.element);
|
|
208
|
+
|
|
193
209
|
// ---- open and close ---------------------------------------------------
|
|
194
210
|
// The deck is narrowed by CSS while the drawer is out (see theme.base.css);
|
|
195
211
|
// reveal only rescales when told to, so layout() is driven for as long as
|
|
@@ -204,6 +220,29 @@ const build = (deck) => {
|
|
|
204
220
|
requestAnimationFrame(step);
|
|
205
221
|
};
|
|
206
222
|
|
|
223
|
+
// Half the page for a slide that is mostly markup, the narrow drawer for one
|
|
224
|
+
// that is mostly words. The width is a token, so saying it on the body is
|
|
225
|
+
// enough — the deck, the badge, the signature and the toolbar all step aside
|
|
226
|
+
// through the same `calc()` they already use. It survives the reload a save
|
|
227
|
+
// ends in, which is the only way it could be worth setting at all.
|
|
228
|
+
const WIDE_KEY = 'fb-slides.edit-wide';
|
|
229
|
+
|
|
230
|
+
const setWide = (wide) => {
|
|
231
|
+
document.body.classList.toggle('edit-wide', wide);
|
|
232
|
+
wideButton.setAttribute('aria-pressed', String(wide));
|
|
233
|
+
wideButton.innerHTML = wide ? NEXT_ICON : PREV_ICON;
|
|
234
|
+
const label = wide ? 'Narrow the drawer' : 'Widen the drawer to half the page';
|
|
235
|
+
wideButton.title = label;
|
|
236
|
+
wideButton.setAttribute('aria-label', label);
|
|
237
|
+
try {
|
|
238
|
+
if (wide) sessionStorage.setItem(WIDE_KEY, '1');
|
|
239
|
+
else sessionStorage.removeItem(WIDE_KEY);
|
|
240
|
+
} catch {
|
|
241
|
+
/* walled-off storage only costs the width after a reload */
|
|
242
|
+
}
|
|
243
|
+
reflow();
|
|
244
|
+
};
|
|
245
|
+
|
|
207
246
|
const setOpen = (open) => {
|
|
208
247
|
document.body.classList.toggle('edit-open', open);
|
|
209
248
|
button.setAttribute('aria-pressed', String(open));
|
|
@@ -216,6 +255,7 @@ const build = (deck) => {
|
|
|
216
255
|
const discard = () => {
|
|
217
256
|
cancelAnimationFrame(frame);
|
|
218
257
|
frame = 0;
|
|
258
|
+
snippets.close({ restore: false });
|
|
219
259
|
confirmBox.hidden = true;
|
|
220
260
|
editing.slide.innerHTML = editing.html;
|
|
221
261
|
// The original fragments are back; reveal counts them again.
|
|
@@ -230,7 +270,10 @@ const build = (deck) => {
|
|
|
230
270
|
deck.layout();
|
|
231
271
|
};
|
|
232
272
|
|
|
233
|
-
|
|
273
|
+
// `focus` names the box that gets the room and the cursor — navigation and
|
|
274
|
+
// save-reloads pass the one the drawer already had, so following the deck
|
|
275
|
+
// never folds the notes away under a writing hand.
|
|
276
|
+
const open = async (focus = 'source') => {
|
|
234
277
|
const slide = deck.getCurrentSlide();
|
|
235
278
|
const src = SRC(slide);
|
|
236
279
|
if (!src) return;
|
|
@@ -266,15 +309,16 @@ const build = (deck) => {
|
|
|
266
309
|
fileLabel.textContent = src.file;
|
|
267
310
|
sourceArea.value = content;
|
|
268
311
|
notesArea.value = notes;
|
|
312
|
+
snippets.reset();
|
|
269
313
|
status.textContent = '';
|
|
270
314
|
saveButton.disabled = false;
|
|
271
|
-
panel.dataset.focus =
|
|
315
|
+
panel.dataset.focus = focus;
|
|
272
316
|
|
|
273
317
|
// One pointer, one tool: the pen, spotlight and pointer stand down — and
|
|
274
318
|
// stand this drawer down in turn when one of them arms (see below).
|
|
275
319
|
document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'edit' }));
|
|
276
320
|
setOpen(true);
|
|
277
|
-
sourceArea.focus();
|
|
321
|
+
(focus === 'notes' ? notesArea : sourceArea).focus();
|
|
278
322
|
};
|
|
279
323
|
|
|
280
324
|
const toggle = () => (editing ? close() : open());
|
|
@@ -287,9 +331,11 @@ const build = (deck) => {
|
|
|
287
331
|
|
|
288
332
|
const REOPEN_KEY = 'fb-slides.edit-reopen';
|
|
289
333
|
|
|
334
|
+
// The flag's value is the box that was open, so the reload hands the room
|
|
335
|
+
// (and the cursor) back to it.
|
|
290
336
|
const reloadEditing = () => {
|
|
291
337
|
try {
|
|
292
|
-
sessionStorage.setItem(REOPEN_KEY, '
|
|
338
|
+
sessionStorage.setItem(REOPEN_KEY, panel.dataset.focus === 'notes' ? 'notes' : 'source');
|
|
293
339
|
} catch {
|
|
294
340
|
/* walled-off storage only costs the reopen */
|
|
295
341
|
}
|
|
@@ -348,6 +394,10 @@ const build = (deck) => {
|
|
|
348
394
|
|
|
349
395
|
button.addEventListener('click', toggle);
|
|
350
396
|
panel.querySelector('.deck-edit-close').addEventListener('click', close);
|
|
397
|
+
wideButton.addEventListener('click', () =>
|
|
398
|
+
setWide(!document.body.classList.contains('edit-wide')),
|
|
399
|
+
);
|
|
400
|
+
panel.querySelector('.deck-edit-snip').addEventListener('click', () => snippets.open());
|
|
351
401
|
saveButton.addEventListener('click', save);
|
|
352
402
|
sourceArea.addEventListener('input', schedulePaint);
|
|
353
403
|
|
|
@@ -400,8 +450,10 @@ const build = (deck) => {
|
|
|
400
450
|
'keydown',
|
|
401
451
|
(event) => {
|
|
402
452
|
if (event.key !== 'Escape' || !editing) return;
|
|
403
|
-
// One layer at a time:
|
|
404
|
-
|
|
453
|
+
// One layer at a time, innermost first: the picker, then a pending
|
|
454
|
+
// delete question, then the drawer itself.
|
|
455
|
+
if (snippets.isOpen()) snippets.close();
|
|
456
|
+
else if (confirmBox.hidden) close();
|
|
405
457
|
else confirmBox.hidden = true;
|
|
406
458
|
event.stopPropagation();
|
|
407
459
|
event.preventDefault();
|
|
@@ -416,13 +468,22 @@ const build = (deck) => {
|
|
|
416
468
|
if (event.detail !== 'edit') close();
|
|
417
469
|
});
|
|
418
470
|
|
|
471
|
+
let wideAtStart = false;
|
|
472
|
+
try {
|
|
473
|
+
wideAtStart = Boolean(sessionStorage.getItem(WIDE_KEY));
|
|
474
|
+
} catch {
|
|
475
|
+
/* walled-off storage: the drawer opens at its usual width */
|
|
476
|
+
}
|
|
477
|
+
setWide(wideAtStart);
|
|
478
|
+
|
|
419
479
|
// A write reloaded the page with the drawer out: pick the editing back up
|
|
420
480
|
// on whatever slide the hash landed on.
|
|
421
481
|
try {
|
|
422
|
-
|
|
482
|
+
const reopen = sessionStorage.getItem(REOPEN_KEY);
|
|
483
|
+
if (reopen) {
|
|
423
484
|
sessionStorage.removeItem(REOPEN_KEY);
|
|
424
|
-
if (deck.isReady?.()) open();
|
|
425
|
-
else deck.on('ready', open);
|
|
485
|
+
if (deck.isReady?.()) open(reopen);
|
|
486
|
+
else deck.on('ready', () => open(reopen));
|
|
426
487
|
}
|
|
427
488
|
} catch {
|
|
428
489
|
/* walled-off storage: the drawer just stays closed */
|
|
@@ -433,8 +494,9 @@ const build = (deck) => {
|
|
|
433
494
|
// overview is a different surface altogether, so there it does close.
|
|
434
495
|
deck.on('slidechanged', async () => {
|
|
435
496
|
if (!editing) return;
|
|
497
|
+
const focus = panel.dataset.focus;
|
|
436
498
|
discard();
|
|
437
|
-
await open();
|
|
499
|
+
await open(focus);
|
|
438
500
|
// The new slide would not load — a drawer left standing would lie about
|
|
439
501
|
// what it edits.
|
|
440
502
|
if (!editing) setOpen(false);
|
package/runtime/pointer.js
CHANGED
|
@@ -198,7 +198,15 @@ const RevealPointer = () => ({
|
|
|
198
198
|
const setPointer = (on) => {
|
|
199
199
|
armed = on;
|
|
200
200
|
button.setAttribute('aria-pressed', String(on));
|
|
201
|
-
|
|
201
|
+
// Recomputed from every module's button, not toggled with this one's
|
|
202
|
+
// boolean: the arming handshake runs the loser's stand-down after the
|
|
203
|
+
// winner set the class, and the last writer would close the bar with a
|
|
204
|
+
// tool still armed. The edit chip's pressed state is the drawer, so it
|
|
205
|
+
// does not count.
|
|
206
|
+
toolbar.classList.toggle(
|
|
207
|
+
'is-armed',
|
|
208
|
+
!!toolbar.querySelector('.deck-tool[aria-pressed="true"]:not(#tool-edit)'),
|
|
209
|
+
);
|
|
202
210
|
picker.hidden = !on;
|
|
203
211
|
applyCursor();
|
|
204
212
|
if (on) document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'pointer' }));
|