@a2f0/agent-tool 0.1.33 → 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
CHANGED
|
@@ -281,7 +281,9 @@
|
|
|
281
281
|
text-align: center;
|
|
282
282
|
}
|
|
283
283
|
|
|
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. */
|
|
284
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));
|
|
285
287
|
position: relative;
|
|
286
288
|
display: flex;
|
|
287
289
|
gap: 0.5rem;
|
|
@@ -296,7 +298,8 @@
|
|
|
296
298
|
flex: 0 0 auto;
|
|
297
299
|
flex-direction: column;
|
|
298
300
|
gap: 0.25rem;
|
|
299
|
-
|
|
301
|
+
box-sizing: content-box;
|
|
302
|
+
width: var(--_shots-thumb-width);
|
|
300
303
|
padding: 0.25rem;
|
|
301
304
|
border: 1px solid transparent;
|
|
302
305
|
border-radius: calc(var(--_shots-radius) / 2);
|
|
@@ -309,13 +312,11 @@
|
|
|
309
312
|
border-color: var(--_shots-accent);
|
|
310
313
|
}
|
|
311
314
|
|
|
312
|
-
/*
|
|
315
|
+
/* A capture of another shape is cropped to the thumbnail, keeping its top. */
|
|
313
316
|
.shots-thumb-image {
|
|
314
317
|
display: block;
|
|
318
|
+
width: 100%;
|
|
315
319
|
height: var(--_shots-thumb-height);
|
|
316
|
-
min-width: 4.5rem;
|
|
317
|
-
max-width: calc(var(--_shots-thumb-max-width) - 0.5rem);
|
|
318
|
-
aspect-ratio: 3 / 4;
|
|
319
320
|
border-radius: 4px;
|
|
320
321
|
background: var(--_shots-surface-raised);
|
|
321
322
|
overflow: hidden;
|
|
@@ -329,8 +330,11 @@
|
|
|
329
330
|
object-position: top;
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
/* Long labels never widen a thumbnail; they end in an ellipsis, with the full label in the button's title. */
|
|
332
334
|
.shots-thumb-label {
|
|
333
|
-
|
|
335
|
+
display: block;
|
|
336
|
+
min-width: 0;
|
|
337
|
+
max-width: 100%;
|
|
334
338
|
overflow: hidden;
|
|
335
339
|
color: var(--_shots-ink-muted);
|
|
336
340
|
font-size: 0.75rem;
|
|
@@ -501,11 +505,22 @@
|
|
|
501
505
|
|
|
502
506
|
.shots-carousel-controls {
|
|
503
507
|
display: flex;
|
|
508
|
+
flex-wrap: wrap;
|
|
509
|
+
align-items: center;
|
|
504
510
|
justify-content: flex-end;
|
|
505
511
|
gap: 0.5rem;
|
|
506
512
|
padding: 0 var(--_shots-carousel-padding);
|
|
507
513
|
}
|
|
508
514
|
|
|
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
|
+
|
|
509
524
|
.shots-carousel-step {
|
|
510
525
|
display: inline-grid;
|
|
511
526
|
place-items: center;
|
|
@@ -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 {
|
|
@@ -191,6 +243,7 @@ function defineGallery(): CustomElementConstructor {
|
|
|
191
243
|
#overflow = "";
|
|
192
244
|
#rendered = false;
|
|
193
245
|
#announced = "";
|
|
246
|
+
#wheel = createWheelStepper();
|
|
194
247
|
|
|
195
248
|
constructor() {
|
|
196
249
|
super();
|
|
@@ -280,6 +333,7 @@ function defineGallery(): CustomElementConstructor {
|
|
|
280
333
|
|
|
281
334
|
const track = element("div", { class: "shots-track", tabindex: "0", role: "group", "aria-roledescription": "carousel" });
|
|
282
335
|
onScrollSettle(track, () => this.#settle(), signal);
|
|
336
|
+
track.addEventListener("wheel", (event: WheelEvent) => this.#onWheel(event), { passive: false, signal });
|
|
283
337
|
const previous = element("button", { type: "button", class: "shots-icon-button", "data-action": "previous", "aria-label": "Previous screen" }, [icon("prev")]);
|
|
284
338
|
const next = element("button", { type: "button", class: "shots-icon-button", "data-action": "next", "aria-label": "Next screen" }, [icon("next")]);
|
|
285
339
|
previous.addEventListener("click", () => this.step(-1), { signal });
|
|
@@ -316,6 +370,30 @@ function defineGallery(): CustomElementConstructor {
|
|
|
316
370
|
else if (event.key === "ArrowLeft" || (inside && event.key === "ArrowUp")) { event.preventDefault(); this.step(-1); }
|
|
317
371
|
}
|
|
318
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
|
+
|
|
319
397
|
#setExpanded(expanded: boolean): void {
|
|
320
398
|
if (expanded === this.#expanded || !this.#root) return;
|
|
321
399
|
this.#expanded = expanded;
|
|
@@ -340,14 +418,20 @@ function defineGallery(): CustomElementConstructor {
|
|
|
340
418
|
this.#screens = projectScreens(manifest, this.#project);
|
|
341
419
|
this.#index = Math.max(0, screen === undefined ? 0 : this.#screens.indexOf(screen));
|
|
342
420
|
this.#loaded.clear();
|
|
421
|
+
this.#wheel.reset();
|
|
343
422
|
const frame = projectFrame(manifest, this.#project);
|
|
344
423
|
this.#root!.dataset["frame"] = frame;
|
|
345
424
|
this.#root!.dataset["project"] = this.#project;
|
|
346
425
|
for (const button of this.#root!.querySelectorAll<HTMLButtonElement>(".shots-toggle-button")) button.setAttribute("aria-pressed", String(button.dataset["project"] === this.#project));
|
|
347
426
|
track.setAttribute("aria-label", `${projectInfo(manifest, this.#project).label} screens`);
|
|
348
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");
|
|
349
432
|
strip.replaceChildren(...this.#screens.map((name, index) => {
|
|
350
|
-
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 })]);
|
|
351
435
|
button.addEventListener("click", () => this.#select(index, scrollBehavior()), { signal: this.#abort!.signal });
|
|
352
436
|
return button;
|
|
353
437
|
}));
|
|
@@ -398,7 +482,6 @@ function defineGallery(): CustomElementConstructor {
|
|
|
398
482
|
const holder = thumb.querySelector(".shots-thumb-image")!;
|
|
399
483
|
thumb.setAttribute("aria-label", screenInfo(manifest, name).label);
|
|
400
484
|
if (!entry) { holder.replaceChildren(element("span", { class: "shots-thumb-missing" })); continue; }
|
|
401
|
-
holder.setAttribute("style", `aspect-ratio: ${entry.width} / ${entry.height}`);
|
|
402
485
|
holder.replaceChildren(element("img", { src: entry.src, alt: "", loading: "lazy", decoding: "async", width: entry.width, height: entry.height }));
|
|
403
486
|
}
|
|
404
487
|
this.#fit();
|
|
@@ -569,7 +652,7 @@ function defineGallery(): CustomElementConstructor {
|
|
|
569
652
|
|
|
570
653
|
function defineCarousel(): CustomElementConstructor {
|
|
571
654
|
return class ScreenshotCarousel extends HTMLElement implements ScreenshotCarouselElement {
|
|
572
|
-
static observedAttributes = ["manifest", "project", "limit"];
|
|
655
|
+
static observedAttributes = ["manifest", "project", "limit", "view-all-href", "view-all-label"];
|
|
573
656
|
#data: ScreenshotManifest | null = null;
|
|
574
657
|
#abort: AbortController | null = null;
|
|
575
658
|
#track: HTMLDivElement | null = null;
|
|
@@ -646,7 +729,9 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
646
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 })] : []);
|
|
647
730
|
picture.append(element("img", {
|
|
648
731
|
src: slide.light.src, alt: clone ? "" : screen.alt ?? `${screen.label}, ${projectInfo(manifest, project).label.toLowerCase()} screenshot`,
|
|
649
|
-
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,
|
|
650
735
|
}));
|
|
651
736
|
const device = linkBase
|
|
652
737
|
? element("a", { class: "shots-device", href: screenshotPath(linkBase, project, slide.name), tabindex: clone ? "-1" : undefined }, [picture])
|
|
@@ -662,12 +747,15 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
662
747
|
const track = element("div", { class: "shots-carousel-track", tabindex: "0" }, [list]);
|
|
663
748
|
this.#track = track;
|
|
664
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" }));
|
|
665
753
|
if (loop) {
|
|
666
754
|
const previous = element("button", { type: "button", class: "shots-carousel-step", "data-step": "-1", "aria-label": "Previous screens" }, [icon("prev")]);
|
|
667
755
|
const next = element("button", { type: "button", class: "shots-carousel-step", "data-step": "1", "aria-label": "Next screens" }, [icon("next")]);
|
|
668
756
|
previous.addEventListener("click", () => this.step(-1), { signal });
|
|
669
757
|
next.addEventListener("click", () => this.step(1), { signal });
|
|
670
|
-
|
|
758
|
+
controls.append(previous, next);
|
|
671
759
|
onScrollSettle(track, () => this.#normalize(), signal);
|
|
672
760
|
if (typeof ResizeObserver === "function") {
|
|
673
761
|
const observer = new ResizeObserver(() => this.#resize());
|
|
@@ -675,6 +763,7 @@ function defineCarousel(): CustomElementConstructor {
|
|
|
675
763
|
signal.addEventListener("abort", () => observer.disconnect());
|
|
676
764
|
}
|
|
677
765
|
}
|
|
766
|
+
if (controls.childElementCount) root.append(controls);
|
|
678
767
|
this.replaceChildren(root);
|
|
679
768
|
if (loop) this.#jump(copies * slides.length + (keep ?? 0));
|
|
680
769
|
}
|