@anton-gustafsson/snapshot-core 0.0.5 → 0.0.7
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/snapshot-nav-list.d.ts +17 -5
- package/dist/snapshot-nav-list.js +169 -23
- package/package.json +1 -1
|
@@ -8,7 +8,9 @@ export interface NavItem {
|
|
|
8
8
|
route?: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
}
|
|
11
|
-
export type SnapshotNavListVariant = 'list' | 'icon-only';
|
|
11
|
+
export type SnapshotNavListVariant = 'list' | 'icon-only' | 'card';
|
|
12
|
+
/** `overlay` floats the edit button over the thumbnail (top-right, reveals on hover); `meta` pins it to the right edge of the title's line (description below), always visible. */
|
|
13
|
+
export type SnapshotNavListEditButtonPosition = 'overlay' | 'meta';
|
|
12
14
|
/**
|
|
13
15
|
* Visual identity: a contact sheet. Every tile is a "frame" — numbered like a strip
|
|
14
16
|
* of negatives — because that's literally what a snapshot thumbnail is. All colors
|
|
@@ -21,16 +23,22 @@ export declare class SnapshotNavList extends LitElement {
|
|
|
21
23
|
variant: SnapshotNavListVariant;
|
|
22
24
|
/** icon-only tile overlay: tint behind the title so it stays legible over any image. Transparent by default — opt into a scrim explicitly. */
|
|
23
25
|
overlayTint: 'dark' | 'light' | 'none';
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
+
/** caption background tint strength, 0-1 */
|
|
27
|
+
textOverlayOpacity: number;
|
|
28
|
+
/** image scrim tint strength, 0-1 — 0 keeps the image clear (blur only) */
|
|
29
|
+
imageOverlayOpacity: number;
|
|
26
30
|
/** backdrop blur behind the title, in px */
|
|
27
31
|
overlayBlur: number;
|
|
28
32
|
/** icon-only only: 'bottom' is the caption strip (default), 'center' centers a larger title. */
|
|
29
33
|
labelPosition: 'bottom' | 'center';
|
|
30
34
|
/** Defaults to the shared singleton — set your own instance (e.g. a namespaced or custom-storage SnapshotService) per <snapshot-nav-list> if needed. */
|
|
31
35
|
snapshotService: SnapshotService;
|
|
32
|
-
/** Shows
|
|
36
|
+
/** 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). */
|
|
33
37
|
editable: boolean;
|
|
38
|
+
/** 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. */
|
|
39
|
+
editButtonPosition: SnapshotNavListEditButtonPosition;
|
|
40
|
+
/** 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>`). */
|
|
41
|
+
editIcon: string;
|
|
34
42
|
private thumbs;
|
|
35
43
|
private loadingIds;
|
|
36
44
|
/** In-flight dedup guard, separate from `loadingIds` (which is only for spinner display) so a repeat `loadThumb` call for an id already being fetched is a no-op. */
|
|
@@ -42,9 +50,13 @@ export declare class SnapshotNavList extends LitElement {
|
|
|
42
50
|
willUpdate(changed: Map<string, unknown>): void;
|
|
43
51
|
updated(changed: Map<string, unknown>): void;
|
|
44
52
|
private loadThumb;
|
|
45
|
-
|
|
53
|
+
/** image scrim: blur + its own (usually 0) tint strength — independent of the caption's. */
|
|
54
|
+
private get imageOverlayStyle();
|
|
55
|
+
/** caption background: tint (to pop the text) + the same blur. */
|
|
56
|
+
private get metaStyle();
|
|
46
57
|
private select;
|
|
47
58
|
private edit;
|
|
59
|
+
private renderEditButton;
|
|
48
60
|
render(): import("lit-html").TemplateResult<1>;
|
|
49
61
|
}
|
|
50
62
|
declare global {
|
|
@@ -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
|
|
@@ -25,16 +26,22 @@ export class SnapshotNavList extends LitElement {
|
|
|
25
26
|
this.variant = 'icon-only';
|
|
26
27
|
/** icon-only tile overlay: tint behind the title so it stays legible over any image. Transparent by default — opt into a scrim explicitly. */
|
|
27
28
|
this.overlayTint = 'none';
|
|
28
|
-
/**
|
|
29
|
-
this.
|
|
29
|
+
/** caption background tint strength, 0-1 */
|
|
30
|
+
this.textOverlayOpacity = 0.35;
|
|
31
|
+
/** image scrim tint strength, 0-1 — 0 keeps the image clear (blur only) */
|
|
32
|
+
this.imageOverlayOpacity = 0;
|
|
30
33
|
/** backdrop blur behind the title, in px */
|
|
31
34
|
this.overlayBlur = 0;
|
|
32
35
|
/** icon-only only: 'bottom' is the caption strip (default), 'center' centers a larger title. */
|
|
33
36
|
this.labelPosition = 'bottom';
|
|
34
37
|
/** Defaults to the shared singleton — set your own instance (e.g. a namespaced or custom-storage SnapshotService) per <snapshot-nav-list> if needed. */
|
|
35
38
|
this.snapshotService = defaultSnapshotService;
|
|
36
|
-
/** Shows
|
|
39
|
+
/** 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). */
|
|
37
40
|
this.editable = false;
|
|
41
|
+
/** 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. */
|
|
42
|
+
this.editButtonPosition = 'overlay';
|
|
43
|
+
/** 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>`). */
|
|
44
|
+
this.editIcon = DEFAULT_EDIT_ICON;
|
|
38
45
|
this.thumbs = new Map();
|
|
39
46
|
this.loadingIds = new Set();
|
|
40
47
|
/** In-flight dedup guard, separate from `loadingIds` (which is only for spinner display) so a repeat `loadThumb` call for an id already being fetched is a no-op. */
|
|
@@ -171,7 +178,7 @@ export class SnapshotNavList extends LitElement {
|
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
/* tint
|
|
181
|
+
/* independent from .meta's tint — image-overlay-opacity defaults to 0 so the image stays clear (blur only) */
|
|
175
182
|
.image-overlay {
|
|
176
183
|
position: absolute;
|
|
177
184
|
inset: 0;
|
|
@@ -241,6 +248,50 @@ export class SnapshotNavList extends LitElement {
|
|
|
241
248
|
outline: 2px solid var(--frame-accent);
|
|
242
249
|
outline-offset: 1px;
|
|
243
250
|
}
|
|
251
|
+
.edit-button svg {
|
|
252
|
+
width: 1em;
|
|
253
|
+
height: 1em;
|
|
254
|
+
display: block;
|
|
255
|
+
fill: currentColor;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Transparent by default (display: contents) so the existing per-variant
|
|
259
|
+
.label/.meta rules — including icon-only's absolute caption strip —
|
|
260
|
+
keep applying unchanged; it only becomes a real row when the edit
|
|
261
|
+
button moves in beside the title. */
|
|
262
|
+
.label-row {
|
|
263
|
+
display: contents;
|
|
264
|
+
}
|
|
265
|
+
/* edit-button-position="meta": button on the title's line, description
|
|
266
|
+
still on its own line underneath. Not offered for icon-only, whose
|
|
267
|
+
.meta is an absolutely positioned overlay strip — the overlay button is
|
|
268
|
+
already the right place there. */
|
|
269
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .label-row {
|
|
270
|
+
display: flex;
|
|
271
|
+
align-items: center;
|
|
272
|
+
gap: 0.5rem;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
}
|
|
275
|
+
/* the title takes the whole row so the button lands on the card's right
|
|
276
|
+
edge, still on the title's own line (the description sits below it);
|
|
277
|
+
min-width: 0 keeps a long title ellipsising instead of pushing out. */
|
|
278
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .label {
|
|
279
|
+
flex: 1;
|
|
280
|
+
min-width: 0;
|
|
281
|
+
}
|
|
282
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .edit-button {
|
|
283
|
+
position: static;
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
/* in-flow, over the host's own background: currentColor-derived instead
|
|
286
|
+
of the overlay's dark scrim, and always visible (it isn't covering
|
|
287
|
+
anything, so hiding it until hover would just make it hard to find) */
|
|
288
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
289
|
+
color: inherit;
|
|
290
|
+
opacity: 1;
|
|
291
|
+
}
|
|
292
|
+
:host([edit-button-position='meta']:not([variant='icon-only'])) .edit-button:hover {
|
|
293
|
+
background: color-mix(in srgb, currentColor 20%, transparent);
|
|
294
|
+
}
|
|
244
295
|
|
|
245
296
|
/* list: a compact thumb reads better in a narrow sidebar than the grid's 160x100 */
|
|
246
297
|
:host([variant='list']) .thumb-wrap {
|
|
@@ -298,6 +349,72 @@ export class SnapshotNavList extends LitElement {
|
|
|
298
349
|
font-weight: 600;
|
|
299
350
|
text-align: center;
|
|
300
351
|
}
|
|
352
|
+
|
|
353
|
+
/* card: a contained (never cropped) preview above real body text below it —
|
|
354
|
+
text never sits on top of the image, so unlike icon-only it needs no
|
|
355
|
+
overlay tint to stay legible. Modeled on a typical "preview card"
|
|
356
|
+
pattern: framed shot, title + description underneath, shadow on hover. */
|
|
357
|
+
:host([variant='card']) ul {
|
|
358
|
+
display: grid;
|
|
359
|
+
grid-template-columns: repeat(auto-fill, minmax(var(--snapshot-nav-list-card-min-width, 220px), 1fr));
|
|
360
|
+
gap: var(--snapshot-nav-list-card-gap, 1.25rem);
|
|
361
|
+
}
|
|
362
|
+
:host([variant='card']) li {
|
|
363
|
+
flex-direction: column;
|
|
364
|
+
align-items: stretch;
|
|
365
|
+
gap: 0;
|
|
366
|
+
padding: var(--snapshot-nav-list-card-padding, 0.5rem);
|
|
367
|
+
background: var(--snapshot-nav-list-card-bg, transparent);
|
|
368
|
+
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
|
|
369
|
+
box-shadow: none;
|
|
370
|
+
transition:
|
|
371
|
+
box-shadow var(--snapshot-nav-list-card-transition-dur, 0.15s) ease,
|
|
372
|
+
border-color var(--snapshot-nav-list-card-transition-dur, 0.15s) ease;
|
|
373
|
+
}
|
|
374
|
+
:host([variant='card']) li:hover,
|
|
375
|
+
:host([variant='card']) li:focus-visible {
|
|
376
|
+
background: var(--snapshot-nav-list-card-bg, transparent);
|
|
377
|
+
box-shadow: var(--snapshot-nav-list-card-shadow, 0 2px 8px color-mix(in srgb, currentColor 18%, transparent));
|
|
378
|
+
border-color: color-mix(in srgb, currentColor 22%, transparent);
|
|
379
|
+
}
|
|
380
|
+
:host([variant='card']) li:focus-visible {
|
|
381
|
+
outline: none;
|
|
382
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--frame-accent) 55%, transparent);
|
|
383
|
+
}
|
|
384
|
+
:host([variant='card']) li:focus-visible .thumb-wrap {
|
|
385
|
+
outline: none;
|
|
386
|
+
}
|
|
387
|
+
:host([variant='card']) .thumb-wrap {
|
|
388
|
+
width: 100%;
|
|
389
|
+
height: auto;
|
|
390
|
+
aspect-ratio: 2 / 1;
|
|
391
|
+
border-radius: var(--snapshot-nav-list-radius-sm, 7px);
|
|
392
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 12%, transparent);
|
|
393
|
+
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
394
|
+
display: grid;
|
|
395
|
+
place-items: center;
|
|
396
|
+
}
|
|
397
|
+
:host([variant='card']) .thumb-wrap::before,
|
|
398
|
+
:host([variant='card']) .thumb-wrap::after {
|
|
399
|
+
display: none;
|
|
400
|
+
}
|
|
401
|
+
:host([variant='card']) img.thumb {
|
|
402
|
+
/* contain, not cover — the whole dashboard stays readable, nothing cropped */
|
|
403
|
+
object-fit: contain;
|
|
404
|
+
}
|
|
405
|
+
:host([variant='card']) .meta {
|
|
406
|
+
padding: 0.75rem 0.5rem 0.5rem;
|
|
407
|
+
gap: 0.25rem;
|
|
408
|
+
}
|
|
409
|
+
:host([variant='card']) .label {
|
|
410
|
+
white-space: normal;
|
|
411
|
+
font-size: 1rem;
|
|
412
|
+
font-weight: 600;
|
|
413
|
+
}
|
|
414
|
+
:host([variant='card']) .description {
|
|
415
|
+
white-space: normal;
|
|
416
|
+
font-size: 0.8125rem;
|
|
417
|
+
}
|
|
301
418
|
`; }
|
|
302
419
|
subscribeToService() {
|
|
303
420
|
this.unsubscribe?.();
|
|
@@ -371,13 +488,24 @@ export class SnapshotNavList extends LitElement {
|
|
|
371
488
|
this.requestUpdate();
|
|
372
489
|
}
|
|
373
490
|
}
|
|
374
|
-
|
|
491
|
+
/** image scrim: blur + its own (usually 0) tint strength — independent of the caption's. */
|
|
492
|
+
get imageOverlayStyle() {
|
|
493
|
+
const blur = `${this.overlayBlur}px`;
|
|
494
|
+
if (this.overlayTint === 'none' || this.imageOverlayOpacity === 0) {
|
|
495
|
+
return { '--overlay-blur': blur };
|
|
496
|
+
}
|
|
497
|
+
const tintColor = this.overlayTint === 'light' ? '#fff' : '#000';
|
|
498
|
+
const bg = `color-mix(in srgb, ${tintColor} ${Math.round(this.imageOverlayOpacity * 100)}%, transparent)`;
|
|
499
|
+
return { '--overlay-bg': bg, '--overlay-blur': blur };
|
|
500
|
+
}
|
|
501
|
+
/** caption background: tint (to pop the text) + the same blur. */
|
|
502
|
+
get metaStyle() {
|
|
375
503
|
const blur = `${this.overlayBlur}px`;
|
|
376
504
|
if (this.overlayTint === 'none') {
|
|
377
505
|
return { '--overlay-bg': 'transparent', '--overlay-text': 'inherit', '--overlay-blur': blur };
|
|
378
506
|
}
|
|
379
507
|
const tintColor = this.overlayTint === 'light' ? '#fff' : '#000';
|
|
380
|
-
const bg = `color-mix(in srgb, ${tintColor} ${Math.round(this.
|
|
508
|
+
const bg = `color-mix(in srgb, ${tintColor} ${Math.round(this.textOverlayOpacity * 100)}%, transparent)`;
|
|
381
509
|
const text = this.overlayTint === 'light' ? '#111' : '#fff';
|
|
382
510
|
return { '--overlay-bg': bg, '--overlay-text': text, '--overlay-blur': blur };
|
|
383
511
|
}
|
|
@@ -396,8 +524,24 @@ export class SnapshotNavList extends LitElement {
|
|
|
396
524
|
composed: true,
|
|
397
525
|
}));
|
|
398
526
|
}
|
|
527
|
+
renderEditButton(item) {
|
|
528
|
+
const icon = this.editIcon || DEFAULT_EDIT_ICON;
|
|
529
|
+
return html `<button
|
|
530
|
+
type="button"
|
|
531
|
+
class="edit-button"
|
|
532
|
+
part="edit-button"
|
|
533
|
+
aria-label="Edit ${item.label}"
|
|
534
|
+
@click=${(e) => this.edit(e, item)}
|
|
535
|
+
>
|
|
536
|
+
${isMarkupIcon(icon) ? unsafeHTML(icon) : icon}
|
|
537
|
+
</button>`;
|
|
538
|
+
}
|
|
399
539
|
render() {
|
|
400
|
-
const
|
|
540
|
+
const imageOverlayStyle = this.imageOverlayStyle;
|
|
541
|
+
const metaStyle = this.metaStyle;
|
|
542
|
+
// icon-only's caption is itself an overlay strip on the image, so there's
|
|
543
|
+
// no in-flow text row to put the button in — fall back to the overlay.
|
|
544
|
+
const editInMeta = this.editButtonPosition === 'meta' && this.variant !== 'icon-only';
|
|
401
545
|
return html `
|
|
402
546
|
<ul role="listbox">
|
|
403
547
|
${this.items.map((item) => html `
|
|
@@ -420,21 +564,14 @@ export class SnapshotNavList extends LitElement {
|
|
|
420
564
|
>${item.icon ? (isMarkupIcon(item.icon) ? unsafeHTML(item.icon) : item.icon) : ''}</span
|
|
421
565
|
>
|
|
422
566
|
</div>`}
|
|
423
|
-
<div class="image-overlay" part="overlay" style=${styleMap(
|
|
424
|
-
${this.editable
|
|
425
|
-
? html `<button
|
|
426
|
-
type="button"
|
|
427
|
-
class="edit-button"
|
|
428
|
-
part="edit-button"
|
|
429
|
-
aria-label="Edit ${item.label}"
|
|
430
|
-
@click=${(e) => this.edit(e, item)}
|
|
431
|
-
>
|
|
432
|
-
✎
|
|
433
|
-
</button>`
|
|
434
|
-
: ''}
|
|
567
|
+
<div class="image-overlay" part="overlay" style=${styleMap(imageOverlayStyle)}></div>
|
|
568
|
+
${this.editable && !editInMeta ? this.renderEditButton(item) : ''}
|
|
435
569
|
</div>
|
|
436
|
-
<div class="meta" part="meta" style=${styleMap(
|
|
437
|
-
<
|
|
570
|
+
<div class="meta" part="meta" style=${styleMap(metaStyle)}>
|
|
571
|
+
<div class="label-row" part="label-row">
|
|
572
|
+
<span class="label" part="label">${item.label}</span>
|
|
573
|
+
${this.editable && editInMeta ? this.renderEditButton(item) : ''}
|
|
574
|
+
</div>
|
|
438
575
|
${item.description ? html `<span class="description" part="description">${item.description}</span>` : ''}
|
|
439
576
|
</div>
|
|
440
577
|
</li>
|
|
@@ -453,8 +590,11 @@ __decorate([
|
|
|
453
590
|
property({ attribute: 'overlay-tint' })
|
|
454
591
|
], SnapshotNavList.prototype, "overlayTint", void 0);
|
|
455
592
|
__decorate([
|
|
456
|
-
property({ type: Number, attribute: 'overlay-opacity' })
|
|
457
|
-
], SnapshotNavList.prototype, "
|
|
593
|
+
property({ type: Number, attribute: 'text-overlay-opacity' })
|
|
594
|
+
], SnapshotNavList.prototype, "textOverlayOpacity", void 0);
|
|
595
|
+
__decorate([
|
|
596
|
+
property({ type: Number, attribute: 'image-overlay-opacity' })
|
|
597
|
+
], SnapshotNavList.prototype, "imageOverlayOpacity", void 0);
|
|
458
598
|
__decorate([
|
|
459
599
|
property({ type: Number, attribute: 'overlay-blur' })
|
|
460
600
|
], SnapshotNavList.prototype, "overlayBlur", void 0);
|
|
@@ -467,6 +607,12 @@ __decorate([
|
|
|
467
607
|
__decorate([
|
|
468
608
|
property({ type: Boolean })
|
|
469
609
|
], SnapshotNavList.prototype, "editable", void 0);
|
|
610
|
+
__decorate([
|
|
611
|
+
property({ reflect: true, attribute: 'edit-button-position' })
|
|
612
|
+
], SnapshotNavList.prototype, "editButtonPosition", void 0);
|
|
613
|
+
__decorate([
|
|
614
|
+
property({ attribute: 'edit-icon' })
|
|
615
|
+
], SnapshotNavList.prototype, "editIcon", void 0);
|
|
470
616
|
__decorate([
|
|
471
617
|
state()
|
|
472
618
|
], SnapshotNavList.prototype, "thumbs", void 0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anton-gustafsson/snapshot-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "A pluggable snapshot service that turns any DOM element into a stored, shareable image, plus an optional <snapshot-nav-list> web component to display them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|