PageSourceSearch

https://mauriciowolff.com/lightbox.js?v=2i21xpowu7u6b

js mauriciowolff.com collected 2026-10-09 23:41:32 UTC 6,065 bytes, 182 lines download raw bytes

1// Lightbox: images in article content, and links to video files (played in a
2// centered <dialog> with the Bunny Stream embed player).
3// CSP: requires frame-src https://iframe.mediadelivery.net (set in app.ts).
4
5const BUNNY_LIBRARY = "733733";
6const embed = (id) => `https://iframe.mediadelivery.net/embed/${BUNNY_LIBRARY}/${id}`;
7
8const VIDEOS = {
9  "/flexq.mov": { src: embed("d970269b-ade1-438a-91aa-88bf285bd1a6"), w: 1080, h: 720, inset: 12 },
10  "/syom.mp4": { src: embed("019ee1db-9207-48a2-b252-c4365fd99e2d"), w: 1716, h: 1080 },
11  "/odara.com.mp4": { src: embed("fc708a86-0d03-47bc-b174-63637e8cb250"), w: 1022, h: 720 },
12  "/odara.com.webm": { src: embed("fc708a86-0d03-47bc-b174-63637e8cb250"), w: 1022, h: 720 },
13  "/psi-vision.mp4": { src: embed("fbe4338f-f4d2-4cd9-9730-1fa965b28559"), w: 1048, h: 720 },
14  "/vibelab-welcome.mov": { src: embed("8b76791e-84c3-45b8-a8ab-4362b83f3629"), w: 640, h: 403 },
15  "/vibelab-aiproxy.mov": { src: embed("87f3ba06-8217-4cb5-a59d-00a7d0c70245"), w: 640, h: 363 },
16  "/vibelab-mds.mov": { src: embed("4c0cdfaa-290f-47ad-a19e-fa406b9e7db6"), w: 640, h: 413 },
17  "/vibelab-postlaunch.mov": { src: embed("56a3773c-24d3-44b4-8c5f-fa1b6d034c48"), w: 640, h: 404 },
18  "/vibelab-multi.mov": { src: embed("3a6c3fb5-36a5-4716-98f3-fa9ba6cdbaf0"), w: 640, h: 405 },
19  "/ade.mov": { src: embed("972e8ecb-a3aa-4a75-849e-5e43756093c3"), w: 640, h: 403 },
20};
21
22const IMAGE_LIGHTBOX = `
23dialog.image-lightbox {
24  border: none;
25  padding: 0;
26  background: transparent;
27  max-width: 92vw;
28  max-height: 86vh;
29  inset: 0;
30  margin: auto;
31  overflow: visible;
32  cursor: zoom-out;
33}
34dialog.image-lightbox img {
35  display: block;
36  max-width: 92vw;
37  max-height: 86vh;
38  width: auto;
39  height: auto;
40  border-radius: 1rem;
41  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
42}
43dialog.image-lightbox[open],
44dialog.video-lightbox[open] {
45  animation: lightbox-in 200ms cubic-bezier(0.2, 0.8, 0.2, 1);
46}
47dialog.image-lightbox[open]::backdrop,
48dialog.video-lightbox[open]::backdrop {
49  animation: lightbox-fade 200ms ease;
50}
51dialog.image-lightbox.closing,
52dialog.video-lightbox.closing {
53  animation: lightbox-in 160ms ease reverse;
54}
55dialog.image-lightbox.closing::backdrop,
56dialog.video-lightbox.closing::backdrop {
57  animation: lightbox-fade 160ms ease reverse;
58}
59@keyframes lightbox-in {
60  from { opacity: 0; transform: scale(0.92); }
61}
62@keyframes lightbox-fade {
63  from { opacity: 0; }
64}
65@media (prefers-reduced-motion: reduce) {
66  dialog.image-lightbox[open], dialog.video-lightbox[open],
67  dialog.image-lightbox[open]::backdrop, dialog.video-lightbox[open]::backdrop {
68    animation: none;
69  }
70}
71`;
72
73const VIDEO_LIGHTBOX = `
74dialog.video-lightbox {
75  border: none;
76  padding: 0;
77  background: transparent;
78  max-width: 92vw;
79  max-height: 86vh;
80  inset: 0;
81  margin: auto;
82  position: relative;
83  overflow: hidden;
84  border-radius: 0.75rem;
85  background: #000;
86}
87dialog.video-lightbox iframe {
88  position: absolute;
89  border: none;
90  background: #000;
91  display: block;
92}
93dialog.video-lightbox .close {
94  position: absolute;
95  top: 0.5rem;
96  right: 0.75rem;
97  z-index: 1;
98  border: none;
99  background: none;
100  color: #fff;
101  font-size: 2rem;
102  line-height: 1;
103  cursor: pointer;
104  opacity: 0.8;
105  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.6);
106}
107dialog.video-lightbox .close:hover { opacity: 1; }
108dialog.image-lightbox::backdrop,
109dialog.video-lightbox::backdrop {
110  background: transparent;
111  backdrop-filter: blur(9px);
112  -webkit-backdrop-filter: blur(9px);
113}
114article .content img { cursor: zoom-in; }
115`;
116
117document.head.appendChild(Object.assign(document.createElement("style"), { textContent: IMAGE_LIGHTBOX + VIDEO_LIGHTBOX }));
118
119const autoplay = document.body.dataset.play;
120if (autoplay && VIDEOS[autoplay]) openVideoDialog(VIDEOS[autoplay]);
121
122// Animated close: play the reverse animation before removing the dialog.
123function closeAnimated(dialog) {
124  if (dialog.classList.contains("closing")) return;
125  dialog.classList.add("closing");
126  dialog.addEventListener("animationend", () => dialog.close(), { once: true });
127  // Reduced motion: no animation fires, so close straight away.
128  setTimeout(() => dialog.open && dialog.close(), 400);
129}
130
131// Click anywhere closes: the image itself, the letterbox area, the backdrop.
132function dismissOnClick(dialog) {
133  dialog.addEventListener("click", () => closeAnimated(dialog));
134  dialog.addEventListener("close", () => dialog.remove());
135}
136
137function openImageDialog(source) {
138  const dialog = document.createElement("dialog");
139  dialog.className = "image-lightbox";
140  const image = document.createElement("img");
141  image.src = source.currentSrc || source.src;
142  image.alt = source.alt;
143  image.width = source.naturalWidth;
144  image.height = source.naturalHeight;
145  dialog.appendChild(image);
146  dismissOnClick(dialog);
147  document.body.appendChild(dialog);
148  dialog.showModal();
149}
150
151function openVideoDialog(video) {
152  const i = video.inset ?? 0;
153  const w = video.w - 2 * i;
154  const h = video.h - 2 * i;
155  const dialog = document.createElement("dialog");
156  dialog.className = "video-lightbox";
157  dialog.style.aspectRatio = `${w} / ${h}`;
158  dialog.style.width = `min(92vw, ${(86 * w) / h}vh)`;
159  dialog.innerHTML = `
160    <iframe src="${video.src}?autoplay=true" style="width: ${(100 * video.w) / w}%; height: ${(100 * video.h) / h}%; left: ${(-100 * i) / w}%; top: ${(-100 * i) / h}%;" allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture" allowfullscreen></iframe>
161    <button class="close" type="button" aria-label="Close video">&times;</button>
162  `;
163  dismissOnClick(dialog);
164  document.body.appendChild(dialog);
165  dialog.showModal();
166}
167
168document.addEventListener("click", (event) => {
169  const image = event.target.closest("article .content img");
170  if (image) {
171    event.preventDefault();
172    openImageDialog(image);
173    return;
174  }
175  const link = event.target.closest("a[href]");
176  if (!link) return;
177  const url = new URL(link.href, location.origin);
178  const video = VIDEOS[url.pathname];
179  if (!video) return;
180  event.preventDefault();
181  openVideoDialog(video);
182});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.