@anton-gustafsson/snapshot-core 0.0.6 → 0.4.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/dist/cached-snapshot-storage.d.ts +79 -0
- package/dist/cached-snapshot-storage.js +176 -0
- package/dist/encode.d.ts +19 -0
- package/dist/encode.js +56 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +44 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/snapshot-nav-list.d.ts +52 -8
- package/dist/snapshot-nav-list.js +178 -54
- package/dist/snapshot-service.d.ts +87 -12
- package/dist/snapshot-service.js +216 -39
- package/dist/snapshot-storage.d.ts +46 -7
- package/dist/snapshot-storage.js +54 -18
- package/package.json +7 -3
|
@@ -12,6 +12,7 @@ import { snapshotService as defaultSnapshotService } from './snapshot-service';
|
|
|
12
12
|
function isMarkupIcon(icon) {
|
|
13
13
|
return icon.trimStart().startsWith('<');
|
|
14
14
|
}
|
|
15
|
+
const DEFAULT_EDIT_ICON = '✎';
|
|
15
16
|
/**
|
|
16
17
|
* Visual identity: a contact sheet. Every tile is a "frame" — numbered like a strip
|
|
17
18
|
* of negatives — because that's literally what a snapshot thumbnail is. All colors
|
|
@@ -22,8 +23,11 @@ export class SnapshotNavList extends LitElement {
|
|
|
22
23
|
constructor() {
|
|
23
24
|
super(...arguments);
|
|
24
25
|
this.items = [];
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/** `card` by default — a framed preview with title/description underneath. `tile` is the compact contact-sheet grid, `list` a sidebar row. */
|
|
27
|
+
this.variant = 'card';
|
|
28
|
+
/** Lets the host itself scroll (see `--snapshot-nav-list-max-height`) instead of growing unbounded. */
|
|
29
|
+
this.scrollable = false;
|
|
30
|
+
/** tile overlay: tint behind the title so it stays legible over any image. Transparent by default — opt into a scrim explicitly. */
|
|
27
31
|
this.overlayTint = 'none';
|
|
28
32
|
/** caption background tint strength, 0-1 */
|
|
29
33
|
this.textOverlayOpacity = 0.35;
|
|
@@ -31,15 +35,26 @@ export class SnapshotNavList extends LitElement {
|
|
|
31
35
|
this.imageOverlayOpacity = 0;
|
|
32
36
|
/** backdrop blur behind the title, in px */
|
|
33
37
|
this.overlayBlur = 0;
|
|
34
|
-
/**
|
|
38
|
+
/** tile only: 'bottom' is the caption strip (default), 'center' centers a larger title. */
|
|
35
39
|
this.labelPosition = 'bottom';
|
|
36
40
|
/** Defaults to the shared singleton — set your own instance (e.g. a namespaced or custom-storage SnapshotService) per <snapshot-nav-list> if needed. */
|
|
37
41
|
this.snapshotService = defaultSnapshotService;
|
|
38
|
-
/** Shows
|
|
42
|
+
/** Shows an edit button per card. Off by default — clicking it fires `nav-edit` instead of `nav-select`; the host decides what "edit" means (e.g. open its own dialog component). Overridable per row via `NavItem.editable`. */
|
|
39
43
|
this.editable = false;
|
|
44
|
+
/** Where the edit button sits: `overlay` (default) floats it over the thumbnail; `meta` pins it to the right edge of the title row, with the description below. Ignored by the icon-only variant, whose caption is itself an overlay. */
|
|
45
|
+
this.editButtonPosition = 'overlay';
|
|
46
|
+
/** Edit button glyph. Same convention as `NavItem.icon`: a plain-text glyph (e.g. an emoji), or markup — a string starting with `<` renders as raw HTML/SVG, so a consumer can pass its own icon (e.g. `<svg>...</svg>`). */
|
|
47
|
+
this.editIcon = DEFAULT_EDIT_ICON;
|
|
40
48
|
this.thumbs = new Map();
|
|
41
49
|
this.loadingIds = new Set();
|
|
42
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* In-flight dedup guard, separate from `loadingIds` (which is only for
|
|
52
|
+
* spinner display) so a repeat `loadThumb` call for an id already being
|
|
53
|
+
* fetched is a no-op. Keyed by `${variant}\0${id}` — not just `id` — so a
|
|
54
|
+
* `variantKey` switch mid-flight doesn't have the new variant's fetch
|
|
55
|
+
* silently dropped because the *previous* variant's id is still marked
|
|
56
|
+
* in-flight.
|
|
57
|
+
*/
|
|
43
58
|
this.fetchingIds = new Set();
|
|
44
59
|
}
|
|
45
60
|
static { this.styles = css `
|
|
@@ -47,6 +62,16 @@ export class SnapshotNavList extends LitElement {
|
|
|
47
62
|
display: block;
|
|
48
63
|
font-family: var(--snapshot-nav-list-font, inherit);
|
|
49
64
|
--frame-accent: var(--snapshot-nav-list-accent, #ff5a1f);
|
|
65
|
+
max-height: var(--snapshot-nav-list-max-height, none);
|
|
66
|
+
}
|
|
67
|
+
/* scrollable: the host owns the scroll, so a long list in a flex sidebar
|
|
68
|
+
doesn't push its siblings off-screen and the consumer doesn't have to
|
|
69
|
+
reach in with its own overflow/min-height CSS. */
|
|
70
|
+
:host([scrollable]) {
|
|
71
|
+
flex: 1 1 auto;
|
|
72
|
+
min-height: 0;
|
|
73
|
+
overflow-y: auto;
|
|
74
|
+
overscroll-behavior: contain;
|
|
50
75
|
}
|
|
51
76
|
ul {
|
|
52
77
|
list-style: none;
|
|
@@ -182,10 +207,10 @@ export class SnapshotNavList extends LitElement {
|
|
|
182
207
|
-webkit-backdrop-filter: blur(var(--overlay-blur, 0px));
|
|
183
208
|
pointer-events: none;
|
|
184
209
|
}
|
|
185
|
-
/* the overlay exists so
|
|
210
|
+
/* the overlay exists so a tile's overlaid title stays legible — list
|
|
186
211
|
variant shows the label beside the thumb, not on top of it, so the
|
|
187
212
|
tint has nothing to do there. */
|
|
188
|
-
:host(:not([variant='
|
|
213
|
+
:host(:not([variant='tile'])) .image-overlay {
|
|
189
214
|
display: none;
|
|
190
215
|
}
|
|
191
216
|
|
|
@@ -243,6 +268,50 @@ export class SnapshotNavList extends LitElement {
|
|
|
243
268
|
outline: 2px solid var(--frame-accent);
|
|
244
269
|
outline-offset: 1px;
|
|
245
270
|
}
|
|
271
|
+
.edit-button svg {
|
|
272
|
+
width: 1em;
|
|
273
|
+
height: 1em;
|
|
274
|
+
display: block;
|
|
275
|
+
fill: currentColor;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Transparent by default (display: contents) so the existing per-variant
|
|
279
|
+
.label/.meta rules — including icon-only's absolute caption strip —
|
|
280
|
+
keep applying unchanged; it only becomes a real row when the edit
|
|
281
|
+
button moves in beside the title. */
|
|
282
|
+
.label-row {
|
|
283
|
+
display: contents;
|
|
284
|
+
}
|
|
285
|
+
/* edit-button-position="meta": button on the title's line, description
|
|
286
|
+
still on its own line underneath. Not offered for icon-only, whose
|
|
287
|
+
.meta is an absolutely positioned overlay strip — the overlay button is
|
|
288
|
+
already the right place there. */
|
|
289
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .label-row {
|
|
290
|
+
display: flex;
|
|
291
|
+
align-items: center;
|
|
292
|
+
gap: 0.5rem;
|
|
293
|
+
min-width: 0;
|
|
294
|
+
}
|
|
295
|
+
/* the title takes the whole row so the button lands on the card's right
|
|
296
|
+
edge, still on the title's own line (the description sits below it);
|
|
297
|
+
min-width: 0 keeps a long title ellipsising instead of pushing out. */
|
|
298
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .label {
|
|
299
|
+
flex: 1;
|
|
300
|
+
min-width: 0;
|
|
301
|
+
}
|
|
302
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .edit-button {
|
|
303
|
+
position: static;
|
|
304
|
+
flex-shrink: 0;
|
|
305
|
+
/* in-flow, over the host's own background: currentColor-derived instead
|
|
306
|
+
of the overlay's dark scrim, and always visible (it isn't covering
|
|
307
|
+
anything, so hiding it until hover would just make it hard to find) */
|
|
308
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
309
|
+
color: inherit;
|
|
310
|
+
opacity: 1;
|
|
311
|
+
}
|
|
312
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .edit-button:hover {
|
|
313
|
+
background: color-mix(in srgb, currentColor 20%, transparent);
|
|
314
|
+
}
|
|
246
315
|
|
|
247
316
|
/* list: a compact thumb reads better in a narrow sidebar than the grid's 160x100 */
|
|
248
317
|
:host([variant='list']) .thumb-wrap {
|
|
@@ -250,25 +319,25 @@ export class SnapshotNavList extends LitElement {
|
|
|
250
319
|
height: 68px;
|
|
251
320
|
}
|
|
252
321
|
|
|
253
|
-
/*
|
|
254
|
-
:host([variant='
|
|
322
|
+
/* tile: contact-sheet grid, caption strip pinned to the bottom of each frame */
|
|
323
|
+
:host([variant='tile']) ul {
|
|
255
324
|
flex-direction: row;
|
|
256
325
|
flex-wrap: wrap;
|
|
257
326
|
gap: 0.6rem;
|
|
258
327
|
}
|
|
259
|
-
:host([variant='
|
|
328
|
+
:host([variant='tile']) li {
|
|
260
329
|
position: relative;
|
|
261
330
|
width: var(--snapshot-nav-list-tile-width, 160px);
|
|
262
331
|
height: var(--snapshot-nav-list-tile-height, 100px);
|
|
263
332
|
padding: 0;
|
|
264
333
|
overflow: hidden;
|
|
265
334
|
}
|
|
266
|
-
:host([variant='
|
|
335
|
+
:host([variant='tile']) .thumb-wrap {
|
|
267
336
|
width: 100%;
|
|
268
337
|
height: 100%;
|
|
269
338
|
border-radius: var(--snapshot-nav-list-radius, 10px);
|
|
270
339
|
}
|
|
271
|
-
:host([variant='
|
|
340
|
+
:host([variant='tile']) .meta {
|
|
272
341
|
position: absolute;
|
|
273
342
|
inset: auto 0 0 0;
|
|
274
343
|
margin: var(--snapshot-nav-list-overlay-margin, 0);
|
|
@@ -281,28 +350,28 @@ export class SnapshotNavList extends LitElement {
|
|
|
281
350
|
backdrop-filter: blur(var(--overlay-blur, 0px));
|
|
282
351
|
-webkit-backdrop-filter: blur(var(--overlay-blur, 0px));
|
|
283
352
|
}
|
|
284
|
-
:host([variant='
|
|
353
|
+
:host([variant='tile']) .label {
|
|
285
354
|
white-space: normal;
|
|
286
355
|
}
|
|
287
|
-
:host([variant='
|
|
356
|
+
:host([variant='tile']) .description {
|
|
288
357
|
color: color-mix(in srgb, var(--overlay-text, #fff) 75%, transparent);
|
|
289
358
|
}
|
|
290
359
|
|
|
291
360
|
/* label-position="center": title big and centered */
|
|
292
|
-
:host([variant='
|
|
361
|
+
:host([variant='tile'][label-position='center']) .meta {
|
|
293
362
|
inset: 0;
|
|
294
363
|
align-items: center;
|
|
295
364
|
justify-content: center;
|
|
296
365
|
padding: 0.6rem;
|
|
297
366
|
}
|
|
298
|
-
:host([variant='
|
|
367
|
+
:host([variant='tile'][label-position='center']) .label {
|
|
299
368
|
font-size: 1.15rem;
|
|
300
369
|
font-weight: 600;
|
|
301
370
|
text-align: center;
|
|
302
371
|
}
|
|
303
372
|
|
|
304
373
|
/* card: a contained (never cropped) preview above real body text below it —
|
|
305
|
-
text never sits on top of the image, so unlike
|
|
374
|
+
text never sits on top of the image, so unlike a tile it needs no
|
|
306
375
|
overlay tint to stay legible. Modeled on a typical "preview card"
|
|
307
376
|
pattern: framed shot, title + description underneath, shadow on hover. */
|
|
308
377
|
:host([variant='card']) ul {
|
|
@@ -367,15 +436,25 @@ export class SnapshotNavList extends LitElement {
|
|
|
367
436
|
font-size: 0.8125rem;
|
|
368
437
|
}
|
|
369
438
|
`; }
|
|
439
|
+
fetchKey(variant, id) {
|
|
440
|
+
return `${variant ?? ''}\0${id}`;
|
|
441
|
+
}
|
|
370
442
|
subscribeToService() {
|
|
371
443
|
this.unsubscribe?.();
|
|
372
|
-
this.unsubscribe = this.snapshotService.subscribe((id, url) => {
|
|
444
|
+
this.unsubscribe = this.snapshotService.subscribe((id, url, variant) => {
|
|
373
445
|
// Ignore captures for ids this list isn't showing — the service is
|
|
374
446
|
// often a shared singleton, so without this guard `thumbs` would grow
|
|
375
447
|
// forever with urls for every snapshot captured anywhere on the page,
|
|
376
448
|
// not just this list's own items.
|
|
377
449
|
if (!this.items.some((item) => item.id === id))
|
|
378
450
|
return;
|
|
451
|
+
// Same reason, one dimension over: a dark-theme capture must not
|
|
452
|
+
// overwrite the light-theme tile this list is currently showing. Folds
|
|
453
|
+
// '' and undefined together on both sides — SnapshotService.keyOf()
|
|
454
|
+
// does the same, so a caller that sets variantKey to '' instead of
|
|
455
|
+
// leaving it undefined must still match a plain (no-variant) capture.
|
|
456
|
+
if ((variant || undefined) !== (this.variantKey || undefined))
|
|
457
|
+
return;
|
|
379
458
|
if (url === null) {
|
|
380
459
|
this.thumbs.delete(id);
|
|
381
460
|
}
|
|
@@ -400,14 +479,23 @@ export class SnapshotNavList extends LitElement {
|
|
|
400
479
|
// update instead of triggering Lit's "update scheduled from updated()"
|
|
401
480
|
// warning that came from doing this same flip inside updated().
|
|
402
481
|
willUpdate(changed) {
|
|
403
|
-
|
|
482
|
+
// 'icon-only' is the pre-0.3 name for 'tile'. Normalising here (rather
|
|
483
|
+
// than in a setter) keeps the reflected attribute — and therefore every
|
|
484
|
+
// CSS selector — on the one canonical value.
|
|
485
|
+
if (this.variant === 'icon-only')
|
|
486
|
+
this.variant = 'tile';
|
|
487
|
+
// A variant-key switch (e.g. light -> dark) invalidates every thumbnail:
|
|
488
|
+
// they're separate snapshots under separate keys.
|
|
489
|
+
if (changed.has('variantKey'))
|
|
490
|
+
this.thumbs.clear();
|
|
491
|
+
if (changed.has('items') || changed.has('variantKey')) {
|
|
404
492
|
const ids = new Set(this.items.map((item) => item.id));
|
|
405
493
|
for (const id of this.thumbs.keys()) {
|
|
406
494
|
if (!ids.has(id))
|
|
407
495
|
this.thumbs.delete(id);
|
|
408
496
|
}
|
|
409
497
|
for (const item of this.items) {
|
|
410
|
-
if (!this.thumbs.has(item.id) && !this.fetchingIds.has(item.id)) {
|
|
498
|
+
if (!this.thumbs.has(item.id) && !this.fetchingIds.has(this.fetchKey(this.variantKey, item.id))) {
|
|
411
499
|
this.loadingIds.add(item.id);
|
|
412
500
|
}
|
|
413
501
|
}
|
|
@@ -417,25 +505,42 @@ export class SnapshotNavList extends LitElement {
|
|
|
417
505
|
if (changed.has('snapshotService')) {
|
|
418
506
|
this.subscribeToService();
|
|
419
507
|
}
|
|
420
|
-
if (changed.has('items')) {
|
|
421
|
-
|
|
508
|
+
if (changed.has('items') || changed.has('variantKey')) {
|
|
509
|
+
void this.loadThumbs();
|
|
422
510
|
}
|
|
423
511
|
}
|
|
424
|
-
|
|
425
|
-
|
|
512
|
+
/**
|
|
513
|
+
* One batched read for the whole list — `getMany()` collapses to a single
|
|
514
|
+
* query/round-trip on a storage that implements `loadMany`, instead of one
|
|
515
|
+
* per row. `fetchingIds` still guards per id, so an `items` reassignment
|
|
516
|
+
* mid-flight doesn't re-request what's already coming.
|
|
517
|
+
*/
|
|
518
|
+
async loadThumbs() {
|
|
519
|
+
const variant = this.variantKey;
|
|
520
|
+
const wanted = this.items.filter((item) => !this.thumbs.has(item.id) && !this.fetchingIds.has(this.fetchKey(variant, item.id)));
|
|
521
|
+
if (wanted.length === 0)
|
|
426
522
|
return;
|
|
427
|
-
|
|
523
|
+
const ids = wanted.map((item) => item.id);
|
|
524
|
+
ids.forEach((id) => this.fetchingIds.add(this.fetchKey(variant, id)));
|
|
428
525
|
try {
|
|
429
|
-
const
|
|
430
|
-
if
|
|
431
|
-
|
|
526
|
+
const urls = await this.snapshotService.getMany(ids, { variant });
|
|
527
|
+
// Dropped if the variant changed while the read was in flight — those
|
|
528
|
+
// urls belong to the previous theme.
|
|
529
|
+
if (variant !== this.variantKey)
|
|
530
|
+
return;
|
|
531
|
+
for (const [id, url] of urls) {
|
|
532
|
+
if (url)
|
|
533
|
+
this.thumbs.set(id, url);
|
|
534
|
+
}
|
|
432
535
|
}
|
|
433
536
|
catch (err) {
|
|
434
|
-
console.error(
|
|
537
|
+
console.error('snapshot-nav-list: failed to load thumbnails', err);
|
|
435
538
|
}
|
|
436
539
|
finally {
|
|
437
|
-
|
|
438
|
-
|
|
540
|
+
ids.forEach((id) => {
|
|
541
|
+
this.fetchingIds.delete(this.fetchKey(variant, id));
|
|
542
|
+
this.loadingIds.delete(id);
|
|
543
|
+
});
|
|
439
544
|
this.requestUpdate();
|
|
440
545
|
}
|
|
441
546
|
}
|
|
@@ -460,24 +565,36 @@ export class SnapshotNavList extends LitElement {
|
|
|
460
565
|
const text = this.overlayTint === 'light' ? '#111' : '#fff';
|
|
461
566
|
return { '--overlay-bg': bg, '--overlay-text': text, '--overlay-blur': blur };
|
|
462
567
|
}
|
|
568
|
+
// Both events carry the whole item — including `data` — so a handler never
|
|
569
|
+
// has to look the item back up by id.
|
|
463
570
|
select(item) {
|
|
464
|
-
this.dispatchEvent(new CustomEvent('nav-select', {
|
|
465
|
-
detail: { id: item.id, route: item.route },
|
|
466
|
-
bubbles: true,
|
|
467
|
-
composed: true,
|
|
468
|
-
}));
|
|
571
|
+
this.dispatchEvent(new CustomEvent('nav-select', { detail: item, bubbles: true, composed: true }));
|
|
469
572
|
}
|
|
470
573
|
edit(e, item) {
|
|
471
574
|
e.stopPropagation();
|
|
472
|
-
this.dispatchEvent(new CustomEvent('nav-edit', {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
575
|
+
this.dispatchEvent(new CustomEvent('nav-edit', { detail: item, bubbles: true, composed: true }));
|
|
576
|
+
}
|
|
577
|
+
isEditable(item) {
|
|
578
|
+
return item.editable ?? this.editable;
|
|
579
|
+
}
|
|
580
|
+
renderEditButton(item) {
|
|
581
|
+
const icon = this.editIcon || DEFAULT_EDIT_ICON;
|
|
582
|
+
return html `<button
|
|
583
|
+
type="button"
|
|
584
|
+
class="edit-button"
|
|
585
|
+
part="edit-button"
|
|
586
|
+
aria-label="Edit ${item.label}"
|
|
587
|
+
@click=${(e) => this.edit(e, item)}
|
|
588
|
+
>
|
|
589
|
+
${isMarkupIcon(icon) ? unsafeHTML(icon) : icon}
|
|
590
|
+
</button>`;
|
|
477
591
|
}
|
|
478
592
|
render() {
|
|
479
593
|
const imageOverlayStyle = this.imageOverlayStyle;
|
|
480
594
|
const metaStyle = this.metaStyle;
|
|
595
|
+
// icon-only's caption is itself an overlay strip on the image, so there's
|
|
596
|
+
// no in-flow text row to put the button in — fall back to the overlay.
|
|
597
|
+
const editInMeta = this.editButtonPosition === 'meta' && this.variant !== 'icon-only';
|
|
481
598
|
return html `
|
|
482
599
|
<ul role="listbox">
|
|
483
600
|
${this.items.map((item) => html `
|
|
@@ -501,20 +618,13 @@ export class SnapshotNavList extends LitElement {
|
|
|
501
618
|
>
|
|
502
619
|
</div>`}
|
|
503
620
|
<div class="image-overlay" part="overlay" style=${styleMap(imageOverlayStyle)}></div>
|
|
504
|
-
${this.
|
|
505
|
-
? html `<button
|
|
506
|
-
type="button"
|
|
507
|
-
class="edit-button"
|
|
508
|
-
part="edit-button"
|
|
509
|
-
aria-label="Edit ${item.label}"
|
|
510
|
-
@click=${(e) => this.edit(e, item)}
|
|
511
|
-
>
|
|
512
|
-
✎
|
|
513
|
-
</button>`
|
|
514
|
-
: ''}
|
|
621
|
+
${this.isEditable(item) && !editInMeta ? this.renderEditButton(item) : ''}
|
|
515
622
|
</div>
|
|
516
623
|
<div class="meta" part="meta" style=${styleMap(metaStyle)}>
|
|
517
|
-
<
|
|
624
|
+
<div class="label-row" part="label-row">
|
|
625
|
+
<span class="label" part="label">${item.label}</span>
|
|
626
|
+
${this.isEditable(item) && editInMeta ? this.renderEditButton(item) : ''}
|
|
627
|
+
</div>
|
|
518
628
|
${item.description ? html `<span class="description" part="description">${item.description}</span>` : ''}
|
|
519
629
|
</div>
|
|
520
630
|
</li>
|
|
@@ -529,6 +639,12 @@ __decorate([
|
|
|
529
639
|
__decorate([
|
|
530
640
|
property({ reflect: true })
|
|
531
641
|
], SnapshotNavList.prototype, "variant", void 0);
|
|
642
|
+
__decorate([
|
|
643
|
+
property({ attribute: 'variant-key' })
|
|
644
|
+
], SnapshotNavList.prototype, "variantKey", void 0);
|
|
645
|
+
__decorate([
|
|
646
|
+
property({ type: Boolean, reflect: true })
|
|
647
|
+
], SnapshotNavList.prototype, "scrollable", void 0);
|
|
532
648
|
__decorate([
|
|
533
649
|
property({ attribute: 'overlay-tint' })
|
|
534
650
|
], SnapshotNavList.prototype, "overlayTint", void 0);
|
|
@@ -550,12 +666,20 @@ __decorate([
|
|
|
550
666
|
__decorate([
|
|
551
667
|
property({ type: Boolean })
|
|
552
668
|
], SnapshotNavList.prototype, "editable", void 0);
|
|
669
|
+
__decorate([
|
|
670
|
+
property({ reflect: true, attribute: 'edit-button-position' })
|
|
671
|
+
], SnapshotNavList.prototype, "editButtonPosition", void 0);
|
|
672
|
+
__decorate([
|
|
673
|
+
property({ attribute: 'edit-icon' })
|
|
674
|
+
], SnapshotNavList.prototype, "editIcon", void 0);
|
|
553
675
|
__decorate([
|
|
554
676
|
state()
|
|
555
677
|
], SnapshotNavList.prototype, "thumbs", void 0);
|
|
556
678
|
__decorate([
|
|
557
679
|
state()
|
|
558
680
|
], SnapshotNavList.prototype, "loadingIds", void 0);
|
|
559
|
-
|
|
681
|
+
// Guarded so importing this package in a DOM-less process (SSR, Node, a test
|
|
682
|
+
// runner without jsdom) is a no-op instead of a crash.
|
|
683
|
+
if (typeof customElements !== 'undefined' && !customElements.get('snapshot-nav-list')) {
|
|
560
684
|
customElements.define('snapshot-nav-list', SnapshotNavList);
|
|
561
685
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EncodeOptions } from './encode';
|
|
2
|
+
import type { SnapshotKey, SnapshotStorage } from './snapshot-storage';
|
|
2
3
|
export interface SnapshotServiceConfig {
|
|
3
4
|
/** Provide your own backend-backed implementation to persist across devices. */
|
|
4
5
|
storage?: SnapshotStorage;
|
|
@@ -11,21 +12,85 @@ export interface SnapshotServiceConfig {
|
|
|
11
12
|
* their keys and live-update notifications can't collide.
|
|
12
13
|
*/
|
|
13
14
|
keyPrefix?: string;
|
|
15
|
+
/** Re-encode applied to every capture before it's stored. Defaults to none (the raw html2canvas PNG). */
|
|
16
|
+
encode?: EncodeOptions;
|
|
17
|
+
/**
|
|
18
|
+
* Escape hatch for an existing store whose keys don't follow
|
|
19
|
+
* `keyPrefix + id [+ '@' + variant]` — return the key to use. Note that
|
|
20
|
+
* `parseKey()` (and therefore `prune()`) can't reverse a custom shape.
|
|
21
|
+
*/
|
|
22
|
+
keyFor?: (key: SnapshotKey) => string;
|
|
23
|
+
}
|
|
24
|
+
/** `url` is null when the id was removed (or invalidated) rather than (re)captured. */
|
|
25
|
+
type Listener = (id: string, url: string | null, variant?: string) => void;
|
|
26
|
+
export interface VariantOptions {
|
|
27
|
+
/**
|
|
28
|
+
* A second dimension on the id — a theme, a density, a locale. Stored under
|
|
29
|
+
* its own key, so `get(id, { variant: 'dark' })` never returns the light one.
|
|
30
|
+
*/
|
|
31
|
+
variant?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CaptureOptions extends VariantOptions {
|
|
34
|
+
/** Per-call override of the instance `scale`. */
|
|
35
|
+
scale?: number;
|
|
36
|
+
/** Per-call override of the instance `encode`. */
|
|
37
|
+
encode?: EncodeOptions;
|
|
14
38
|
}
|
|
15
|
-
/** `url` is null when the id was removed rather than (re)captured. */
|
|
16
|
-
type Listener = (id: string, url: string | null) => void;
|
|
17
39
|
export declare class SnapshotService {
|
|
18
40
|
private storage;
|
|
19
41
|
private scale;
|
|
20
42
|
private keyPrefix;
|
|
21
|
-
private
|
|
43
|
+
private encode?;
|
|
44
|
+
private keyFor?;
|
|
45
|
+
/** Absent under Node/SSR (or any environment without BroadcastChannel) — same-tab notification still works. */
|
|
46
|
+
private channel?;
|
|
22
47
|
private listeners;
|
|
48
|
+
/** In-flight captures, keyed by storage key, so two callers racing on the same view do one render. */
|
|
49
|
+
private capturing;
|
|
23
50
|
constructor(config?: SnapshotServiceConfig);
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The fully-qualified storage key for an id (+ variant) under this instance.
|
|
53
|
+
* `id`/`variant` are `encodeURIComponent`-escaped before joining, so a `@`
|
|
54
|
+
* (or any other character) inside either can never be mistaken for the
|
|
55
|
+
* separator itself — without escaping, `id: 'a@b'` and `id: 'a', variant: 'b'`
|
|
56
|
+
* would otherwise land on the exact same key.
|
|
57
|
+
*/
|
|
58
|
+
keyOf(id: string, opts?: VariantOptions): SnapshotKey;
|
|
59
|
+
/**
|
|
60
|
+
* Inverse of `keyOf()` for the default key shape — splits a stored key back
|
|
61
|
+
* into `{ id, variant }`. Returns `null` for a key belonging to a different
|
|
62
|
+
* `keyPrefix`, or for any key when a custom `keyFor` is configured (a custom
|
|
63
|
+
* shape isn't reversible).
|
|
64
|
+
*/
|
|
65
|
+
parseKey(key: string): SnapshotKey | null;
|
|
66
|
+
/**
|
|
67
|
+
* Capture works on any element — a snapshot-nav-list item is one convention,
|
|
68
|
+
* not a requirement. Concurrent calls for the same id/variant share one
|
|
69
|
+
* render instead of racing two.
|
|
70
|
+
*/
|
|
71
|
+
capture(el: HTMLElement, id: string, opts?: CaptureOptions): Promise<string>;
|
|
72
|
+
private runCapture;
|
|
73
|
+
get(id: string, opts?: VariantOptions): Promise<string | null>;
|
|
74
|
+
/**
|
|
75
|
+
* Batch read, keyed by bare id. Uses the storage's `loadMany()` when it has
|
|
76
|
+
* one (a single query/round-trip for a whole list) and falls back to parallel
|
|
77
|
+
* `load()`s when it doesn't.
|
|
78
|
+
*/
|
|
79
|
+
getMany(ids: string[], opts?: VariantOptions): Promise<Map<string, string | null>>;
|
|
27
80
|
/** Deletes a stored snapshot and notifies subscribers (this tab and others) that `id` is gone. */
|
|
28
|
-
remove(id: string): Promise<void>;
|
|
81
|
+
remove(id: string, opts?: VariantOptions): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Deletes every stored snapshot (all variants) whose id isn't in `keepIds`,
|
|
84
|
+
* so thumbnails don't outlive the entities they belong to. Requires a storage
|
|
85
|
+
* that implements `keys()`; returns the number of snapshots removed.
|
|
86
|
+
*/
|
|
87
|
+
prune(keepIds: Iterable<string>): Promise<number>;
|
|
88
|
+
/**
|
|
89
|
+
* Warms the storage for a list of ids ahead of render (one `loadMany()` where
|
|
90
|
+
* the storage supports it) and publishes whatever it finds, so any mounted
|
|
91
|
+
* `<snapshot-nav-list>` paints from the first frame instead of spinning.
|
|
92
|
+
*/
|
|
93
|
+
prefetch(ids: string[], opts?: VariantOptions): Promise<void>;
|
|
29
94
|
/**
|
|
30
95
|
* Announces a URL for `id` to subscribers (this tab and others) without
|
|
31
96
|
* touching storage. For a storage layer that resolves a fresher value
|
|
@@ -33,7 +98,7 @@ export declare class SnapshotService {
|
|
|
33
98
|
* local cache in front of a slower authoritative database — call this once
|
|
34
99
|
* the slow read settles so any mounted <snapshot-nav-list> updates live.
|
|
35
100
|
*/
|
|
36
|
-
publish(id: string, url: string): void;
|
|
101
|
+
publish(id: string, url: string, opts?: VariantOptions): void;
|
|
37
102
|
/**
|
|
38
103
|
* Tells subscribers to drop their locally cached thumbnail for `id` and
|
|
39
104
|
* treat it as not-yet-loaded — without deleting anything from storage.
|
|
@@ -41,12 +106,22 @@ export declare class SnapshotService {
|
|
|
41
106
|
* it again. Useful when the underlying data changed out from under the
|
|
42
107
|
* cache (or, for a demo, to replay a loading state on demand).
|
|
43
108
|
*/
|
|
44
|
-
invalidate(id: string): void;
|
|
45
|
-
subscribe(cb: Listener): () =>
|
|
109
|
+
invalidate(id: string, opts?: VariantOptions): void;
|
|
110
|
+
subscribe(cb: Listener): () => void;
|
|
46
111
|
/** Releases the cross-tab BroadcastChannel. Call when this instance (a non-default, namespaced one) is no longer needed. */
|
|
47
112
|
close(): void;
|
|
48
113
|
private notify;
|
|
49
114
|
}
|
|
50
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* The shared IndexedDB-backed instance, created on first call — so importing
|
|
117
|
+
* this package never opens a BroadcastChannel or an IndexedDB connection an
|
|
118
|
+
* app that brings its own `SnapshotService` would never use.
|
|
119
|
+
*/
|
|
120
|
+
export declare function getDefaultSnapshotService(): SnapshotService;
|
|
121
|
+
/**
|
|
122
|
+
* @deprecated Use `getDefaultSnapshotService()`. A lazy stand-in for the
|
|
123
|
+
* default instance: it forwards every access to the real service, constructing
|
|
124
|
+
* it on first touch rather than at import time.
|
|
125
|
+
*/
|
|
51
126
|
export declare const snapshotService: SnapshotService;
|
|
52
127
|
export {};
|