fb-slides 0.5.1 → 0.6.3
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 +118 -0
- package/README.md +99 -1
- package/lib/config.mjs +4 -0
- package/lib/render.mjs +1 -0
- package/package.json +2 -1
- package/runtime/annotate.js +11 -1
- package/runtime/deck.js +117 -1
- package/runtime/edit.js +76 -14
- package/runtime/outline.js +24 -5
- package/runtime/pointer.js +9 -1
- package/runtime/snippets.js +319 -0
- package/runtime/spotlight.js +9 -1
- package/runtime/theme.base.css +397 -30
- 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 +44 -6
- package/templates/starter/theme.css +0 -11
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/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' }));
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// The block picker — the `+` in the edit drawer, and `/` on an empty line.
|
|
3
|
+
//
|
|
4
|
+
// A snippet is the markup you would otherwise have to remember: the attributes
|
|
5
|
+
// a <video> needs to behave, the `min-width: 0` two columns fall over without,
|
|
6
|
+
// the comment that turns a slide into a live demo. Nothing here renders — the
|
|
7
|
+
// classes it reaches for are the slide vocabulary in theme.base.css.
|
|
8
|
+
//
|
|
9
|
+
// `${…}` in a body marks something to type over: the braces are dropped on the
|
|
10
|
+
// way in and the first one is left selected, so a snippet lands ready to fill.
|
|
11
|
+
// Text selected in the textarea takes the place of that first one — select a
|
|
12
|
+
// paragraph, pick Fragment, and the paragraph is what gets wrapped.
|
|
13
|
+
//
|
|
14
|
+
// `snippets:` in slides.config.js adds a project's own blocks to the list.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const CFG = window.__FB_SLIDES__ ?? {};
|
|
18
|
+
|
|
19
|
+
// `top: true` — the block only works as the first thing in a slide, so that is
|
|
20
|
+
// where it goes, wherever the cursor happens to be.
|
|
21
|
+
const BUILT_IN = [
|
|
22
|
+
{
|
|
23
|
+
label: 'Demo, full slide',
|
|
24
|
+
hint: 'a folder in demo/, running on the slide',
|
|
25
|
+
top: true,
|
|
26
|
+
body: '<!-- demo: ${counter} -->',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'External page, full slide',
|
|
30
|
+
hint: 'any URL, taking the whole slide',
|
|
31
|
+
top: true,
|
|
32
|
+
body: '<!-- demo: ${https://example.com/} -->',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'Framed embed',
|
|
36
|
+
hint: 'an iframe that shares the slide with the text',
|
|
37
|
+
body: '<div class="frame">\n <iframe src="${https://example.com/}"></iframe>\n</div>',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: 'Video',
|
|
41
|
+
hint: 'a clip from assets/, with the attributes that make it behave',
|
|
42
|
+
body:
|
|
43
|
+
'<video src="assets/${clip.mp4}" controls muted playsinline preload="metadata"\n' +
|
|
44
|
+
' style="width: 78%; aspect-ratio: 16 / 9;"></video>',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
label: 'Image',
|
|
48
|
+
hint: 'centred, no frame around it',
|
|
49
|
+
body: '',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'Two columns',
|
|
53
|
+
hint: 'code on one side, words on the other',
|
|
54
|
+
body:
|
|
55
|
+
'<div class="cols">\n<div class="col">\n\n${left}\n\n</div>\n' +
|
|
56
|
+
'<div class="col">\n\n${right}\n\n</div>\n</div>',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: 'Code',
|
|
60
|
+
hint: 'a fenced block, highlighted',
|
|
61
|
+
body: '```${ts}\n${code}\n```',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: 'Code, stepped',
|
|
65
|
+
hint: 'the block reveals itself a range of lines at a time',
|
|
66
|
+
body: '```${ts} [1-3|5-7|9]\n${code}\n```',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: 'Diagram',
|
|
70
|
+
hint: 'a mermaid graph, drawn at load',
|
|
71
|
+
body: '```mermaid\nflowchart LR\n A[${Client}] --> B[Server]\n```',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
label: 'Fragment',
|
|
75
|
+
hint: 'appears on the next press, on its own',
|
|
76
|
+
body: '<p class="fragment">${text}</p>',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: 'Callout',
|
|
80
|
+
hint: 'a bordered block — the point the slide comes back to',
|
|
81
|
+
body: '<div class="box">\n\n${text}\n\n</div>',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: 'Caption',
|
|
85
|
+
hint: 'a quiet line under a figure',
|
|
86
|
+
body: '<p class="caption">${text}</p>',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: 'Speaker page',
|
|
90
|
+
hint: 'photos down the left edge, who you are on the right',
|
|
91
|
+
top: true,
|
|
92
|
+
body:
|
|
93
|
+
'<!-- .slide: class="author-slide" -->\n\n' +
|
|
94
|
+
'<div class="author-photo">\n' +
|
|
95
|
+
' <img src="assets/author/${portrait.jpg}" alt="" />\n' +
|
|
96
|
+
' <img src="assets/author/on-stage.jpg" alt="" />\n' +
|
|
97
|
+
'</div>\n\n' +
|
|
98
|
+
'<div class="author-bio">\n' +
|
|
99
|
+
' <h1>Your name</h1>\n' +
|
|
100
|
+
' <ul>\n' +
|
|
101
|
+
' <li>What you do</li>\n' +
|
|
102
|
+
' <li>What you are known for</li>\n' +
|
|
103
|
+
' </ul>\n' +
|
|
104
|
+
' <p class="author-meta">The stack · you · work · with</p>\n' +
|
|
105
|
+
' <p class="author-meta">❤️ Life outside work — <strong>yoursite.dev</strong></p>\n' +
|
|
106
|
+
'</div>',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: 'Disabled',
|
|
110
|
+
hint: 'keep the slide, leave it out of the talk',
|
|
111
|
+
top: true,
|
|
112
|
+
body: '<!-- disabled -->',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: 'Slide class',
|
|
116
|
+
hint: 'hand this slide a class of your own',
|
|
117
|
+
top: true,
|
|
118
|
+
body: '<!-- .slide: class="${name}" -->',
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// The project's own blocks land after the built-in ones. A snippet without a
|
|
123
|
+
// body would be an entry that inserts nothing, so it never reaches the list.
|
|
124
|
+
const catalog = () => [
|
|
125
|
+
...BUILT_IN,
|
|
126
|
+
...(Array.isArray(CFG.snippets) ? CFG.snippets : []).filter(
|
|
127
|
+
(snippet) => snippet && typeof snippet.body === 'string',
|
|
128
|
+
),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const PLACEHOLDER = /\$\{([^}]*)\}/g;
|
|
132
|
+
|
|
133
|
+
// Unwrap every `${…}` to the word inside it, and hand back where the first one
|
|
134
|
+
// ended up — that is the range the cursor lands on. `taken` is the text the
|
|
135
|
+
// editor had selected, which stands in for that first placeholder.
|
|
136
|
+
const fill = (body, taken) => {
|
|
137
|
+
let text = '';
|
|
138
|
+
let range = null;
|
|
139
|
+
let read = 0;
|
|
140
|
+
for (const match of body.matchAll(PLACEHOLDER)) {
|
|
141
|
+
const word = range === null && taken ? taken : match[1];
|
|
142
|
+
text += body.slice(read, match.index) + word;
|
|
143
|
+
range ??= [text.length - word.length, text.length];
|
|
144
|
+
read = match.index + match[0].length;
|
|
145
|
+
}
|
|
146
|
+
return { text: text + body.slice(read), range };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// A block wants a blank line around it, and exactly one: pasting into the
|
|
150
|
+
// middle of a paragraph should not glue the markup onto a sentence, and
|
|
151
|
+
// pasting into a gap should not open a second one.
|
|
152
|
+
const gap = (text, atEnd) => {
|
|
153
|
+
if (!text) return '';
|
|
154
|
+
const touching = atEnd ? /\n\n$/.test(text) : /^\n\n/.test(text);
|
|
155
|
+
if (touching) return '';
|
|
156
|
+
return (atEnd ? /\n$/.test(text) : /^\n/.test(text)) ? '\n' : '\n\n';
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const PLUS_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"
|
|
160
|
+
stroke-linecap="round" aria-hidden="true">
|
|
161
|
+
<path d="M12 6v12" /><path d="M6 12h12" />
|
|
162
|
+
</svg>`;
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// The picker itself. `area` is the slide's textarea; `paint` re-renders the
|
|
166
|
+
// slide the way typing into it does.
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
export const createSnippets = ({ area, paint }) => {
|
|
170
|
+
const element = document.createElement('div');
|
|
171
|
+
element.className = 'deck-snip';
|
|
172
|
+
element.hidden = true;
|
|
173
|
+
element.innerHTML = `
|
|
174
|
+
<input class="deck-snip-filter" type="text" spellcheck="false" autocomplete="off"
|
|
175
|
+
placeholder="Insert a block…" aria-label="Find a block" />
|
|
176
|
+
<ul class="deck-snip-list" role="listbox"></ul>
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
const filter = element.querySelector('.deck-snip-filter');
|
|
180
|
+
const list = element.querySelector('.deck-snip-list');
|
|
181
|
+
|
|
182
|
+
const blocks = catalog();
|
|
183
|
+
list.innerHTML = blocks
|
|
184
|
+
.map(
|
|
185
|
+
(snippet, index) =>
|
|
186
|
+
`<li role="option" data-index="${index}"><span>${snippet.label}</span>` +
|
|
187
|
+
`<small>${snippet.hint ?? ''}</small></li>`,
|
|
188
|
+
)
|
|
189
|
+
.join('');
|
|
190
|
+
const items = [...list.children];
|
|
191
|
+
|
|
192
|
+
// Where the block goes, decided when the picker opens: the cursor at that
|
|
193
|
+
// moment, or the `/` that summoned it. `take` says whether what that range
|
|
194
|
+
// covers is the author's own text, to be threaded through the snippet.
|
|
195
|
+
let target = null;
|
|
196
|
+
|
|
197
|
+
// The cursor as it was when the textarea last had it. A textarea that has
|
|
198
|
+
// never been clicked reports 0, which would drop every block at the top of
|
|
199
|
+
// the slide instead of at the end of what is written.
|
|
200
|
+
let caret = [0, 0];
|
|
201
|
+
const remember = () => {
|
|
202
|
+
caret = [area.selectionStart, area.selectionEnd];
|
|
203
|
+
};
|
|
204
|
+
for (const event of ['keyup', 'click', 'select', 'blur']) area.addEventListener(event, remember);
|
|
205
|
+
|
|
206
|
+
// A new slide in the drawer: the old cursor belonged to the old text.
|
|
207
|
+
const reset = () => {
|
|
208
|
+
caret = [area.value.length, area.value.length];
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const current = () => list.querySelector('.is-current');
|
|
212
|
+
|
|
213
|
+
const highlight = (item) => {
|
|
214
|
+
current()?.classList.remove('is-current');
|
|
215
|
+
item?.classList.add('is-current');
|
|
216
|
+
item?.scrollIntoView({ block: 'nearest' });
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const apply = () => {
|
|
220
|
+
const shown = items.filter((item) => !item.hidden);
|
|
221
|
+
if (!shown.length) return;
|
|
222
|
+
highlight(shown.includes(current()) ? current() : shown[0]);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const search = () => {
|
|
226
|
+
const query = filter.value.trim().toLowerCase();
|
|
227
|
+
for (const item of items) {
|
|
228
|
+
const snippet = blocks[Number(item.dataset.index)];
|
|
229
|
+
item.hidden = Boolean(query) && !`${snippet.label} ${snippet.hint ?? ''}`.toLowerCase().includes(query);
|
|
230
|
+
}
|
|
231
|
+
apply();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const close = ({ restore = true } = {}) => {
|
|
235
|
+
if (element.hidden) return;
|
|
236
|
+
element.hidden = true;
|
|
237
|
+
target = null;
|
|
238
|
+
if (!restore) return;
|
|
239
|
+
area.focus();
|
|
240
|
+
area.setSelectionRange(caret[0], caret[1]);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const open = (where) => {
|
|
244
|
+
target = where ?? {
|
|
245
|
+
// Live if the cursor is still in the box, remembered if it moved on.
|
|
246
|
+
...(document.activeElement === area
|
|
247
|
+
? { from: area.selectionStart, to: area.selectionEnd }
|
|
248
|
+
: { from: caret[0], to: caret[1] }),
|
|
249
|
+
take: true,
|
|
250
|
+
};
|
|
251
|
+
filter.value = '';
|
|
252
|
+
search();
|
|
253
|
+
element.hidden = false;
|
|
254
|
+
filter.focus();
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const insert = (snippet) => {
|
|
258
|
+
const { from, to, take } = target ?? { from: area.value.length, to: area.value.length };
|
|
259
|
+
// A block that only works at the top of a slide goes there whatever the
|
|
260
|
+
// cursor was doing, and never swallows a selection on the way.
|
|
261
|
+
const at = snippet.top ? { from: 0, to: 0, take: false } : { from, to, take };
|
|
262
|
+
|
|
263
|
+
const before = area.value.slice(0, at.from);
|
|
264
|
+
const after = area.value.slice(at.to);
|
|
265
|
+
const { text, range } = fill(snippet.body, at.take ? area.value.slice(at.from, at.to) : '');
|
|
266
|
+
|
|
267
|
+
const lead = gap(before, true);
|
|
268
|
+
const trail = gap(after, false);
|
|
269
|
+
area.value = before + lead + text + trail + after;
|
|
270
|
+
|
|
271
|
+
const start = before.length + lead.length;
|
|
272
|
+
close({ restore: false });
|
|
273
|
+
area.focus();
|
|
274
|
+
if (range) area.setSelectionRange(start + range[0], start + range[1]);
|
|
275
|
+
else area.setSelectionRange(start + text.length, start + text.length);
|
|
276
|
+
remember();
|
|
277
|
+
paint();
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ---- wiring -------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
filter.addEventListener('input', search);
|
|
283
|
+
|
|
284
|
+
filter.addEventListener('keydown', (event) => {
|
|
285
|
+
const shown = items.filter((item) => !item.hidden);
|
|
286
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
const step = event.key === 'ArrowDown' ? 1 : -1;
|
|
289
|
+
const index = shown.indexOf(current());
|
|
290
|
+
highlight(shown[(index + step + shown.length) % shown.length]);
|
|
291
|
+
} else if (event.key === 'Enter') {
|
|
292
|
+
event.preventDefault();
|
|
293
|
+
const item = current();
|
|
294
|
+
if (item) insert(blocks[Number(item.dataset.index)]);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
list.addEventListener('click', (event) => {
|
|
299
|
+
const item = event.target.closest('li');
|
|
300
|
+
if (item) insert(blocks[Number(item.dataset.index)]);
|
|
301
|
+
});
|
|
302
|
+
list.addEventListener('mousemove', (event) => {
|
|
303
|
+
const item = event.target.closest('li');
|
|
304
|
+
if (item && item !== current()) highlight(item);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// `/` on an empty line opens the same list without leaving the keyboard, and
|
|
308
|
+
// the block takes the place of the slash.
|
|
309
|
+
area.addEventListener('input', (event) => {
|
|
310
|
+
if (event.inputType !== 'insertText' || event.data !== '/') return;
|
|
311
|
+
const at = area.selectionStart - 1;
|
|
312
|
+
if (at > 0 && area.value[at - 1] !== '\n') return;
|
|
313
|
+
open({ from: at, to: at + 1, take: false });
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return { element, open, close, isOpen: () => !element.hidden, reset };
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export default createSnippets;
|
package/runtime/spotlight.js
CHANGED
|
@@ -142,7 +142,15 @@ const RevealSpotlight = () => ({
|
|
|
142
142
|
const setSpot = (on) => {
|
|
143
143
|
armed = on;
|
|
144
144
|
button.setAttribute('aria-pressed', String(on));
|
|
145
|
-
|
|
145
|
+
// Recomputed from every module's button, not toggled with this one's
|
|
146
|
+
// boolean: the arming handshake runs the loser's stand-down after the
|
|
147
|
+
// winner set the class, and the last writer would close the bar with a
|
|
148
|
+
// tool still armed. The edit chip's pressed state is the drawer, so it
|
|
149
|
+
// does not count.
|
|
150
|
+
toolbar.classList.toggle(
|
|
151
|
+
'is-armed',
|
|
152
|
+
!!toolbar.querySelector('.deck-tool[aria-pressed="true"]:not(#tool-edit)'),
|
|
153
|
+
);
|
|
146
154
|
// Arming must start from a whole veil: a hole still mid-fade from last
|
|
147
155
|
// time would leak the pointer through (see clear above).
|
|
148
156
|
if (on) veil.style.clipPath = '';
|