@michaelyagi/shoji 0.1.0-beta.16 → 0.1.0-beta.18

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.
@@ -519,7 +519,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
519
519
  const child = target.firstElementChild;
520
520
  const isPlaceholder = child instanceof HTMLElement && (child.classList.contains("shoji-slide-spinner") || child.classList.contains("shoji-slide-open-placeholder"));
521
521
  if (child instanceof HTMLElement && !isPlaceholder) {
522
- const rect = child.getBoundingClientRect();
522
+ const measureTarget = child.classList.contains("shoji-slide-provider-video") ? child.querySelector(".shoji-video-mount, iframe") ?? child : child;
523
+ const rect = measureTarget.getBoundingClientRect();
523
524
  if (rect.width > 0 && rect.height > 0) return rect;
524
525
  }
525
526
  const containerRect = target.getBoundingClientRect();
@@ -528,11 +529,23 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
528
529
  }
529
530
  return containerRect;
530
531
  }
531
- function transformBetween(from, to) {
532
+ function transformBetween(pivot, from, to) {
532
533
  const scale = Math.min(to.width / from.width, to.height / from.height);
533
- const translateX = to.left + to.width / 2 - (from.left + from.width / 2);
534
- const translateY = to.top + to.height / 2 - (from.top + from.height / 2);
535
- return `translate3d(${translateX}px, ${translateY}px, 0) scale3d(${scale}, ${scale}, 1)`;
534
+ return transformString(pivot, from, to, scale, scale);
535
+ }
536
+ function transformBetweenStretch(pivot, from, to) {
537
+ return transformString(pivot, from, to, to.width / from.width, to.height / from.height);
538
+ }
539
+ function transformString(pivot, from, to, scaleX, scaleY) {
540
+ const pivotX = pivot.left + pivot.width / 2;
541
+ const pivotY = pivot.top + pivot.height / 2;
542
+ const fromX = from.left + from.width / 2;
543
+ const fromY = from.top + from.height / 2;
544
+ const toX = to.left + to.width / 2;
545
+ const toY = to.top + to.height / 2;
546
+ const translateX = toX - pivotX - scaleX * (fromX - pivotX);
547
+ const translateY = toY - pivotY - scaleY * (fromY - pivotY);
548
+ return `translate3d(${translateX}px, ${translateY}px, 0) scale3d(${scaleX}, ${scaleY}, 1)`;
536
549
  }
537
550
  function effectiveOriginBox(origin) {
538
551
  const img = origin.querySelector("img");
@@ -550,26 +563,26 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
550
563
  const rect = child.getBoundingClientRect();
551
564
  return rect.width > 0 && rect.height > 0;
552
565
  }
553
- function computeTransformOutcome(origin, target, aspectRatio, naturalSize) {
566
+ function computeTransformOutcome(origin, target, direction, aspectRatio, naturalSize) {
554
567
  const originRect = effectiveOriginBox(origin);
555
568
  const targetRect = effectiveTargetBox(target, aspectRatio, naturalSize);
569
+ const pivotRect = target.getBoundingClientRect();
556
570
  if (targetRect.width === 0 || targetRect.height === 0 || originRect.width === 0 || originRect.height === 0) {
557
571
  return { kind: "none" };
558
572
  }
559
- const transform = transformBetween(targetRect, originRect);
560
573
  const unknownTargetSize = !naturalSize && !hasRealTargetContent(target);
561
574
  if (unknownTargetSize) {
562
- return { kind: "fade", transform };
575
+ return { kind: "fade", transform: transformBetween(pivotRect, targetRect, originRect) };
563
576
  }
564
577
  const originRatio = originRect.width / originRect.height;
565
578
  const targetRatio = targetRect.width / targetRect.height;
566
579
  const ratioOfRatios = Math.max(originRatio, targetRatio) / Math.min(originRatio, targetRatio);
567
- if (ratioOfRatios > ASPECT_MISMATCH_THRESHOLD) {
568
- return { kind: "fade", transform };
569
- }
570
- return { kind: "zoom", transform };
580
+ const isMismatch = direction === "out" && ratioOfRatios > ASPECT_MISMATCH_THRESHOLD;
581
+ const transform = direction === "out" ? transformBetweenStretch(pivotRect, targetRect, originRect) : transformBetween(pivotRect, targetRect, originRect);
582
+ return isMismatch ? { kind: "fade", transform } : { kind: "zoom", transform };
571
583
  }
572
- const FADE_TRANSITION = "transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-duration) var(--shoji-easing)";
584
+ const FADE_IN_TRANSITION = "transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-fade-duration, 120ms) var(--shoji-easing)";
585
+ const FADE_OUT_TRANSITION = "transform var(--shoji-duration) var(--shoji-easing), opacity var(--shoji-fade-duration, 120ms) var(--shoji-easing) calc(var(--shoji-duration) - var(--shoji-fade-duration, 120ms))";
573
586
  function waitForTransitionEnd(target, cb, property = "transform") {
574
587
  const durationMs = parseCssTime$1(getComputedStyle(target).transitionDuration);
575
588
  let done = false;
@@ -594,7 +607,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
594
607
  }
595
608
  function zoomIn({ origin, target, aspectRatio, naturalSize }) {
596
609
  if (prefersReducedMotion$1()) return;
597
- const outcome = computeTransformOutcome(origin, target, aspectRatio, naturalSize);
610
+ const outcome = computeTransformOutcome(origin, target, "in", aspectRatio, naturalSize);
598
611
  if (outcome.kind === "none") return;
599
612
  const { transform } = outcome;
600
613
  const isFade = outcome.kind === "fade";
@@ -604,7 +617,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
604
617
  target.style.transform = transform;
605
618
  if (isFade) target.style.opacity = "0";
606
619
  void target.offsetHeight;
607
- target.style.transition = isFade ? FADE_TRANSITION : "transform var(--shoji-duration) var(--shoji-easing)";
620
+ target.style.transition = isFade ? FADE_IN_TRANSITION : "transform var(--shoji-duration) var(--shoji-easing)";
608
621
  target.style.transform = "none";
609
622
  if (isFade) target.style.opacity = "1";
610
623
  waitForTransitionEnd(target, () => clearInlineTransform$1(target, "none"));
@@ -614,7 +627,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
614
627
  onComplete();
615
628
  return;
616
629
  }
617
- const outcome = computeTransformOutcome(origin, target, aspectRatio, naturalSize);
630
+ const outcome = computeTransformOutcome(origin, target, "out", aspectRatio, naturalSize);
618
631
  if (outcome.kind === "none") {
619
632
  onComplete();
620
633
  return;
@@ -632,13 +645,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
632
645
  const targetRect = effectiveTargetBox(target, aspectRatio, naturalSize);
633
646
  if (targetRect.width > 0 && targetRect.height > 0) {
634
647
  const existing = target.style.transform;
635
- const jump = transformBetween(targetRect, zoomStart);
648
+ const jump = transformBetween(target.getBoundingClientRect(), targetRect, zoomStart);
636
649
  target.style.transition = "none";
637
650
  target.style.transform = existing && existing !== "none" ? `${existing} ${jump}` : jump;
638
651
  void target.offsetHeight;
639
652
  }
640
653
  }
641
- target.style.transition = isFade ? FADE_TRANSITION : "transform var(--shoji-duration) var(--shoji-easing)";
654
+ target.style.transition = isFade ? FADE_OUT_TRANSITION : "transform var(--shoji-duration) var(--shoji-easing)";
642
655
  void target.offsetHeight;
643
656
  target.style.transform = transform;
644
657
  if (isFade) target.style.opacity = "0";
@@ -1149,8 +1162,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1149
1162
  slot.root.style.transform = `translateX(calc(${slot.offset * 100}% + ${this.dragOffsetPx}px))`;
1150
1163
  }
1151
1164
  }
1152
- /** Re-renders whichever slots need a different item; `onLoad` fires per index once its media settles. `openPlaceholderSrc` (only from `Gallery.open()`) swaps the centerIndex slot's spinner for a low-res placeholder once it decodes, if not already ready. `loop` matches `Gallery.ts`'s own `nextIndex()`/`prevIndex()` wrap-around — without it, the pool slot just past the last (or before the first) item has no item to preload at all, so a boundary drag reveals nothing even though the completed navigation itself already wraps correctly. */
1153
- render(items, centerIndex, onLoad, openPlaceholderSrc, loop = false) {
1165
+ /** Re-renders whichever slots need a different item; `onLoad` fires per index once its media settles. `openPlaceholderSrc` (only from `Gallery.open()`) swaps the centerIndex slot's spinner for a low-res placeholder once it decodes, if not already ready; `onOpenPlaceholderReady` fires once that swap happens (`Gallery.ts`'s `beginZoom` doc comment explains why). `openPlaceholderAlreadyVisible` (`Gallery.ts`'s own doc comment on it) skips that decode wait entirely when it's not actually needed. `loop` matches `Gallery.ts`'s own `nextIndex()`/`prevIndex()` wrap-around — without it, the pool slot just past the last (or before the first) item has no item to preload at all, so a boundary drag reveals nothing even though the completed navigation itself already wraps correctly. */
1166
+ render(items, centerIndex, onLoad, openPlaceholderSrc, loop = false, onOpenPlaceholderReady, openPlaceholderAlreadyVisible = false) {
1154
1167
  const canWrap = loop && items.length > this.slots.length;
1155
1168
  const targets = this.slots.map((_, i) => {
1156
1169
  const offset = i - this.preload;
@@ -1200,7 +1213,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1200
1213
  this.applyAspect(slot, item);
1201
1214
  if (index === centerIndex && openPlaceholderSrc) {
1202
1215
  const naturalSize = item.width && item.height ? { width: item.width, height: item.height } : void 0;
1203
- this.revealOpenPlaceholder(openPlaceholderSrc, slot, index, naturalSize);
1216
+ const aspectRatio = item.thumbnailWidth && item.thumbnailHeight ? item.thumbnailWidth / item.thumbnailHeight : naturalSize && naturalSize.width / naturalSize.height;
1217
+ this.revealOpenPlaceholder(
1218
+ openPlaceholderSrc,
1219
+ slot,
1220
+ index,
1221
+ naturalSize,
1222
+ aspectRatio,
1223
+ onOpenPlaceholderReady,
1224
+ openPlaceholderAlreadyVisible
1225
+ );
1204
1226
  }
1205
1227
  if (((_a = item.video) == null ? void 0 : _a.provider) === "html5") {
1206
1228
  this.renderVideo(item, slot, index, onLoad);
@@ -1301,25 +1323,41 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
1301
1323
  * (`.shoji-slide`) fixes it: same box, only ever `translateX`'d for
1302
1324
  * pool-offset positioning, never scaled — a stable read regardless of
1303
1325
  * what its child is mid-animation through.
1326
+ *
1327
+ * A third real bug: `aspectRatio` (the box's *shape*) must come from the
1328
+ * thumbnail's own aspect (`item.thumbnailWidth`/`thumbnailHeight` when
1329
+ * declared, the caller's own doc comment on this call explains the
1330
+ * priority) — this image's actual pixels are the thumbnail's, and sizing
1331
+ * the box to `naturalSize`'s aspect (the real photo's) instead left most
1332
+ * of it transparent whenever the two differ, `object-fit: contain`
1333
+ * letterboxing the real thumbnail content down to a thin strip inside a
1334
+ * wrongly-shaped box (DESIGN.md §2.3b).
1304
1335
  */
1305
- revealOpenPlaceholder(src, slot, index, naturalSize) {
1336
+ revealOpenPlaceholder(src, slot, index, naturalSize, aspectRatio, onReady, alreadyVisible = false) {
1306
1337
  const img = createOpenPlaceholder(src);
1307
1338
  const reveal = () => {
1308
- if (slot.assignedIndex !== index || slot.ready) return;
1339
+ if (slot.assignedIndex !== index) return;
1340
+ if (slot.ready) {
1341
+ onReady == null ? void 0 : onReady();
1342
+ return;
1343
+ }
1309
1344
  slot.media.querySelectorAll(".shoji-slide-spinner, .shoji-slide-provider-poster").forEach((el) => el.remove());
1310
1345
  slot.media.appendChild(img);
1311
- if (naturalSize) {
1346
+ if (aspectRatio) {
1312
1347
  const containerRect = slot.root.getBoundingClientRect();
1313
1348
  const box = containedBox(
1314
1349
  { left: 0, top: 0, width: containerRect.width, height: containerRect.height },
1315
- naturalSize.width / naturalSize.height,
1350
+ aspectRatio,
1316
1351
  naturalSize
1317
1352
  );
1318
1353
  img.style.width = `${box.width}px`;
1319
1354
  img.style.height = `${box.height}px`;
1320
1355
  }
1356
+ onReady == null ? void 0 : onReady();
1321
1357
  };
1322
- if (typeof img.decode === "function") {
1358
+ if (alreadyVisible) {
1359
+ queueMicrotask(reveal);
1360
+ } else if (typeof img.decode === "function") {
1323
1361
  img.decode().then(reveal, reveal);
1324
1362
  } else {
1325
1363
  img.addEventListener("load", reveal, { once: true });
@@ -2927,7 +2965,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
2927
2965
  this.onActivity();
2928
2966
  this.bus.emit("captionModalChange", { open: false });
2929
2967
  }
2930
- renderCurrentSlide(openPlaceholderSrc) {
2968
+ /** `onCenterContentVisible` — only `open()` passes this, to start the zoom-in once something real is actually visible (its own `beginZoom` doc comment). */
2969
+ renderCurrentSlide(openPlaceholderSrc, onCenterContentVisible, openPlaceholderAlreadyVisible = false) {
2931
2970
  if (!this.slides || !this.dom) return;
2932
2971
  const dom = this.dom;
2933
2972
  this.slides.render(
@@ -2938,10 +2977,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
2938
2977
  this.bus.emit("slideItemLoad", { index: loadedIndex });
2939
2978
  this.setSlideLoading(false);
2940
2979
  if (!this.captionFadePending) this.updateCaptionVisibility();
2980
+ onCenterContentVisible == null ? void 0 : onCenterContentVisible();
2941
2981
  }
2942
2982
  },
2943
2983
  openPlaceholderSrc,
2944
- this.loop
2984
+ this.loop,
2985
+ onCenterContentVisible,
2986
+ openPlaceholderAlreadyVisible
2945
2987
  );
2946
2988
  this.setSlideLoading(!this.slides.isActiveReady());
2947
2989
  const item = this.itemList[this.activeIndex];
@@ -2972,27 +3014,39 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
2972
3014
  this.captionFadePending = true;
2973
3015
  this.dom.caption.style.opacity = "0";
2974
3016
  }
3017
+ const openPlaceholderSrc = naturalSize && !((_a = this.itemList[index]) == null ? void 0 : _a.video) ? this.resolveOpenPlaceholderSrc(this.itemList[index], origin) : void 0;
3018
+ const originImg = origin == null ? void 0 : origin.querySelector("img");
3019
+ const openPlaceholderAlreadyVisible = !!openPlaceholderSrc && !!originImg && (originImg.currentSrc || originImg.src) === openPlaceholderSrc;
3020
+ let zoomStarted = false;
3021
+ const beginZoom = () => {
3022
+ var _a2;
3023
+ if (zoomStarted) return;
3024
+ zoomStarted = true;
3025
+ const media = (_a2 = this.slides) == null ? void 0 : _a2.getActiveMedia();
3026
+ if (media && origin) {
3027
+ zoomIn({
3028
+ origin,
3029
+ target: media,
3030
+ aspectRatio: this.resolveAspectRatio(index, origin),
3031
+ naturalSize
3032
+ });
3033
+ }
3034
+ if (fadeInOnOpen && this.dom) {
3035
+ this.captionFadePending = false;
3036
+ this.updateCaptionVisibility();
3037
+ this.dom.caption.style.opacity = "";
3038
+ }
3039
+ };
2975
3040
  this.renderCurrentSlide(
2976
- naturalSize && !((_a = this.itemList[index]) == null ? void 0 : _a.video) ? this.resolveOpenPlaceholderSrc(this.itemList[index], origin) : void 0
3041
+ openPlaceholderSrc,
3042
+ openPlaceholderSrc ? beginZoom : void 0,
3043
+ openPlaceholderAlreadyVisible
2977
3044
  );
2978
3045
  this.dom.outer.classList.add("shoji-open");
2979
3046
  document.addEventListener("keydown", this.onKeydown);
2980
3047
  this.focusTrap.activate(this.dom.dialog);
2981
3048
  this.scheduleAutoHide();
2982
- const media = (_b = this.slides) == null ? void 0 : _b.getActiveMedia();
2983
- if (media && origin) {
2984
- zoomIn({
2985
- origin,
2986
- target: media,
2987
- aspectRatio: this.resolveAspectRatio(index, origin),
2988
- naturalSize
2989
- });
2990
- }
2991
- if (fadeInOnOpen && this.dom) {
2992
- this.captionFadePending = false;
2993
- this.updateCaptionVisibility();
2994
- this.dom.caption.style.opacity = "";
2995
- }
3049
+ if (!openPlaceholderSrc || ((_b = this.slides) == null ? void 0 : _b.isActiveReady())) beginZoom();
2996
3050
  this.bus.emit("open", { index });
2997
3051
  this.bus.emit("afterOpen", { index });
2998
3052
  }