fb-slides 0.9.0 → 0.10.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 +18 -0
- package/README.md +7 -4
- package/lib/assets.mjs +29 -3
- package/package.json +1 -1
- package/runtime/assets.js +144 -63
- package/runtime/theme.base.css +54 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ 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.10.0 — 2026-09-08
|
|
11
|
+
|
|
12
|
+
- **A file in the assets panel opens on click.** The name on a row carries the
|
|
13
|
+
full path in its tooltip, and clicking it shows the file: an image comes up
|
|
14
|
+
right there, over the deck — full size, the path under it, a click anywhere
|
|
15
|
+
or Escape puts it away — and everything else opens in a new tab, where the
|
|
16
|
+
browser plays the clip, renders the PDF, and offers to save the rest.
|
|
17
|
+
|
|
18
|
+
## 0.9.1 — 2026-09-08
|
|
19
|
+
|
|
20
|
+
- **The assets panel shows the folder, not just what you dropped.** The list
|
|
21
|
+
under the folder picker is now that folder as it is on disk — the screenshot
|
|
22
|
+
from a moment ago and the one from last month, side by side — so an asset
|
|
23
|
+
that was already there can be copied, written into the slide, or deleted
|
|
24
|
+
without going to the Finder for it. Picking another folder lists that one,
|
|
25
|
+
what this session put there is tinted, and a file under a kilobyte is counted
|
|
26
|
+
in bytes rather than rounding to `0 KB`.
|
|
27
|
+
|
|
10
28
|
## 0.9.0 — 2026-09-08
|
|
11
29
|
|
|
12
30
|
- **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.
|
|
@@ -161,6 +167,32 @@ export const createAssets = ({ paste, active }) => {
|
|
|
161
167
|
const wordInput = confirmBox.querySelector('.deck-assets-word');
|
|
162
168
|
const confirmButton = confirmBox.querySelector('.deck-assets-confirm-delete');
|
|
163
169
|
|
|
170
|
+
// ---- the image preview --------------------------------------------------
|
|
171
|
+
// A click on an image's name opens the image itself, over everything — the
|
|
172
|
+
// drawer is narrow and a thumbnail would say less than the file name does.
|
|
173
|
+
// A click anywhere on it, or Escape, puts it away. Every other kind of file
|
|
174
|
+
// goes to a new tab instead: the browser knows a clip and a PDF better.
|
|
175
|
+
|
|
176
|
+
const preview = document.createElement('div');
|
|
177
|
+
preview.className = 'deck-assets-preview';
|
|
178
|
+
preview.hidden = true;
|
|
179
|
+
preview.innerHTML = `<figure><img alt="" /><figcaption><code></code></figcaption></figure>`;
|
|
180
|
+
const previewImage = preview.querySelector('img');
|
|
181
|
+
|
|
182
|
+
const openPreview = (path) => {
|
|
183
|
+
previewImage.src = path;
|
|
184
|
+
preview.querySelector('figcaption code').textContent = path;
|
|
185
|
+
preview.hidden = false;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const closePreview = () => {
|
|
189
|
+
preview.hidden = true;
|
|
190
|
+
// A 20 MB screenshot should stop arriving the moment nobody is looking.
|
|
191
|
+
previewImage.removeAttribute('src');
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
preview.addEventListener('click', closePreview);
|
|
195
|
+
|
|
164
196
|
// ---- the full-page sheet ----------------------------------------------
|
|
165
197
|
|
|
166
198
|
const overlay = document.createElement('div');
|
|
@@ -181,7 +213,10 @@ export const createAssets = ({ paste, active }) => {
|
|
|
181
213
|
let dirs = []; // its subfolders, as the server last listed them
|
|
182
214
|
let chosen = ''; // where the next drop goes, remembered between drops
|
|
183
215
|
let pending = null; // files waiting for a folder that has yet to be named
|
|
184
|
-
let asking = null; //
|
|
216
|
+
let asking = null; // the path the delete question is standing over
|
|
217
|
+
let files = []; // the chosen folder, as the server last listed it
|
|
218
|
+
let failures = []; // uploads that did not land, kept under the listing
|
|
219
|
+
const fresh = new Set(); // what this session put there — worth pointing at
|
|
185
220
|
|
|
186
221
|
// The root, what the server last listed, and — however that listing raced
|
|
187
222
|
// with the upload that made it — the folder this drop actually went to.
|
|
@@ -213,21 +248,28 @@ export const createAssets = ({ paste, active }) => {
|
|
|
213
248
|
targets.insertAdjacentHTML('beforeend', `<li data-dir="${NEW}" class="is-new">+ new folder…</li>`);
|
|
214
249
|
};
|
|
215
250
|
|
|
216
|
-
// The folder
|
|
217
|
-
//
|
|
251
|
+
// The folder as it is right now — its subfolders to aim at, and its files to
|
|
252
|
+
// list. Called on open, on picking another folder, and after every write.
|
|
218
253
|
const refresh = async () => {
|
|
254
|
+
const where = pending ? '' : chosen;
|
|
219
255
|
try {
|
|
220
|
-
const response = await fetch(
|
|
256
|
+
const response = await fetch(`/api/assets?dir=${encodeURIComponent(where)}`, {
|
|
257
|
+
headers: API_HEADERS,
|
|
258
|
+
cache: 'no-cache',
|
|
259
|
+
});
|
|
221
260
|
if (response.ok) {
|
|
222
261
|
const answer = await response.json();
|
|
223
262
|
root = answer.dir ?? root;
|
|
224
263
|
dirs = Array.isArray(answer.dirs) ? answer.dirs : [];
|
|
264
|
+
files = Array.isArray(answer.files) ? answer.files : [];
|
|
225
265
|
}
|
|
226
266
|
} catch {
|
|
227
267
|
/* no listing: assets/ itself is always there to drop into */
|
|
268
|
+
files = [];
|
|
228
269
|
}
|
|
229
270
|
drawSelect();
|
|
230
271
|
drawTargets();
|
|
272
|
+
drawFiles();
|
|
231
273
|
};
|
|
232
274
|
|
|
233
275
|
// ---- open and close ---------------------------------------------------
|
|
@@ -250,6 +292,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
250
292
|
nameRow.hidden = true;
|
|
251
293
|
waiting.hidden = true;
|
|
252
294
|
closeConfirm();
|
|
295
|
+
closePreview();
|
|
253
296
|
};
|
|
254
297
|
|
|
255
298
|
// ---- the rows ---------------------------------------------------------
|
|
@@ -269,6 +312,70 @@ export const createAssets = ({ paste, active }) => {
|
|
|
269
312
|
}
|
|
270
313
|
};
|
|
271
314
|
|
|
315
|
+
// The folder, one row per file: the path, and the three things worth doing
|
|
316
|
+
// with it. Rebuilt whole on every refresh — a listing is cheap, and the
|
|
317
|
+
// alternative is keeping rows in step with a folder two other things write to.
|
|
318
|
+
const drawFiles = () => {
|
|
319
|
+
list.innerHTML = '';
|
|
320
|
+
|
|
321
|
+
if (!files.length && !failures.length) {
|
|
322
|
+
const empty = document.createElement('li');
|
|
323
|
+
empty.className = 'deck-assets-none';
|
|
324
|
+
empty.textContent = pending ? 'The folder will be made when you name it.' : 'Nothing in this folder yet.';
|
|
325
|
+
list.append(empty);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const file of files) {
|
|
329
|
+
const row = document.createElement('li');
|
|
330
|
+
if (fresh.has(file.path)) row.classList.add('is-fresh');
|
|
331
|
+
row.innerHTML =
|
|
332
|
+
`<code></code>` +
|
|
333
|
+
`<span class="deck-assets-size"></span>` +
|
|
334
|
+
`<span class="deck-assets-acts">` +
|
|
335
|
+
`<button type="button" class="deck-assets-copy" title="Copy the path">${COPY_ICON}</button>` +
|
|
336
|
+
`<button type="button" class="deck-assets-insert" title="Write it into the slide">${INSERT_ICON}</button>` +
|
|
337
|
+
`<button type="button" class="deck-assets-trash" title="Delete the file">${TRASH_ICON}</button>` +
|
|
338
|
+
`</span>`;
|
|
339
|
+
const name = row.querySelector('code');
|
|
340
|
+
name.textContent = file.path;
|
|
341
|
+
// The drawer is narrow and a path can be long: the row truncates, and
|
|
342
|
+
// hovering the name says the whole thing. Clicking it shows the file: an
|
|
343
|
+
// image right here, over the deck; anything else in a new tab, where the
|
|
344
|
+
// browser plays the clip, renders the PDF, and offers to save the rest.
|
|
345
|
+
name.title = file.path;
|
|
346
|
+
name.classList.add('is-viewable');
|
|
347
|
+
if (IMAGE.test(file.path)) {
|
|
348
|
+
name.classList.add('is-image');
|
|
349
|
+
name.addEventListener('click', () => openPreview(file.path));
|
|
350
|
+
} else {
|
|
351
|
+
name.addEventListener('click', () => window.open(file.path, '_blank', 'noopener'));
|
|
352
|
+
}
|
|
353
|
+
row.querySelector('.deck-assets-size').textContent = bytes(file.size);
|
|
354
|
+
row.querySelector('.deck-assets-copy').addEventListener('click', (event) =>
|
|
355
|
+
copy(file.path, event.currentTarget),
|
|
356
|
+
);
|
|
357
|
+
row.querySelector('.deck-assets-insert').addEventListener('click', () => {
|
|
358
|
+
paste(blockFor(file.path, file.name));
|
|
359
|
+
close();
|
|
360
|
+
});
|
|
361
|
+
row.querySelector('.deck-assets-trash').addEventListener('click', () => askDelete(file.path));
|
|
362
|
+
list.append(row);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// What did not land stays under the listing rather than vanishing with the
|
|
366
|
+
// refresh that follows the upload.
|
|
367
|
+
for (const failure of failures) {
|
|
368
|
+
const row = document.createElement('li');
|
|
369
|
+
row.className = 'is-failed';
|
|
370
|
+
row.innerHTML = `<span class="deck-assets-file"></span><span class="deck-assets-pct"></span>`;
|
|
371
|
+
row.querySelector('.deck-assets-file').textContent = failure.name;
|
|
372
|
+
row.querySelector('.deck-assets-pct').textContent = failure.message;
|
|
373
|
+
list.append(row);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// While an upload is in flight it has a row of its own, under the listing,
|
|
378
|
+
// counting up. The refresh that follows replaces it with the real thing.
|
|
272
379
|
const addRow = (file) => {
|
|
273
380
|
const row = document.createElement('li');
|
|
274
381
|
row.innerHTML = `<span class="deck-assets-file"></span><span class="deck-assets-pct"></span>`;
|
|
@@ -277,64 +384,37 @@ export const createAssets = ({ paste, active }) => {
|
|
|
277
384
|
pct.textContent = bytes(file.size);
|
|
278
385
|
list.append(row);
|
|
279
386
|
row.scrollIntoView({ block: 'nearest' });
|
|
280
|
-
|
|
281
387
|
return {
|
|
282
388
|
progress: (share) => {
|
|
283
389
|
pct.textContent = `${Math.round(share * 100)}%`;
|
|
284
390
|
},
|
|
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
391
|
};
|
|
309
392
|
};
|
|
310
393
|
|
|
311
394
|
// One at a time: a folder of screenshots dropped at once should not open ten
|
|
312
395
|
// sockets, and the rows then fill in the order they were dropped.
|
|
313
|
-
const send = async (
|
|
314
|
-
if (!
|
|
396
|
+
const send = async (dropped, dir) => {
|
|
397
|
+
if (!dropped.length) return;
|
|
315
398
|
chosen = dir;
|
|
399
|
+
failures = [];
|
|
316
400
|
open();
|
|
317
|
-
for (const file of
|
|
401
|
+
for (const file of dropped) {
|
|
318
402
|
const row = addRow(file);
|
|
319
403
|
try {
|
|
320
404
|
const answer = await upload(file, dir, row.progress);
|
|
321
|
-
row.done(answer);
|
|
322
405
|
// The folder as the server spells it — `talk 2026` was typed, and what
|
|
323
406
|
// exists on disk is `talk-2026`. The list should say the latter.
|
|
324
407
|
chosen = answer.dir;
|
|
325
|
-
|
|
326
|
-
dirs = [...dirs, answer.dir].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
|
|
327
|
-
}
|
|
328
|
-
drawSelect();
|
|
329
|
-
drawTargets();
|
|
408
|
+
fresh.add(answer.path);
|
|
330
409
|
} catch (error) {
|
|
331
|
-
|
|
410
|
+
failures.push({ name: file.name, message: error.message });
|
|
332
411
|
}
|
|
333
412
|
}
|
|
413
|
+
await refresh();
|
|
334
414
|
};
|
|
335
415
|
|
|
336
|
-
const askDelete = (path
|
|
337
|
-
asking =
|
|
416
|
+
const askDelete = (path) => {
|
|
417
|
+
asking = path;
|
|
338
418
|
confirmBox.querySelector('code').textContent = path;
|
|
339
419
|
wordInput.value = '';
|
|
340
420
|
confirmButton.disabled = true;
|
|
@@ -344,7 +424,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
344
424
|
|
|
345
425
|
const doDelete = async () => {
|
|
346
426
|
if (!asking || confirmButton.disabled) return;
|
|
347
|
-
const
|
|
427
|
+
const path = asking;
|
|
348
428
|
confirmButton.disabled = true;
|
|
349
429
|
try {
|
|
350
430
|
const response = await fetch(`/api/assets?path=${encodeURIComponent(path)}`, {
|
|
@@ -352,10 +432,8 @@ export const createAssets = ({ paste, active }) => {
|
|
|
352
432
|
headers: API_HEADERS,
|
|
353
433
|
});
|
|
354
434
|
if (!response.ok) throw new Error((await response.json()).error ?? response.status);
|
|
355
|
-
|
|
435
|
+
fresh.delete(path);
|
|
356
436
|
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
437
|
refresh();
|
|
360
438
|
} catch (error) {
|
|
361
439
|
confirmBox.querySelector('code').textContent = error.message;
|
|
@@ -364,14 +442,14 @@ export const createAssets = ({ paste, active }) => {
|
|
|
364
442
|
};
|
|
365
443
|
|
|
366
444
|
// A drop that named no folder yet: hold the files, ask, and send on the answer.
|
|
367
|
-
const askName = (
|
|
368
|
-
pending =
|
|
445
|
+
const askName = (dropped) => {
|
|
446
|
+
pending = dropped;
|
|
369
447
|
open();
|
|
370
448
|
drawSelect();
|
|
371
449
|
nameRow.hidden = false;
|
|
372
450
|
goButton.hidden = false;
|
|
373
451
|
waiting.hidden = false;
|
|
374
|
-
waiting.textContent = `${
|
|
452
|
+
waiting.textContent = `${dropped.length} file${dropped.length === 1 ? '' : 's'} waiting for a folder name.`;
|
|
375
453
|
nameInput.focus();
|
|
376
454
|
nameInput.select();
|
|
377
455
|
};
|
|
@@ -379,23 +457,23 @@ export const createAssets = ({ paste, active }) => {
|
|
|
379
457
|
const confirmName = () => {
|
|
380
458
|
const dir = nameInput.value.trim().replace(/^\/+|\/+$/g, '');
|
|
381
459
|
if (!dir) return nameInput.focus();
|
|
382
|
-
const
|
|
460
|
+
const held = pending;
|
|
383
461
|
pending = null;
|
|
384
462
|
nameInput.value = '';
|
|
385
463
|
nameRow.hidden = true;
|
|
386
464
|
goButton.hidden = true;
|
|
387
465
|
waiting.hidden = true;
|
|
388
|
-
if (
|
|
466
|
+
if (held) send(held, dir);
|
|
389
467
|
else {
|
|
390
468
|
chosen = dir;
|
|
391
|
-
|
|
469
|
+
refresh();
|
|
392
470
|
}
|
|
393
471
|
};
|
|
394
472
|
|
|
395
|
-
const drop = (
|
|
396
|
-
if (!
|
|
397
|
-
if (dir === NEW) askName(
|
|
398
|
-
else send(
|
|
473
|
+
const drop = (dropped, dir) => {
|
|
474
|
+
if (!dropped.length) return;
|
|
475
|
+
if (dir === NEW) askName(dropped);
|
|
476
|
+
else send(dropped, dir);
|
|
399
477
|
};
|
|
400
478
|
|
|
401
479
|
// ---- wiring: the panel -------------------------------------------------
|
|
@@ -410,6 +488,8 @@ export const createAssets = ({ paste, active }) => {
|
|
|
410
488
|
}
|
|
411
489
|
nameRow.hidden = true;
|
|
412
490
|
chosen = select.value;
|
|
491
|
+
failures = [];
|
|
492
|
+
refresh();
|
|
413
493
|
});
|
|
414
494
|
|
|
415
495
|
wordInput.addEventListener('input', () => {
|
|
@@ -435,9 +515,9 @@ export const createAssets = ({ paste, active }) => {
|
|
|
435
515
|
fileInput.click();
|
|
436
516
|
});
|
|
437
517
|
fileInput.addEventListener('change', () => {
|
|
438
|
-
const
|
|
518
|
+
const picked = [...fileInput.files];
|
|
439
519
|
fileInput.value = '';
|
|
440
|
-
drop(
|
|
520
|
+
drop(picked, select.value === NEW ? nameInput.value.trim() : select.value);
|
|
441
521
|
});
|
|
442
522
|
|
|
443
523
|
// ---- wiring: the sheet -------------------------------------------------
|
|
@@ -492,7 +572,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
492
572
|
drop([...event.dataTransfer.files], target?.dataset.dir ?? chosen);
|
|
493
573
|
});
|
|
494
574
|
|
|
495
|
-
document.body.append(overlay);
|
|
575
|
+
document.body.append(overlay, preview);
|
|
496
576
|
|
|
497
577
|
return {
|
|
498
578
|
element,
|
|
@@ -503,6 +583,7 @@ export const createAssets = ({ paste, active }) => {
|
|
|
503
583
|
// One layer per press, innermost first — the drawer asks this before it
|
|
504
584
|
// decides whether Escape was meant for it.
|
|
505
585
|
escape: () => {
|
|
586
|
+
if (!preview.hidden) return closePreview(), true;
|
|
506
587
|
if (!confirmBox.hidden) return closeConfirm(), true;
|
|
507
588
|
if (!element.hidden) return close(), true;
|
|
508
589
|
return false;
|
package/runtime/theme.base.css
CHANGED
|
@@ -1683,6 +1683,12 @@ body.edit-open #deck-edit { transform: none; }
|
|
|
1683
1683
|
text-overflow: ellipsis;
|
|
1684
1684
|
white-space: nowrap;
|
|
1685
1685
|
}
|
|
1686
|
+
/* A file's name shows the file — an image over the deck, the rest in a new
|
|
1687
|
+
tab; the cursor and the underline are the whole affordance, the row stays a
|
|
1688
|
+
row. */
|
|
1689
|
+
.deck-assets-list code.is-viewable { cursor: pointer; }
|
|
1690
|
+
.deck-assets-list code.is-image { cursor: zoom-in; }
|
|
1691
|
+
.deck-assets-list code.is-viewable:hover { color: var(--accent); text-decoration: underline; }
|
|
1686
1692
|
.deck-assets-file {
|
|
1687
1693
|
min-width: 0;
|
|
1688
1694
|
font: 400 12px/1.5 var(--sans);
|
|
@@ -1691,11 +1697,24 @@ body.edit-open #deck-edit { transform: none; }
|
|
|
1691
1697
|
text-overflow: ellipsis;
|
|
1692
1698
|
white-space: nowrap;
|
|
1693
1699
|
}
|
|
1694
|
-
.deck-assets-pct
|
|
1700
|
+
.deck-assets-pct,
|
|
1701
|
+
.deck-assets-size {
|
|
1695
1702
|
flex: 0 0 auto;
|
|
1696
1703
|
font: 400 11px/1.5 var(--mono);
|
|
1697
1704
|
color: var(--muted);
|
|
1698
1705
|
}
|
|
1706
|
+
/* The row grows to the path and the size is pinned right of it, so a long name
|
|
1707
|
+
truncates rather than pushing the buttons off the edge. */
|
|
1708
|
+
.deck-assets-list li code { flex: 1 1 auto; }
|
|
1709
|
+
/* Put there in this session — the row you are most likely reaching for. */
|
|
1710
|
+
.deck-assets-list li.is-fresh { background: color-mix(in srgb, var(--accent) 14%, transparent); }
|
|
1711
|
+
.deck-assets-none {
|
|
1712
|
+
justify-content: center;
|
|
1713
|
+
padding: 14px 10px;
|
|
1714
|
+
font: 400 12px/1.5 var(--sans);
|
|
1715
|
+
color: var(--muted);
|
|
1716
|
+
background: none;
|
|
1717
|
+
}
|
|
1699
1718
|
.deck-assets-list li.is-failed { background: color-mix(in srgb, #e5484d 16%, transparent); }
|
|
1700
1719
|
.deck-assets-list li.is-failed .deck-assets-pct { color: #ff8589; }
|
|
1701
1720
|
|
|
@@ -1856,6 +1875,40 @@ body.edit-open #deck-edit { transform: none; }
|
|
|
1856
1875
|
transform: scale(1.04);
|
|
1857
1876
|
}
|
|
1858
1877
|
|
|
1878
|
+
/* The image preview — over everything the way the drop sheet is: the image is
|
|
1879
|
+
the whole point while it is up, and a click anywhere puts it away. */
|
|
1880
|
+
.deck-assets-preview {
|
|
1881
|
+
position: fixed;
|
|
1882
|
+
inset: 0;
|
|
1883
|
+
z-index: 72;
|
|
1884
|
+
display: grid;
|
|
1885
|
+
place-items: center;
|
|
1886
|
+
background: color-mix(in srgb, var(--bg) 74%, transparent);
|
|
1887
|
+
backdrop-filter: blur(8px);
|
|
1888
|
+
cursor: zoom-out;
|
|
1889
|
+
}
|
|
1890
|
+
.deck-assets-preview[hidden] { display: none; }
|
|
1891
|
+
.deck-assets-preview figure {
|
|
1892
|
+
margin: 0;
|
|
1893
|
+
display: flex;
|
|
1894
|
+
flex-direction: column;
|
|
1895
|
+
align-items: center;
|
|
1896
|
+
gap: 12px;
|
|
1897
|
+
}
|
|
1898
|
+
.deck-assets-preview img {
|
|
1899
|
+
max-width: min(1100px, 84vw);
|
|
1900
|
+
max-height: 78vh;
|
|
1901
|
+
border: 1px solid var(--line);
|
|
1902
|
+
border-radius: 10px;
|
|
1903
|
+
/* A card behind it, so a transparent PNG reads as an image and not a hole. */
|
|
1904
|
+
background: var(--card);
|
|
1905
|
+
box-shadow: 0 18px 60px color-mix(in srgb, #000 45%, transparent);
|
|
1906
|
+
}
|
|
1907
|
+
.deck-assets-preview figcaption code {
|
|
1908
|
+
font: 500 12.5px/1.4 var(--mono);
|
|
1909
|
+
color: var(--accent-2);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1859
1912
|
/* The open drawer's button wears the accent — it draws in nothing, like the
|
|
1860
1913
|
pointer's; the id outweighs the generic armed-tool rule. */
|
|
1861
1914
|
#tool-edit[aria-pressed='true'] {
|