@a2f0/agent-tool 0.1.32 → 0.1.34
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/package.json +1 -1
- package/src/screenshots/gallery.css +96 -74
- package/src/screenshots/gallery.ts +120 -6
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Styles for <screenshot-gallery> and <screenshot-carousel>
|
|
3
|
-
* (@a2f0/agent-tool/screenshots/gallery.css).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* (@a2f0/agent-tool/screenshots/gallery.css). Restyle through the --shots-*
|
|
4
|
+
* custom properties first, set on the elements or any ancestor. Defaults,
|
|
5
|
+
* which follow prefers-color-scheme, live in the internal --_shots-*
|
|
6
|
+
* properties.
|
|
7
|
+
*
|
|
8
|
+
* Specificity: rules on the host elements are zero (:where), so any site
|
|
9
|
+
* selector overrides them. Every rule on a part the elements render has
|
|
10
|
+
* exactly one class of specificity (0,1,0), with its context in :where(), so
|
|
11
|
+
* element resets such as `button { background: none }` never reach the
|
|
12
|
+
* parts, while any site rule with a class and an element or a second class
|
|
13
|
+
* (`screenshot-gallery .shots-frame`) still overrides them.
|
|
7
14
|
*/
|
|
8
15
|
|
|
9
16
|
:where(screenshot-gallery, screenshot-carousel) {
|
|
@@ -55,30 +62,30 @@
|
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
.shots-host :where(button) {
|
|
59
66
|
font: inherit;
|
|
60
67
|
color: inherit;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
:where(.shots-host) :where(button
|
|
70
|
+
:where(.shots-host) :where(button, a, [tabindex]):focus-visible {
|
|
64
71
|
outline: 2px solid var(--_shots-focus);
|
|
65
72
|
outline-offset: 2px;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
.shots-icon {
|
|
69
76
|
display: inline-flex;
|
|
70
77
|
width: 1.25rem;
|
|
71
78
|
height: 1.25rem;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
|
|
81
|
+
.shots-icon :where(svg) {
|
|
75
82
|
width: 100%;
|
|
76
83
|
height: 100%;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
/* --- States --- */
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
.shots-status {
|
|
82
89
|
display: grid;
|
|
83
90
|
place-items: center;
|
|
84
91
|
padding: 2rem 1rem;
|
|
@@ -90,13 +97,13 @@
|
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
/* Loading reserves the gallery's height so the page does not jump when it arrives. */
|
|
93
|
-
:where(screenshot-gallery)
|
|
100
|
+
:where(screenshot-gallery) .shots-status:where([data-state="loading"]) {
|
|
94
101
|
min-height: calc(var(--_shots-stage-height) + var(--_shots-thumb-height) + 7rem);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
/* --- Gallery --- */
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
.shots-gallery {
|
|
100
107
|
display: flex;
|
|
101
108
|
flex-direction: column;
|
|
102
109
|
border: 1px solid var(--_shots-border);
|
|
@@ -105,7 +112,7 @@
|
|
|
105
112
|
overflow: hidden;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
.shots-gallery:where([data-expanded]) {
|
|
109
116
|
position: fixed;
|
|
110
117
|
inset: 0;
|
|
111
118
|
z-index: var(--_shots-expanded-z);
|
|
@@ -113,7 +120,7 @@
|
|
|
113
120
|
border-radius: 0;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
.shots-toolbar {
|
|
117
124
|
display: flex;
|
|
118
125
|
flex-wrap: wrap;
|
|
119
126
|
align-items: center;
|
|
@@ -122,7 +129,7 @@
|
|
|
122
129
|
border-bottom: 1px solid var(--_shots-border);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
|
|
132
|
+
.shots-title {
|
|
126
133
|
flex: 1 1 10rem;
|
|
127
134
|
margin: 0;
|
|
128
135
|
font-weight: 600;
|
|
@@ -131,14 +138,14 @@
|
|
|
131
138
|
white-space: nowrap;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
|
|
141
|
+
.shots-toggle {
|
|
135
142
|
display: inline-flex;
|
|
136
143
|
padding: 2px;
|
|
137
144
|
border: 1px solid var(--_shots-border);
|
|
138
145
|
border-radius: 999px;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
|
|
148
|
+
.shots-toggle-button {
|
|
142
149
|
display: inline-flex;
|
|
143
150
|
align-items: center;
|
|
144
151
|
gap: 0.375rem;
|
|
@@ -149,17 +156,17 @@
|
|
|
149
156
|
cursor: pointer;
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
|
|
159
|
+
.shots-toggle-button:where([aria-pressed="true"]) {
|
|
153
160
|
background: var(--_shots-accent);
|
|
154
161
|
color: var(--_shots-accent-ink);
|
|
155
162
|
}
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
.shots-actions {
|
|
158
165
|
display: inline-flex;
|
|
159
166
|
gap: 0.25rem;
|
|
160
167
|
}
|
|
161
168
|
|
|
162
|
-
|
|
169
|
+
.shots-icon-button {
|
|
163
170
|
display: inline-grid;
|
|
164
171
|
place-items: center;
|
|
165
172
|
width: var(--_shots-button-size);
|
|
@@ -171,12 +178,12 @@
|
|
|
171
178
|
cursor: pointer;
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
|
|
181
|
+
.shots-icon-button:where(:disabled) {
|
|
175
182
|
opacity: 0.4;
|
|
176
183
|
cursor: default;
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
|
|
186
|
+
.shots-body {
|
|
180
187
|
display: flex;
|
|
181
188
|
flex: 1 1 auto;
|
|
182
189
|
flex-direction: column;
|
|
@@ -184,7 +191,7 @@
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
/* The main panel: one screen per page of a horizontal scroll-snap track. */
|
|
187
|
-
|
|
194
|
+
.shots-track {
|
|
188
195
|
position: relative;
|
|
189
196
|
display: flex;
|
|
190
197
|
height: var(--_shots-stage-height);
|
|
@@ -196,16 +203,16 @@
|
|
|
196
203
|
background: var(--_shots-surface-raised);
|
|
197
204
|
}
|
|
198
205
|
|
|
199
|
-
:where(.shots-gallery[data-expanded])
|
|
206
|
+
:where(.shots-gallery[data-expanded]) .shots-track {
|
|
200
207
|
flex: 1 1 auto;
|
|
201
208
|
height: auto;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
|
|
211
|
+
.shots-track::-webkit-scrollbar {
|
|
205
212
|
display: none;
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
|
|
215
|
+
.shots-slide {
|
|
209
216
|
display: flex;
|
|
210
217
|
flex: 0 0 100%;
|
|
211
218
|
align-items: center;
|
|
@@ -217,7 +224,7 @@
|
|
|
217
224
|
scroll-snap-stop: always;
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
.shots-frame {
|
|
221
228
|
display: block;
|
|
222
229
|
box-sizing: content-box;
|
|
223
230
|
max-width: 100%;
|
|
@@ -229,14 +236,14 @@
|
|
|
229
236
|
cursor: zoom-in;
|
|
230
237
|
}
|
|
231
238
|
|
|
232
|
-
|
|
239
|
+
.shots-image {
|
|
233
240
|
display: block;
|
|
234
241
|
max-width: 100%;
|
|
235
242
|
height: auto;
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
/* Phone and tablet captures sit in a device bezel sized from the capture (fitFrame). */
|
|
239
|
-
:where(.shots-gallery[data-frame="phone"], .shots-gallery[data-frame="tablet"])
|
|
246
|
+
:where(.shots-gallery[data-frame="phone"], .shots-gallery[data-frame="tablet"]) .shots-frame:where(:not(.shots-missing)) {
|
|
240
247
|
padding: var(--shots-frame-pad, 0.6rem);
|
|
241
248
|
border: 0;
|
|
242
249
|
border-radius: calc(var(--shots-frame-radius, 1.5rem) + var(--shots-frame-pad, 0.6rem));
|
|
@@ -244,11 +251,11 @@
|
|
|
244
251
|
box-shadow: inset 0 0 0 1px var(--_shots-bezel-edge), var(--_shots-shadow);
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
:where(.shots-gallery[data-frame="phone"], .shots-gallery[data-frame="tablet"]) :where(.shots-frame:not(.shots-missing) .shots-image
|
|
254
|
+
:where(.shots-gallery[data-frame="phone"], .shots-gallery[data-frame="tablet"]) :where(.shots-frame:not(.shots-missing)) .shots-image {
|
|
248
255
|
border-radius: var(--shots-frame-radius, 1.5rem);
|
|
249
256
|
}
|
|
250
257
|
|
|
251
|
-
|
|
258
|
+
.shots-missing {
|
|
252
259
|
display: grid;
|
|
253
260
|
place-items: center;
|
|
254
261
|
max-width: 24rem;
|
|
@@ -258,7 +265,7 @@
|
|
|
258
265
|
cursor: default;
|
|
259
266
|
}
|
|
260
267
|
|
|
261
|
-
|
|
268
|
+
.shots-navrow {
|
|
262
269
|
display: flex;
|
|
263
270
|
align-items: center;
|
|
264
271
|
justify-content: center;
|
|
@@ -266,7 +273,7 @@
|
|
|
266
273
|
padding: 0.5rem;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
|
|
276
|
+
.shots-counter {
|
|
270
277
|
min-width: 4.5rem;
|
|
271
278
|
margin: 0;
|
|
272
279
|
color: var(--_shots-ink-muted);
|
|
@@ -274,7 +281,9 @@
|
|
|
274
281
|
text-align: center;
|
|
275
282
|
}
|
|
276
283
|
|
|
277
|
-
:
|
|
284
|
+
/* Every thumbnail of a project has one width: the stage's typical capture shape (--_shots-thumb-ratio, set by the element) at the thumbnail height, within the 4.5rem to max-width range. */
|
|
285
|
+
.shots-filmstrip {
|
|
286
|
+
--_shots-thumb-width: clamp(4.5rem, calc(var(--_shots-thumb-height) * var(--_shots-thumb-ratio, 0.75)), calc(var(--_shots-thumb-max-width) - 0.5rem));
|
|
278
287
|
position: relative;
|
|
279
288
|
display: flex;
|
|
280
289
|
gap: 0.5rem;
|
|
@@ -284,12 +293,13 @@
|
|
|
284
293
|
scroll-snap-type: x proximity;
|
|
285
294
|
}
|
|
286
295
|
|
|
287
|
-
|
|
296
|
+
.shots-thumb {
|
|
288
297
|
display: flex;
|
|
289
298
|
flex: 0 0 auto;
|
|
290
299
|
flex-direction: column;
|
|
291
300
|
gap: 0.25rem;
|
|
292
|
-
|
|
301
|
+
box-sizing: content-box;
|
|
302
|
+
width: var(--_shots-thumb-width);
|
|
293
303
|
padding: 0.25rem;
|
|
294
304
|
border: 1px solid transparent;
|
|
295
305
|
border-radius: calc(var(--_shots-radius) / 2);
|
|
@@ -298,23 +308,21 @@
|
|
|
298
308
|
scroll-snap-align: start;
|
|
299
309
|
}
|
|
300
310
|
|
|
301
|
-
|
|
311
|
+
.shots-thumb:where([aria-current="true"]) {
|
|
302
312
|
border-color: var(--_shots-accent);
|
|
303
313
|
}
|
|
304
314
|
|
|
305
|
-
/*
|
|
306
|
-
|
|
315
|
+
/* A capture of another shape is cropped to the thumbnail, keeping its top. */
|
|
316
|
+
.shots-thumb-image {
|
|
307
317
|
display: block;
|
|
318
|
+
width: 100%;
|
|
308
319
|
height: var(--_shots-thumb-height);
|
|
309
|
-
min-width: 4.5rem;
|
|
310
|
-
max-width: calc(var(--_shots-thumb-max-width) - 0.5rem);
|
|
311
|
-
aspect-ratio: 3 / 4;
|
|
312
320
|
border-radius: 4px;
|
|
313
321
|
background: var(--_shots-surface-raised);
|
|
314
322
|
overflow: hidden;
|
|
315
323
|
}
|
|
316
324
|
|
|
317
|
-
|
|
325
|
+
.shots-thumb-image :where(img) {
|
|
318
326
|
display: block;
|
|
319
327
|
width: 100%;
|
|
320
328
|
height: 100%;
|
|
@@ -322,8 +330,11 @@
|
|
|
322
330
|
object-position: top;
|
|
323
331
|
}
|
|
324
332
|
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
/* Long labels never widen a thumbnail; they end in an ellipsis, with the full label in the button's title. */
|
|
334
|
+
.shots-thumb-label {
|
|
335
|
+
display: block;
|
|
336
|
+
min-width: 0;
|
|
337
|
+
max-width: 100%;
|
|
327
338
|
overflow: hidden;
|
|
328
339
|
color: var(--_shots-ink-muted);
|
|
329
340
|
font-size: 0.75rem;
|
|
@@ -332,11 +343,11 @@
|
|
|
332
343
|
white-space: nowrap;
|
|
333
344
|
}
|
|
334
345
|
|
|
335
|
-
:where(.shots-thumb[aria-current="true"])
|
|
346
|
+
:where(.shots-thumb[aria-current="true"]) .shots-thumb-label {
|
|
336
347
|
color: var(--_shots-ink);
|
|
337
348
|
}
|
|
338
349
|
|
|
339
|
-
|
|
350
|
+
.shots-lightbox {
|
|
340
351
|
max-width: 100vw;
|
|
341
352
|
max-height: 100vh;
|
|
342
353
|
padding: 0;
|
|
@@ -345,11 +356,11 @@
|
|
|
345
356
|
overflow: visible;
|
|
346
357
|
}
|
|
347
358
|
|
|
348
|
-
|
|
359
|
+
.shots-lightbox::backdrop {
|
|
349
360
|
background: var(--_shots-backdrop);
|
|
350
361
|
}
|
|
351
362
|
|
|
352
|
-
|
|
363
|
+
.shots-lightbox-backdrop {
|
|
353
364
|
position: fixed;
|
|
354
365
|
inset: 0;
|
|
355
366
|
border: 0;
|
|
@@ -357,11 +368,11 @@
|
|
|
357
368
|
cursor: zoom-out;
|
|
358
369
|
}
|
|
359
370
|
|
|
360
|
-
|
|
371
|
+
.shots-lightbox-frame {
|
|
361
372
|
position: relative;
|
|
362
373
|
}
|
|
363
374
|
|
|
364
|
-
|
|
375
|
+
.shots-lightbox-image {
|
|
365
376
|
display: block;
|
|
366
377
|
width: auto;
|
|
367
378
|
height: auto;
|
|
@@ -370,18 +381,18 @@
|
|
|
370
381
|
border-radius: var(--shots-lightbox-radius, 8px);
|
|
371
382
|
}
|
|
372
383
|
|
|
373
|
-
:where(.shots-lightbox[data-frame="phone"], .shots-lightbox[data-frame="tablet"])
|
|
384
|
+
:where(.shots-lightbox[data-frame="phone"], .shots-lightbox[data-frame="tablet"]) .shots-lightbox-frame {
|
|
374
385
|
padding: var(--shots-frame-pad, 0.75rem);
|
|
375
386
|
border-radius: calc(var(--shots-frame-radius, 2rem) + var(--shots-frame-pad, 0.75rem));
|
|
376
387
|
background: var(--_shots-bezel);
|
|
377
388
|
box-shadow: inset 0 0 0 1px var(--_shots-bezel-edge);
|
|
378
389
|
}
|
|
379
390
|
|
|
380
|
-
:where(.shots-lightbox[data-frame="phone"], .shots-lightbox[data-frame="tablet"])
|
|
391
|
+
:where(.shots-lightbox[data-frame="phone"], .shots-lightbox[data-frame="tablet"]) .shots-lightbox-image {
|
|
381
392
|
border-radius: var(--shots-frame-radius, 2rem);
|
|
382
393
|
}
|
|
383
394
|
|
|
384
|
-
|
|
395
|
+
.shots-lightbox-close {
|
|
385
396
|
position: fixed;
|
|
386
397
|
top: 1rem;
|
|
387
398
|
right: 1rem;
|
|
@@ -397,7 +408,7 @@
|
|
|
397
408
|
cursor: pointer;
|
|
398
409
|
}
|
|
399
410
|
|
|
400
|
-
:where(.shots-lightbox)
|
|
411
|
+
:where(.shots-lightbox) .shots-missing {
|
|
401
412
|
position: relative;
|
|
402
413
|
background: var(--_shots-surface);
|
|
403
414
|
border-radius: var(--_shots-radius);
|
|
@@ -405,11 +416,11 @@
|
|
|
405
416
|
|
|
406
417
|
/* --- Carousel --- */
|
|
407
418
|
|
|
408
|
-
|
|
419
|
+
.shots-carousel {
|
|
409
420
|
position: relative;
|
|
410
421
|
}
|
|
411
422
|
|
|
412
|
-
|
|
423
|
+
.shots-carousel-track {
|
|
413
424
|
position: relative;
|
|
414
425
|
overflow-x: auto;
|
|
415
426
|
overscroll-behavior-x: contain;
|
|
@@ -419,11 +430,11 @@
|
|
|
419
430
|
scrollbar-width: none;
|
|
420
431
|
}
|
|
421
432
|
|
|
422
|
-
|
|
433
|
+
.shots-carousel-track::-webkit-scrollbar {
|
|
423
434
|
display: none;
|
|
424
435
|
}
|
|
425
436
|
|
|
426
|
-
|
|
437
|
+
.shots-carousel-list {
|
|
427
438
|
display: flex;
|
|
428
439
|
gap: var(--_shots-carousel-gap);
|
|
429
440
|
margin: 0;
|
|
@@ -431,33 +442,33 @@
|
|
|
431
442
|
list-style: none;
|
|
432
443
|
}
|
|
433
444
|
|
|
434
|
-
|
|
445
|
+
.shots-carousel-slide {
|
|
435
446
|
flex: 0 0 var(--_shots-carousel-slide-width);
|
|
436
447
|
scroll-snap-align: start;
|
|
437
448
|
}
|
|
438
449
|
|
|
439
|
-
|
|
450
|
+
.shots-carousel-slide :where(figure) {
|
|
440
451
|
margin: 0;
|
|
441
452
|
}
|
|
442
453
|
|
|
443
|
-
|
|
454
|
+
.shots-carousel-slide :where(figcaption) {
|
|
444
455
|
margin-top: 0.75rem;
|
|
445
456
|
color: var(--_shots-ink-muted);
|
|
446
457
|
font-size: 0.875rem;
|
|
447
458
|
text-align: center;
|
|
448
459
|
}
|
|
449
460
|
|
|
450
|
-
|
|
461
|
+
.shots-device {
|
|
451
462
|
display: block;
|
|
452
463
|
color: inherit;
|
|
453
464
|
container-type: inline-size;
|
|
454
465
|
}
|
|
455
466
|
|
|
456
|
-
|
|
467
|
+
.shots-device :where(picture) {
|
|
457
468
|
display: block;
|
|
458
469
|
}
|
|
459
470
|
|
|
460
|
-
|
|
471
|
+
.shots-device :where(img) {
|
|
461
472
|
display: block;
|
|
462
473
|
width: 100%;
|
|
463
474
|
height: auto;
|
|
@@ -465,7 +476,7 @@
|
|
|
465
476
|
}
|
|
466
477
|
|
|
467
478
|
/* The bezel scales with the device's width, so it keeps its proportions at every size (FRAME_METRICS). */
|
|
468
|
-
:where(.shots-carousel[data-frame="phone"], .shots-carousel[data-frame="tablet"])
|
|
479
|
+
:where(.shots-carousel[data-frame="phone"], .shots-carousel[data-frame="tablet"]) .shots-device :where(picture) {
|
|
469
480
|
--shots-frame-pad: 3.5cqi;
|
|
470
481
|
--shots-frame-radius: calc((100cqi - 2 * var(--shots-frame-pad)) * 0.1214);
|
|
471
482
|
padding: var(--shots-frame-pad);
|
|
@@ -474,32 +485,43 @@
|
|
|
474
485
|
box-shadow: inset 0 0 0 1px var(--_shots-bezel-edge), var(--_shots-shadow);
|
|
475
486
|
}
|
|
476
487
|
|
|
477
|
-
:where(.shots-carousel[data-frame="tablet"])
|
|
488
|
+
:where(.shots-carousel[data-frame="tablet"]) .shots-device :where(picture) {
|
|
478
489
|
--shots-frame-pad: 3cqi;
|
|
479
490
|
--shots-frame-radius: calc((100cqi - 2 * var(--shots-frame-pad)) * 0.025);
|
|
480
491
|
}
|
|
481
492
|
|
|
482
|
-
:where(.shots-carousel[data-frame="phone"], .shots-carousel[data-frame="tablet"])
|
|
493
|
+
:where(.shots-carousel[data-frame="phone"], .shots-carousel[data-frame="tablet"]) .shots-device :where(img) {
|
|
483
494
|
border-radius: var(--shots-frame-radius);
|
|
484
495
|
}
|
|
485
496
|
|
|
486
|
-
|
|
497
|
+
.shots-device:where(a:focus-visible) {
|
|
487
498
|
outline: none;
|
|
488
499
|
}
|
|
489
500
|
|
|
490
|
-
|
|
501
|
+
.shots-device:where(a:focus-visible) :where(picture) {
|
|
491
502
|
outline: 2px solid var(--_shots-focus);
|
|
492
503
|
outline-offset: 3px;
|
|
493
504
|
}
|
|
494
505
|
|
|
495
|
-
|
|
506
|
+
.shots-carousel-controls {
|
|
496
507
|
display: flex;
|
|
508
|
+
flex-wrap: wrap;
|
|
509
|
+
align-items: center;
|
|
497
510
|
justify-content: flex-end;
|
|
498
511
|
gap: 0.5rem;
|
|
499
512
|
padding: 0 var(--_shots-carousel-padding);
|
|
500
513
|
}
|
|
501
514
|
|
|
502
|
-
|
|
515
|
+
/* The view-all link sits at the start of the controls row, before the step buttons. */
|
|
516
|
+
.shots-carousel-view-all {
|
|
517
|
+
margin-inline-end: auto;
|
|
518
|
+
color: var(--shots-link, var(--_shots-ink));
|
|
519
|
+
font-weight: 600;
|
|
520
|
+
text-decoration: underline;
|
|
521
|
+
text-underline-offset: 0.2em;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.shots-carousel-step {
|
|
503
525
|
display: inline-grid;
|
|
504
526
|
place-items: center;
|
|
505
527
|
width: var(--_shots-button-size);
|
|
@@ -512,16 +534,16 @@
|
|
|
512
534
|
}
|
|
513
535
|
|
|
514
536
|
@media (prefers-reduced-motion: no-preference) {
|
|
515
|
-
:
|
|
537
|
+
:is(.shots-track, .shots-carousel-track, .shots-filmstrip) {
|
|
516
538
|
scroll-behavior: smooth;
|
|
517
539
|
}
|
|
518
540
|
|
|
519
|
-
:where(a
|
|
541
|
+
.shots-device:where(a) :where(picture) {
|
|
520
542
|
transition: transform 200ms ease;
|
|
521
543
|
}
|
|
522
544
|
|
|
523
545
|
@media (hover: hover) {
|
|
524
|
-
|
|
546
|
+
.shots-device:where(a:hover) :where(picture) {
|
|
525
547
|
transform: translateY(-4px);
|
|
526
548
|
}
|
|
527
549
|
}
|
|
@@ -73,6 +73,58 @@ export function loopCopies(viewportWidth: number, copyWidth: number): number {
|
|
|
73
73
|
return copyWidth > 0 ? Math.max(1, Math.ceil(viewportWidth / copyWidth)) : 1;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** Wheel travel, in CSS pixels on a gesture's dominant axis, that steps one screen. */
|
|
77
|
+
export const WHEEL_STEP_THRESHOLD = 30;
|
|
78
|
+
/** A pause between wheel events this long, in milliseconds, ends a gesture, including a trackpad's inertia. */
|
|
79
|
+
export const WHEEL_GESTURE_IDLE = 200;
|
|
80
|
+
|
|
81
|
+
export interface WheelStepper {
|
|
82
|
+
/** Adds one wheel event's `delta` (pixels, signed) at `time` (ms); returns the step it completes: -1, 1, or 0. */
|
|
83
|
+
push(delta: number, time: number): -1 | 0 | 1;
|
|
84
|
+
reset(): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Turns a stream of wheel deltas into screen steps. Deltas accumulate until
|
|
89
|
+
* they reach `threshold`, so small wheel ticks add up, then the gesture steps
|
|
90
|
+
* once: further travel in the same direction, such as a trackpad's inertial
|
|
91
|
+
* tail, is ignored until the wheel pauses for `idle` ms. Travel of `threshold`
|
|
92
|
+
* in the other direction is a new gesture and steps back at once.
|
|
93
|
+
*/
|
|
94
|
+
export function createWheelStepper(threshold = WHEEL_STEP_THRESHOLD, idle = WHEEL_GESTURE_IDLE): WheelStepper {
|
|
95
|
+
let total = 0, last = -Infinity, stepped: -1 | 0 | 1 = 0;
|
|
96
|
+
return {
|
|
97
|
+
push(delta, time) {
|
|
98
|
+
if (!(time - last < idle)) { total = 0; stepped = 0; }
|
|
99
|
+
last = time;
|
|
100
|
+
if (!delta || !Number.isFinite(delta)) return 0;
|
|
101
|
+
const direction = delta > 0 ? 1 : -1;
|
|
102
|
+
// The step's own direction keeps the gesture going; an opposing blip does not add up.
|
|
103
|
+
if (direction === stepped) { total = 0; return 0; }
|
|
104
|
+
if (total && Math.sign(total) !== direction) total = 0;
|
|
105
|
+
total += delta;
|
|
106
|
+
if (Math.abs(total) < threshold) return 0;
|
|
107
|
+
total = 0;
|
|
108
|
+
stepped = direction;
|
|
109
|
+
return direction;
|
|
110
|
+
},
|
|
111
|
+
reset() { total = 0; last = -Infinity; stepped = 0; },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A wheel event's delta in CSS pixels; `page` is the size of one page of the scroller. */
|
|
116
|
+
export function wheelPixels(delta: number, deltaMode: number, page: number): number {
|
|
117
|
+
return deltaMode === 1 ? delta * 16 : deltaMode === 2 ? delta * page : delta;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Median width-to-height ratio of the captures, so one odd capture does not resize every thumbnail. */
|
|
121
|
+
export function typicalRatio(entries: readonly { width: number; height: number }[]): number | undefined {
|
|
122
|
+
const ratios = entries.filter(entry => entry.width > 0 && entry.height > 0).map(entry => entry.width / entry.height).sort((a, b) => a - b);
|
|
123
|
+
if (!ratios.length) return undefined;
|
|
124
|
+
const middle = ratios.length >> 1;
|
|
125
|
+
return ratios.length % 2 ? ratios[middle]! : (ratios[middle - 1]! + ratios[middle]!) / 2;
|
|
126
|
+
}
|
|
127
|
+
|
|
76
128
|
export interface ScreenshotChangeDetail { project: string; theme: string; screen: string; index: number; total: number }
|
|
77
129
|
|
|
78
130
|
export interface ScreenshotGalleryElement extends HTMLElement {
|
|
@@ -156,6 +208,19 @@ function status(root: HTMLElement, state: string, message: string): void {
|
|
|
156
208
|
root.replaceChildren(element("div", { class: "shots-status", role: "status", "data-state": state }, [element("p", { text: message })]));
|
|
157
209
|
}
|
|
158
210
|
|
|
211
|
+
/**
|
|
212
|
+
* A page can set `manifestData` before the elements are defined; that value is
|
|
213
|
+
* an own property that shadows the accessor. Take it off, so the constructor
|
|
214
|
+
* can pass it through the setter.
|
|
215
|
+
*/
|
|
216
|
+
function takePendingData(element: HTMLElement): { value: unknown } | undefined {
|
|
217
|
+
if (!Object.prototype.hasOwnProperty.call(element, "manifestData")) return undefined;
|
|
218
|
+
const own = element as unknown as Record<string, unknown>;
|
|
219
|
+
const value = own["manifestData"];
|
|
220
|
+
delete own["manifestData"];
|
|
221
|
+
return { value };
|
|
222
|
+
}
|
|
223
|
+
|
|
159
224
|
function defineGallery(): CustomElementConstructor {
|
|
160
225
|
return class ScreenshotGallery extends HTMLElement implements ScreenshotGalleryElement {
|
|
161
226
|
static observedAttributes = ["manifest"];
|
|
@@ -178,6 +243,13 @@ function defineGallery(): CustomElementConstructor {
|
|
|
178
243
|
#overflow = "";
|
|
179
244
|
#rendered = false;
|
|
180
245
|
#announced = "";
|
|
246
|
+
#wheel = createWheelStepper();
|
|
247
|
+
|
|
248
|
+
constructor() {
|
|
249
|
+
super();
|
|
250
|
+
const pending = takePendingData(this);
|
|
251
|
+
if (pending) this.manifestData = pending.value as ScreenshotManifest | null;
|
|
252
|
+
}
|
|
181
253
|
|
|
182
254
|
get manifestData(): ScreenshotManifest | null { return this.#data; }
|
|
183
255
|
set manifestData(value: ScreenshotManifest | null) { this.#data = value && parseScreenshotManifest(value); if (this.#connected) this.#load(); }
|
|
@@ -261,6 +333,7 @@ function defineGallery(): CustomElementConstructor {
|
|
|
261
333
|
|
|
262
334
|
const track = element("div", { class: "shots-track", tabindex: "0", role: "group", "aria-roledescription": "carousel" });
|
|
263
335
|
onScrollSettle(track, () => this.#settle(), signal);
|
|
336
|
+
track.addEventListener("wheel", (event: WheelEvent) => this.#onWheel(event), { passive: false, signal });
|
|
264
337
|
const previous = element("button", { type: "button", class: "shots-icon-button", "data-action": "previous", "aria-label": "Previous screen" }, [icon("prev")]);
|
|
265
338
|
const next = element("button", { type: "button", class: "shots-icon-button", "data-action": "next", "aria-label": "Next screen" }, [icon("next")]);
|
|
266
339
|
previous.addEventListener("click", () => this.step(-1), { signal });
|
|
@@ -297,6 +370,30 @@ function defineGallery(): CustomElementConstructor {
|
|
|
297
370
|
else if (event.key === "ArrowLeft" || (inside && event.key === "ArrowUp")) { event.preventDefault(); this.step(-1); }
|
|
298
371
|
}
|
|
299
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Steps once per wheel gesture over the main panel, instead of letting
|
|
375
|
+
* scroll snapping pull a small scroll back to the same screen. Horizontal
|
|
376
|
+
* travel (a trackpad swipe, a tilt wheel, Shift with a wheel) always steps;
|
|
377
|
+
* vertical travel steps only while the gallery is expanded, or with
|
|
378
|
+
* `vertical-wheel` until the first or last screen, so the page still scrolls.
|
|
379
|
+
*/
|
|
380
|
+
#onWheel(event: WheelEvent): void {
|
|
381
|
+
const track = this.#track;
|
|
382
|
+
if (!track || event.defaultPrevented || event.ctrlKey || event.metaKey) return;
|
|
383
|
+
let x = event.deltaX, y = event.deltaY;
|
|
384
|
+
if (event.shiftKey && !x) { x = y; y = 0; }
|
|
385
|
+
const horizontal = Math.abs(x) > Math.abs(y);
|
|
386
|
+
const delta = wheelPixels(horizontal ? x : y, event.deltaMode, horizontal ? track.clientWidth : track.clientHeight);
|
|
387
|
+
if (!delta) return;
|
|
388
|
+
const next = this.#index + Math.sign(delta);
|
|
389
|
+
const inRange = next >= 0 && next < this.#screens.length;
|
|
390
|
+
if (!horizontal && !this.#expanded && (!this.hasAttribute("vertical-wheel") || !inRange)) return;
|
|
391
|
+
event.preventDefault();
|
|
392
|
+
const step = this.#wheel.push(delta, event.timeStamp);
|
|
393
|
+
// The wheel does not wrap: past either end it waits, like a native scroller.
|
|
394
|
+
if (step && this.#index + step >= 0 && this.#index + step < this.#screens.length) this.#select(this.#index + step, scrollBehavior());
|
|
395
|
+
}
|
|
396
|
+
|
|
300
397
|
#setExpanded(expanded: boolean): void {
|
|
301
398
|
if (expanded === this.#expanded || !this.#root) return;
|
|
302
399
|
this.#expanded = expanded;
|
|
@@ -321,14 +418,20 @@ function defineGallery(): CustomElementConstructor {
|
|
|
321
418
|
this.#screens = projectScreens(manifest, this.#project);
|
|
322
419
|
this.#index = Math.max(0, screen === undefined ? 0 : this.#screens.indexOf(screen));
|
|
323
420
|
this.#loaded.clear();
|
|
421
|
+
this.#wheel.reset();
|
|
324
422
|
const frame = projectFrame(manifest, this.#project);
|
|
325
423
|
this.#root!.dataset["frame"] = frame;
|
|
326
424
|
this.#root!.dataset["project"] = this.#project;
|
|
327
425
|
for (const button of this.#root!.querySelectorAll<HTMLButtonElement>(".shots-toggle-button")) button.setAttribute("aria-pressed", String(button.dataset["project"] === this.#project));
|
|
328
426
|
track.setAttribute("aria-label", `${projectInfo(manifest, this.#project).label} screens`);
|
|
329
427
|
track.replaceChildren(...this.#screens.map((_, index) => element("div", { class: "shots-slide", "data-index": index, role: "group", "aria-roledescription": "slide" })));
|
|
428
|
+
// Every thumbnail of a project has the same width, from its captures' typical shape.
|
|
429
|
+
const ratio = typicalRatio(manifest.entries.filter(entry => entry.project === this.#project));
|
|
430
|
+
if (ratio) strip.style.setProperty("--_shots-thumb-ratio", String(Math.round(ratio * 10000) / 10000));
|
|
431
|
+
else strip.style.removeProperty("--_shots-thumb-ratio");
|
|
330
432
|
strip.replaceChildren(...this.#screens.map((name, index) => {
|
|
331
|
-
const
|
|
433
|
+
const label = screenInfo(manifest, name).label;
|
|
434
|
+
const button = element("button", { type: "button", class: "shots-thumb", "data-index": index, "aria-current": "false", title: label }, [element("span", { class: "shots-thumb-image" }), element("span", { class: "shots-thumb-label", text: label })]);
|
|
332
435
|
button.addEventListener("click", () => this.#select(index, scrollBehavior()), { signal: this.#abort!.signal });
|
|
333
436
|
return button;
|
|
334
437
|
}));
|
|
@@ -359,7 +462,7 @@ function defineGallery(): CustomElementConstructor {
|
|
|
359
462
|
button.setAttribute("aria-label", label);
|
|
360
463
|
button.title = label;
|
|
361
464
|
}
|
|
362
|
-
this.#root!.dataset["
|
|
465
|
+
this.#root!.dataset["shotsTheme"] = this.#theme;
|
|
363
466
|
this.#loaded.clear();
|
|
364
467
|
for (const slide of this.#track!.querySelectorAll<HTMLElement>(".shots-slide")) {
|
|
365
468
|
const index = Number(slide.dataset["index"]), name = this.#screens[index]!, entry = this.#entry(name);
|
|
@@ -379,7 +482,6 @@ function defineGallery(): CustomElementConstructor {
|
|
|
379
482
|
const holder = thumb.querySelector(".shots-thumb-image")!;
|
|
380
483
|
thumb.setAttribute("aria-label", screenInfo(manifest, name).label);
|
|
381
484
|
if (!entry) { holder.replaceChildren(element("span", { class: "shots-thumb-missing" })); continue; }
|
|
382
|
-
holder.setAttribute("style", `aspect-ratio: ${entry.width} / ${entry.height}`);
|
|
383
485
|
holder.replaceChildren(element("img", { src: entry.src, alt: "", loading: "lazy", decoding: "async", width: entry.width, height: entry.height }));
|
|
384
486
|
}
|
|
385
487
|
this.#fit();
|
|
@@ -550,7 +652,7 @@ function defineGallery(): CustomElementConstructor {
|
|
|
550
652
|
|
|
551
653
|
function defineCarousel(): CustomElementConstructor {
|
|
552
654
|
return class ScreenshotCarousel extends HTMLElement implements ScreenshotCarouselElement {
|
|
553
|
-
static observedAttributes = ["manifest", "project", "limit"];
|
|
655
|
+
static observedAttributes = ["manifest", "project", "limit", "view-all-href", "view-all-label"];
|
|
554
656
|
#data: ScreenshotManifest | null = null;
|
|
555
657
|
#abort: AbortController | null = null;
|
|
556
658
|
#track: HTMLDivElement | null = null;
|
|
@@ -559,6 +661,12 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
559
661
|
#slides: HTMLElement[] = [];
|
|
560
662
|
#manifest: ScreenshotManifest | null = null;
|
|
561
663
|
|
|
664
|
+
constructor() {
|
|
665
|
+
super();
|
|
666
|
+
const pending = takePendingData(this);
|
|
667
|
+
if (pending) this.manifestData = pending.value as ScreenshotManifest | null;
|
|
668
|
+
}
|
|
669
|
+
|
|
562
670
|
get manifestData(): ScreenshotManifest | null { return this.#data; }
|
|
563
671
|
set manifestData(value: ScreenshotManifest | null) { this.#data = value && parseScreenshotManifest(value); if (this.#connected) this.#load(); }
|
|
564
672
|
|
|
@@ -621,7 +729,9 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
621
729
|
const picture = element("picture", {}, slide.dark ? [element("source", { media: "(prefers-color-scheme: dark)", srcset: slide.dark.src, width: slide.dark.width, height: slide.dark.height })] : []);
|
|
622
730
|
picture.append(element("img", {
|
|
623
731
|
src: slide.light.src, alt: clone ? "" : screen.alt ?? `${screen.label}, ${projectInfo(manifest, project).label.toLowerCase()} screenshot`,
|
|
624
|
-
width: slide.light.width, height: slide.light.height,
|
|
732
|
+
width: slide.light.width, height: slide.light.height, decoding: "async",
|
|
733
|
+
// The first original rests at the start edge on load: fetch it first, and the rest as they near the viewport.
|
|
734
|
+
loading: !clone && index === 0 ? "eager" : "lazy", fetchpriority: !clone && index === 0 ? "high" : undefined,
|
|
625
735
|
}));
|
|
626
736
|
const device = linkBase
|
|
627
737
|
? element("a", { class: "shots-device", href: screenshotPath(linkBase, project, slide.name), tabindex: clone ? "-1" : undefined }, [picture])
|
|
@@ -637,12 +747,15 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
637
747
|
const track = element("div", { class: "shots-carousel-track", tabindex: "0" }, [list]);
|
|
638
748
|
this.#track = track;
|
|
639
749
|
const root = element("div", { class: "shots-carousel", role: "region", "aria-roledescription": "carousel", "aria-label": this.getAttribute("label") ?? "Screenshots", "data-frame": frame }, [track]);
|
|
750
|
+
const controls = element("div", { class: "shots-carousel-controls" });
|
|
751
|
+
const viewAll = this.getAttribute("view-all-href");
|
|
752
|
+
if (viewAll) controls.append(element("a", { class: "shots-carousel-view-all", href: viewAll, text: this.getAttribute("view-all-label") || "View all screenshots" }));
|
|
640
753
|
if (loop) {
|
|
641
754
|
const previous = element("button", { type: "button", class: "shots-carousel-step", "data-step": "-1", "aria-label": "Previous screens" }, [icon("prev")]);
|
|
642
755
|
const next = element("button", { type: "button", class: "shots-carousel-step", "data-step": "1", "aria-label": "Next screens" }, [icon("next")]);
|
|
643
756
|
previous.addEventListener("click", () => this.step(-1), { signal });
|
|
644
757
|
next.addEventListener("click", () => this.step(1), { signal });
|
|
645
|
-
|
|
758
|
+
controls.append(previous, next);
|
|
646
759
|
onScrollSettle(track, () => this.#normalize(), signal);
|
|
647
760
|
if (typeof ResizeObserver === "function") {
|
|
648
761
|
const observer = new ResizeObserver(() => this.#resize());
|
|
@@ -650,6 +763,7 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
650
763
|
signal.addEventListener("abort", () => observer.disconnect());
|
|
651
764
|
}
|
|
652
765
|
}
|
|
766
|
+
if (controls.childElementCount) root.append(controls);
|
|
653
767
|
this.replaceChildren(root);
|
|
654
768
|
if (loop) this.#jump(copies * slides.length + (keep ?? 0));
|
|
655
769
|
}
|