fb-slides 0.9.0 → 0.9.1
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 +10 -0
- package/README.md +7 -4
- package/lib/assets.mjs +29 -3
- package/package.json +1 -1
- package/runtime/assets.js +105 -62
- package/runtime/theme.base.css +14 -1
- package/templates/starter/assets/screen_2026_08_05_00_24_16.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ this — which makes it worth being able to see what arrived.
|
|
|
7
7
|
Entries are written as the work lands, under **Unreleased**; the release commit
|
|
8
8
|
that stamps the version renames that heading to the version and its date.
|
|
9
9
|
|
|
10
|
+
## 0.9.1 — 2026-09-08
|
|
11
|
+
|
|
12
|
+
- **The assets panel shows the folder, not just what you dropped.** The list
|
|
13
|
+
under the folder picker is now that folder as it is on disk — the screenshot
|
|
14
|
+
from a moment ago and the one from last month, side by side — so an asset
|
|
15
|
+
that was already there can be copied, written into the slide, or deleted
|
|
16
|
+
without going to the Finder for it. Picking another folder lists that one,
|
|
17
|
+
what this session put there is tinted, and a file under a kilobyte is counted
|
|
18
|
+
in bytes rather than rounding to `0 KB`.
|
|
19
|
+
|
|
10
20
|
## 0.9.0 — 2026-09-08
|
|
11
21
|
|
|
12
22
|
- **The editor answers `Shift+E`, not `T`.** The drawer's shortcut moved off
|
package/README.md
CHANGED
|
@@ -273,10 +273,13 @@ files are saved there; drop on **+ new folder…** and it asks for a name and
|
|
|
273
273
|
makes it. The button next to the `+` opens the same panel for files you would
|
|
274
274
|
rather pick than drag.
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
The list under the folder picker is that folder, as it is on disk: what was just
|
|
277
|
+
dropped and what has been there since last month. Each row carries the path to
|
|
278
|
+
write in the slide, a button that copies it, and one that writes it for you at
|
|
279
|
+
the cursor: `` for an image, a `<video>` with the attributes that make it
|
|
280
|
+
behave for a clip, an `<audio>` for sound, a link for everything else. Picking
|
|
281
|
+
another folder lists that one; what this session put there is tinted, so it
|
|
282
|
+
stands out among files that were already around. Names are folded to something
|
|
280
283
|
a URL can carry — `Schermata città (1).PNG` becomes `Schermata-citta-1.png` —
|
|
281
284
|
and nothing is ever overwritten: a second `clip.mp4` lands as `clip-2.mp4`.
|
|
282
285
|
|
package/lib/assets.mjs
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// and putting the picture it points at next to it are the same job, so the
|
|
7
7
|
// drawer that does one does the other.
|
|
8
8
|
//
|
|
9
|
-
// GET /api/assets
|
|
10
|
-
//
|
|
9
|
+
// GET /api/assets?dir=x → { dir, dirs, at, files } — where files land, the
|
|
10
|
+
// subfolders to aim at, and what is in the one asked for
|
|
11
11
|
// POST /api/assets → the file's own bytes, its name and folder riding in
|
|
12
12
|
// headers (a filename is not latin-1, hence the encoding);
|
|
13
13
|
// answers { path } — what the slide should say
|
|
@@ -103,6 +103,23 @@ const freeName = async (dir, name) => {
|
|
|
103
103
|
return null;
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
+
// What is in one folder, as rows the drawer can list: files only, no dotfiles,
|
|
107
|
+
// each carrying the path a slide would write.
|
|
108
|
+
const filesIn = async (path, dir, relative) => {
|
|
109
|
+
const entries = await readdir(path, { withFileTypes: true }).catch(() => []);
|
|
110
|
+
const found = [];
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (!entry.isFile() || entry.name.startsWith('.')) continue;
|
|
113
|
+
const info = await stat(join(path, entry.name)).catch(() => null);
|
|
114
|
+
found.push({
|
|
115
|
+
name: entry.name,
|
|
116
|
+
size: info?.size ?? 0,
|
|
117
|
+
path: [dir, relative, entry.name].filter(Boolean).join('/'),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return found.sort((a, b) => a.name.localeCompare(b.name, 'en', { numeric: true }));
|
|
121
|
+
};
|
|
122
|
+
|
|
106
123
|
const subfolders = async (root) => {
|
|
107
124
|
const walk = async (dir, prefix, left) => {
|
|
108
125
|
if (!left) return [];
|
|
@@ -157,7 +174,16 @@ const receive = async (req, target) => {
|
|
|
157
174
|
// where that is on disk.
|
|
158
175
|
export const createAssetApi = ({ dir, path: root }) => async (req, res) => {
|
|
159
176
|
if (req.method === 'GET') {
|
|
160
|
-
|
|
177
|
+
// `dir` is the folder the drawer is showing — the root when it says nothing,
|
|
178
|
+
// and nothing at all while a new folder is being named.
|
|
179
|
+
const asked = new URL(req.url, 'http://localhost').searchParams.get('dir') ?? '';
|
|
180
|
+
const at = folderIn(root, asked);
|
|
181
|
+
return json(res, 200, {
|
|
182
|
+
dir,
|
|
183
|
+
dirs: await subfolders(root).catch(() => []),
|
|
184
|
+
at: at?.relative ?? '',
|
|
185
|
+
files: at ? await filesIn(at.path, dir, at.relative) : [],
|
|
186
|
+
});
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
if (req.method !== 'POST' && req.method !== 'DELETE') {
|
package/package.json
CHANGED
package/runtime/assets.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// is created if it is new — and the drawer lists what landed, each row a path
|
|
8
8
|
// with two buttons: copy it, or write the markup for it into the slide.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
10
|
+
// The list under the folder picker is that folder, as it is on disk: what was
|
|
11
|
+
// just dropped and what has been there since last month, each row a path to
|
|
12
|
+
// copy, to write into the slide, or to delete. The paths are relative, so they
|
|
13
|
+
// work in dev and in the built deck alike. Deleting asks for the word to be
|
|
14
|
+
// typed — it is the only thing here a reload cannot undo.
|
|
15
15
|
//
|
|
16
16
|
// Only the dev server answers /api/assets, so on a static host the sheet never
|
|
17
17
|
// appears — the drawer it belongs to is not there either.
|
|
@@ -76,7 +76,13 @@ const TRASH_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" s
|
|
|
76
76
|
|
|
77
77
|
const hasFiles = (event) => [...(event.dataTransfer?.types ?? [])].includes('Files');
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
// A 511-byte SVG rounding to `0 KB` is a row that looks like a broken file.
|
|
80
|
+
const bytes = (size) =>
|
|
81
|
+
size < 1024
|
|
82
|
+
? `${size} B`
|
|
83
|
+
: size < 1024 * 1024
|
|
84
|
+
? `${Math.round(size / 1024)} KB`
|
|
85
|
+
: `${(size / 1048576).toFixed(1)} MB`;
|
|
80
86
|
|
|
81
87
|
// One file, streamed. XHR rather than fetch for the one thing fetch cannot do:
|
|
82
88
|
// tell you how far a 60 MB screen recording has got.
|
|
@@ -181,7 +187,10 @@ export const createAssets = ({ paste, active }) => {
|
|
|
181
187
|
let dirs = []; // its subfolders, as the server last listed them
|
|
182
188
|
let chosen = ''; // where the next drop goes, remembered between drops
|
|
183
189
|
let pending = null; // files waiting for a folder that has yet to be named
|
|
184
|
-
let asking = null; //
|
|
190
|
+
let asking = null; // the path the delete question is standing over
|
|
191
|
+
let files = []; // the chosen folder, as the server last listed it
|
|
192
|
+
let failures = []; // uploads that did not land, kept under the listing
|
|
193
|
+
const fresh = new Set(); // what this session put there — worth pointing at
|
|
185
194
|
|
|
186
195
|
// The root, what the server last listed, and — however that listing raced
|
|
187
196
|
// with the upload that made it — the folder this drop actually went to.
|
|
@@ -213,21 +222,28 @@ export const createAssets = ({ paste, active }) => {
|
|
|
213
222
|
targets.insertAdjacentHTML('beforeend', `<li data-dir="${NEW}" class="is-new">+ new folder…</li>`);
|
|
214
223
|
};
|
|
215
224
|
|
|
216
|
-
// The folder
|
|
217
|
-
//
|
|
225
|
+
// The folder as it is right now — its subfolders to aim at, and its files to
|
|
226
|
+
// list. Called on open, on picking another folder, and after every write.
|
|
218
227
|
const refresh = async () => {
|
|
228
|
+
const where = pending ? '' : chosen;
|
|
219
229
|
try {
|
|
220
|
-
const response = await fetch(
|
|
230
|
+
const response = await fetch(`/api/assets?dir=${encodeURIComponent(where)}`, {
|
|
231
|
+
headers: API_HEADERS,
|
|
232
|
+
cache: 'no-cache',
|
|
233
|
+
});
|
|
221
234
|
if (response.ok) {
|
|
222
235
|
const answer = await response.json();
|
|
223
236
|
root = answer.dir ?? root;
|
|
224
237
|
dirs = Array.isArray(answer.dirs) ? answer.dirs : [];
|
|
238
|
+
files = Array.isArray(answer.files) ? answer.files : [];
|
|
225
239
|
}
|
|
226
240
|
} catch {
|
|
227
241
|
/* no listing: assets/ itself is always there to drop into */
|
|
242
|
+
files = [];
|
|
228
243
|
}
|
|
229
244
|
drawSelect();
|
|
230
245
|
drawTargets();
|
|
246
|
+
drawFiles();
|
|
231
247
|
};
|
|
232
248
|
|
|
233
249
|
// ---- open and close ---------------------------------------------------
|
|
@@ -269,6 +285,60 @@ export const createAssets = ({ paste, active }) => {
|
|
|
269
285
|
}
|
|
270
286
|
};
|
|
271
287
|
|
|
288
|
+
// The folder, one row per file: the path, and the three things worth doing
|
|
289
|
+
// with it. Rebuilt whole on every refresh — a listing is cheap, and the
|
|
290
|
+
// alternative is keeping rows in step with a folder two other things write to.
|
|
291
|
+
const drawFiles = () => {
|
|
292
|
+
list.innerHTML = '';
|
|
293
|
+
|
|
294
|
+
if (!files.length && !failures.length) {
|
|
295
|
+
const empty = document.createElement('li');
|
|
296
|
+
empty.className = 'deck-assets-none';
|
|
297
|
+
empty.textContent = pending ? 'The folder will be made when you name it.' : 'Nothing in this folder yet.';
|
|
298
|
+
list.append(empty);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const file of files) {
|
|
302
|
+
const row = document.createElement('li');
|
|
303
|
+
if (fresh.has(file.path)) row.classList.add('is-fresh');
|
|
304
|
+
row.innerHTML =
|
|
305
|
+
`<code></code>` +
|
|
306
|
+
`<span class="deck-assets-size"></span>` +
|
|
307
|
+
`<span class="deck-assets-acts">` +
|
|
308
|
+
`<button type="button" class="deck-assets-copy" title="Copy the path">${COPY_ICON}</button>` +
|
|
309
|
+
`<button type="button" class="deck-assets-insert" title="Write it into the slide">${INSERT_ICON}</button>` +
|
|
310
|
+
`<button type="button" class="deck-assets-trash" title="Delete the file">${TRASH_ICON}</button>` +
|
|
311
|
+
`</span>`;
|
|
312
|
+
row.querySelector('code').textContent = file.path;
|
|
313
|
+
// The drawer is narrow and a path can be long: the row truncates, and
|
|
314
|
+
// hovering it says the whole thing.
|
|
315
|
+
row.title = file.path;
|
|
316
|
+
row.querySelector('.deck-assets-size').textContent = bytes(file.size);
|
|
317
|
+
row.querySelector('.deck-assets-copy').addEventListener('click', (event) =>
|
|
318
|
+
copy(file.path, event.currentTarget),
|
|
319
|
+
);
|
|
320
|
+
row.querySelector('.deck-assets-insert').addEventListener('click', () => {
|
|
321
|
+
paste(blockFor(file.path, file.name));
|
|
322
|
+
close();
|
|
323
|
+
});
|
|
324
|
+
row.querySelector('.deck-assets-trash').addEventListener('click', () => askDelete(file.path));
|
|
325
|
+
list.append(row);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// What did not land stays under the listing rather than vanishing with the
|
|
329
|
+
// refresh that follows the upload.
|
|
330
|
+
for (const failure of failures) {
|
|
331
|
+
const row = document.createElement('li');
|
|
332
|
+
row.className = 'is-failed';
|
|
333
|
+
row.innerHTML = `<span class="deck-assets-file"></span><span class="deck-assets-pct"></span>`;
|
|
334
|
+
row.querySelector('.deck-assets-file').textContent = failure.name;
|
|
335
|
+
row.querySelector('.deck-assets-pct').textContent = failure.message;
|
|
336
|
+
list.append(row);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// While an upload is in flight it has a row of its own, under the listing,
|
|
341
|
+
// counting up. The refresh that follows replaces it with the real thing.
|
|
272
342
|
const addRow = (file) => {
|
|
273
343
|
const row = document.createElement('li');
|
|
274
344
|
row.innerHTML = `<span class="deck-assets-file"></span><span class="deck-assets-pct"></span>`;
|
|
@@ -277,64 +347,37 @@ export const createAssets = ({ paste, active }) => {
|
|
|
277
347
|
pct.textContent = bytes(file.size);
|
|
278
348
|
list.append(row);
|
|
279
349
|
row.scrollIntoView({ block: 'nearest' });
|
|
280
|
-
|
|
281
350
|
return {
|
|
282
351
|
progress: (share) => {
|
|
283
352
|
pct.textContent = `${Math.round(share * 100)}%`;
|
|
284
353
|
},
|
|
285
|
-
failed: (message) => {
|
|
286
|
-
row.classList.add('is-failed');
|
|
287
|
-
pct.textContent = message;
|
|
288
|
-
},
|
|
289
|
-
done: ({ path, name }) => {
|
|
290
|
-
row.classList.add('is-done');
|
|
291
|
-
row.innerHTML =
|
|
292
|
-
`<code></code>` +
|
|
293
|
-
`<span class="deck-assets-acts">` +
|
|
294
|
-
`<button type="button" class="deck-assets-copy" title="Copy the path">${COPY_ICON}</button>` +
|
|
295
|
-
`<button type="button" class="deck-assets-insert" title="Write it into the slide">${INSERT_ICON}</button>` +
|
|
296
|
-
`<button type="button" class="deck-assets-trash" title="Delete the file">${TRASH_ICON}</button>` +
|
|
297
|
-
`</span>`;
|
|
298
|
-
row.querySelector('code').textContent = path;
|
|
299
|
-
row.querySelector('.deck-assets-copy').addEventListener('click', (event) =>
|
|
300
|
-
copy(path, event.currentTarget),
|
|
301
|
-
);
|
|
302
|
-
row.querySelector('.deck-assets-insert').addEventListener('click', () => {
|
|
303
|
-
paste(blockFor(path, name));
|
|
304
|
-
close();
|
|
305
|
-
});
|
|
306
|
-
row.querySelector('.deck-assets-trash').addEventListener('click', () => askDelete(path, row));
|
|
307
|
-
},
|
|
308
354
|
};
|
|
309
355
|
};
|
|
310
356
|
|
|
311
357
|
// One at a time: a folder of screenshots dropped at once should not open ten
|
|
312
358
|
// sockets, and the rows then fill in the order they were dropped.
|
|
313
|
-
const send = async (
|
|
314
|
-
if (!
|
|
359
|
+
const send = async (dropped, dir) => {
|
|
360
|
+
if (!dropped.length) return;
|
|
315
361
|
chosen = dir;
|
|
362
|
+
failures = [];
|
|
316
363
|
open();
|
|
317
|
-
for (const file of
|
|
364
|
+
for (const file of dropped) {
|
|
318
365
|
const row = addRow(file);
|
|
319
366
|
try {
|
|
320
367
|
const answer = await upload(file, dir, row.progress);
|
|
321
|
-
row.done(answer);
|
|
322
368
|
// The folder as the server spells it — `talk 2026` was typed, and what
|
|
323
369
|
// exists on disk is `talk-2026`. The list should say the latter.
|
|
324
370
|
chosen = answer.dir;
|
|
325
|
-
|
|
326
|
-
dirs = [...dirs, answer.dir].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
|
|
327
|
-
}
|
|
328
|
-
drawSelect();
|
|
329
|
-
drawTargets();
|
|
371
|
+
fresh.add(answer.path);
|
|
330
372
|
} catch (error) {
|
|
331
|
-
|
|
373
|
+
failures.push({ name: file.name, message: error.message });
|
|
332
374
|
}
|
|
333
375
|
}
|
|
376
|
+
await refresh();
|
|
334
377
|
};
|
|
335
378
|
|
|
336
|
-
const askDelete = (path
|
|
337
|
-
asking =
|
|
379
|
+
const askDelete = (path) => {
|
|
380
|
+
asking = path;
|
|
338
381
|
confirmBox.querySelector('code').textContent = path;
|
|
339
382
|
wordInput.value = '';
|
|
340
383
|
confirmButton.disabled = true;
|
|
@@ -344,7 +387,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
344
387
|
|
|
345
388
|
const doDelete = async () => {
|
|
346
389
|
if (!asking || confirmButton.disabled) return;
|
|
347
|
-
const
|
|
390
|
+
const path = asking;
|
|
348
391
|
confirmButton.disabled = true;
|
|
349
392
|
try {
|
|
350
393
|
const response = await fetch(`/api/assets?path=${encodeURIComponent(path)}`, {
|
|
@@ -352,10 +395,8 @@ export const createAssets = ({ paste, active }) => {
|
|
|
352
395
|
headers: API_HEADERS,
|
|
353
396
|
});
|
|
354
397
|
if (!response.ok) throw new Error((await response.json()).error ?? response.status);
|
|
355
|
-
|
|
398
|
+
fresh.delete(path);
|
|
356
399
|
closeConfirm();
|
|
357
|
-
// The folder may have been the file's only reason to exist; the list says
|
|
358
|
-
// what is there now either way.
|
|
359
400
|
refresh();
|
|
360
401
|
} catch (error) {
|
|
361
402
|
confirmBox.querySelector('code').textContent = error.message;
|
|
@@ -364,14 +405,14 @@ export const createAssets = ({ paste, active }) => {
|
|
|
364
405
|
};
|
|
365
406
|
|
|
366
407
|
// A drop that named no folder yet: hold the files, ask, and send on the answer.
|
|
367
|
-
const askName = (
|
|
368
|
-
pending =
|
|
408
|
+
const askName = (dropped) => {
|
|
409
|
+
pending = dropped;
|
|
369
410
|
open();
|
|
370
411
|
drawSelect();
|
|
371
412
|
nameRow.hidden = false;
|
|
372
413
|
goButton.hidden = false;
|
|
373
414
|
waiting.hidden = false;
|
|
374
|
-
waiting.textContent = `${
|
|
415
|
+
waiting.textContent = `${dropped.length} file${dropped.length === 1 ? '' : 's'} waiting for a folder name.`;
|
|
375
416
|
nameInput.focus();
|
|
376
417
|
nameInput.select();
|
|
377
418
|
};
|
|
@@ -379,23 +420,23 @@ export const createAssets = ({ paste, active }) => {
|
|
|
379
420
|
const confirmName = () => {
|
|
380
421
|
const dir = nameInput.value.trim().replace(/^\/+|\/+$/g, '');
|
|
381
422
|
if (!dir) return nameInput.focus();
|
|
382
|
-
const
|
|
423
|
+
const held = pending;
|
|
383
424
|
pending = null;
|
|
384
425
|
nameInput.value = '';
|
|
385
426
|
nameRow.hidden = true;
|
|
386
427
|
goButton.hidden = true;
|
|
387
428
|
waiting.hidden = true;
|
|
388
|
-
if (
|
|
429
|
+
if (held) send(held, dir);
|
|
389
430
|
else {
|
|
390
431
|
chosen = dir;
|
|
391
|
-
|
|
432
|
+
refresh();
|
|
392
433
|
}
|
|
393
434
|
};
|
|
394
435
|
|
|
395
|
-
const drop = (
|
|
396
|
-
if (!
|
|
397
|
-
if (dir === NEW) askName(
|
|
398
|
-
else send(
|
|
436
|
+
const drop = (dropped, dir) => {
|
|
437
|
+
if (!dropped.length) return;
|
|
438
|
+
if (dir === NEW) askName(dropped);
|
|
439
|
+
else send(dropped, dir);
|
|
399
440
|
};
|
|
400
441
|
|
|
401
442
|
// ---- wiring: the panel -------------------------------------------------
|
|
@@ -410,6 +451,8 @@ export const createAssets = ({ paste, active }) => {
|
|
|
410
451
|
}
|
|
411
452
|
nameRow.hidden = true;
|
|
412
453
|
chosen = select.value;
|
|
454
|
+
failures = [];
|
|
455
|
+
refresh();
|
|
413
456
|
});
|
|
414
457
|
|
|
415
458
|
wordInput.addEventListener('input', () => {
|
|
@@ -435,9 +478,9 @@ export const createAssets = ({ paste, active }) => {
|
|
|
435
478
|
fileInput.click();
|
|
436
479
|
});
|
|
437
480
|
fileInput.addEventListener('change', () => {
|
|
438
|
-
const
|
|
481
|
+
const picked = [...fileInput.files];
|
|
439
482
|
fileInput.value = '';
|
|
440
|
-
drop(
|
|
483
|
+
drop(picked, select.value === NEW ? nameInput.value.trim() : select.value);
|
|
441
484
|
});
|
|
442
485
|
|
|
443
486
|
// ---- wiring: the sheet -------------------------------------------------
|
package/runtime/theme.base.css
CHANGED
|
@@ -1691,11 +1691,24 @@ body.edit-open #deck-edit { transform: none; }
|
|
|
1691
1691
|
text-overflow: ellipsis;
|
|
1692
1692
|
white-space: nowrap;
|
|
1693
1693
|
}
|
|
1694
|
-
.deck-assets-pct
|
|
1694
|
+
.deck-assets-pct,
|
|
1695
|
+
.deck-assets-size {
|
|
1695
1696
|
flex: 0 0 auto;
|
|
1696
1697
|
font: 400 11px/1.5 var(--mono);
|
|
1697
1698
|
color: var(--muted);
|
|
1698
1699
|
}
|
|
1700
|
+
/* The row grows to the path and the size is pinned right of it, so a long name
|
|
1701
|
+
truncates rather than pushing the buttons off the edge. */
|
|
1702
|
+
.deck-assets-list li code { flex: 1 1 auto; }
|
|
1703
|
+
/* Put there in this session — the row you are most likely reaching for. */
|
|
1704
|
+
.deck-assets-list li.is-fresh { background: color-mix(in srgb, var(--accent) 14%, transparent); }
|
|
1705
|
+
.deck-assets-none {
|
|
1706
|
+
justify-content: center;
|
|
1707
|
+
padding: 14px 10px;
|
|
1708
|
+
font: 400 12px/1.5 var(--sans);
|
|
1709
|
+
color: var(--muted);
|
|
1710
|
+
background: none;
|
|
1711
|
+
}
|
|
1699
1712
|
.deck-assets-list li.is-failed { background: color-mix(in srgb, #e5484d 16%, transparent); }
|
|
1700
1713
|
.deck-assets-list li.is-failed .deck-assets-pct { color: #ff8589; }
|
|
1701
1714
|
|
|
Binary file
|