@michaelyagi/shoji 0.1.0-alpha.10 → 0.1.0-alpha.12

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.
@@ -30,6 +30,8 @@ export declare class Gallery {
30
30
  private autoHidden;
31
31
  private hoveredControlCount;
32
32
  private isClosing;
33
+ /** True while a vertical drag has hidden controls past its own distance threshold (`setControlsHiddenForDrag`) — `onActivity()` defers to it, since the drag's own continuous pointermove stream would otherwise immediately re-reveal what it just hid on every single move. */
34
+ private controlsHiddenByDrag;
33
35
  private itemList;
34
36
  private scannedElements;
35
37
  private activeIndex;
@@ -48,8 +50,22 @@ export declare class Gallery {
48
50
  private pluginCleanups;
49
51
  private readonly onContainerClick;
50
52
  private readonly onOuterClick;
51
- /** DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle clock. `autoHideDelay: 0` = "never show controls" — a no-op here. */
53
+ /**
54
+ * DESIGN.md §2.8 — any interaction re-shows controls, restarts the idle
55
+ * clock. `autoHideDelay: 0` = "never show controls" — a no-op here.
56
+ * `autoHideDelay: false` = "always visible" — also a no-op: nothing to
57
+ * reveal (already shown) or reschedule (no timer ever runs). Also a no-op
58
+ * once `isClosing` — a real bug, reported from real usage: moving the
59
+ * mouse during close()'s own controls-fade-then-zoom-out sequence (§2.6a)
60
+ * re-showed the just-hidden controls mid-animation, since these listeners
61
+ * stay wired for the whole close sequence. Same reasoning for
62
+ * `controlsHiddenByDrag` (§2.4/§2.8): a vertical drag's own `pointermove`
63
+ * stream would otherwise re-reveal what it just hid, every single frame.
64
+ */
52
65
  private readonly onActivity;
66
+ /** x */
67
+ private controlsHiddenAtGestureStart;
68
+ private readonly captureGestureStartState;
53
69
  private readonly onKeydown;
54
70
  constructor(target: HTMLElement | string, options?: GalleryOptions);
55
71
  /** Everything the constructor does after `this.element` is resolved — shared with `reinit()` (§2.7). */
@@ -102,7 +118,19 @@ export declare class Gallery {
102
118
  */
103
119
  getOriginElement(index: number): HTMLElement | null;
104
120
  private scheduleAutoHide;
105
- private hideControls;
121
+ /**
122
+ * Forces the same fade §2.8's idle timer would eventually trigger —
123
+ * public so a plugin can hide controls on its own trigger. Same
124
+ * `isControlActive()` guard as the timer. Also a no-op under
125
+ * `autoHideDelay: false` — a real bug, reported from real usage: Autoplay's
126
+ * tap-to-toggle-chrome behavior called this directly and ignored `false`
127
+ * entirely, since only the idle timer checked it. `false` has to hold for
128
+ * every caller, not just the timer — `mobileSettings.controls: false` is
129
+ * affected the same way, by design. `forceHideControls()`/
130
+ * `setControlsHiddenForDrag()` (close, drag-to-close) deliberately don't
131
+ * check this — direct user actions with their own feedback, not auto-hide.
132
+ */
133
+ hideControls(): void;
106
134
  private showControls;
107
135
  /**
108
136
  * DESIGN.md §2.1 — `caption` is `string | HTMLElement | DangerousHtmlCaption`:
@@ -173,8 +201,52 @@ export declare class Gallery {
173
201
  */
174
202
  private applyMobileControlsSetting;
175
203
  close(): void;
204
+ /**
205
+ * DESIGN.md §2.4/§2.6a — same effect as `close()`, from a completed
206
+ * vertical swipe. `frozenDrag` is threaded through to `zoomOut()`'s own
207
+ * `dragStart`, so the whole close continues as one motion from exactly
208
+ * where the drag left off (see `GestureController.
209
+ * takeFrozenDragTransform`).
210
+ */
211
+ private closeFromSwipe;
212
+ /**
213
+ * `frozenDrag` — see `closeFromSwipe()`'s doc comment; absent for a
214
+ * button-close, which has no drag to continue from. Controls fade and the
215
+ * zoom-out run concurrently, both starting the instant `forceHideControls()`
216
+ * runs — not sequenced one after the other. (A previous version waited for
217
+ * the controls' own fade to fully finish before starting the zoom-out, for
218
+ * a button-close specifically — requested directly, to avoid stationary
219
+ * chrome hovering over an already-shrinking photo. Reversed on later,
220
+ * explicit feedback: waiting read as two distinct steps rather than one
221
+ * motion; starting both together still avoids stationary chrome, since
222
+ * the chrome is disappearing too, just without the pause.)
223
+ */
224
+ private beginClose;
225
+ /** Forces the same fade §2.8's idle timer would eventually trigger, bypassing `hideControls()`'s own `isControlActive()` hover guard — the most common close path (clicking close) is hovering a control at this exact instant, and a deliberate close should hide regardless. No-op if already hidden. */
226
+ private forceHideControls;
227
+ /**
228
+ * DESIGN.md §2.4/§2.8 — `GestureController`'s live vertical-drag cue: hide
229
+ * past the same distance a release would close, reveal again on retreat.
230
+ * `hidden: false` only reveals if visible when *this* gesture started
231
+ * (`controlsHiddenAtGestureStart`) — shouldn't resurrect controls already
232
+ * separately hidden (idle, or `autoHideDelay: 0`) before the drag began.
233
+ * `dragCloseThreshold` (bus event) fires on every crossing regardless of
234
+ * that guard — Autoplay (§4-autoplay) needs to know about the crossing
235
+ * itself, not just whether controls visibly moved.
236
+ */
237
+ private setControlsHiddenForDrag;
176
238
  /** `item.width`/`height`, else origin's `naturalWidth`/`naturalHeight` (accurate when `item.thumb` is unset). Feeds `computeTransform`'s letterbox-aware sizing. */
177
239
  private resolveAspectRatio;
240
+ /**
241
+ * `item.width`/`height` only — deliberately never a thumbnail's own
242
+ * `naturalWidth`/`naturalHeight` the way `resolveAspectRatio` above will:
243
+ * that's a fine stand-in for *shape*, but using it as the real photo's
244
+ * true pixel size would under-cap a genuinely large photo down to
245
+ * thumbnail resolution. `undefined` here just means "genuinely unknown,"
246
+ * not "assume small" — `zoomTransition.ts`'s `containedBox` already
247
+ * treats it that way (no cap applied at all).
248
+ */
249
+ private resolveNaturalSize;
178
250
  /**
179
251
  * Idempotent on purpose: a pending zoom-out's `transitionend`/fallback
180
252
  * timeout can still fire after `destroy()` has already force-finished the
@@ -1,12 +1,15 @@
1
1
  import { GestureEngineOptions } from '../gestures/GestureEngine';
2
2
  import { SlideManager } from './SlideManager';
3
+ import { FrozenDragTransform } from './zoomTransition';
3
4
  /**
4
- * A real interactive control. Also used by `Gallery.ts`'s `isBackdropClick`,
5
- * which used to have its own narrower list (missing select/input/textarea/
6
- * a[href]) — a plugin-mounted non-button control got misread as a backdrop
7
- * click and closed the gallery. One shared selector so that can't recur.
5
+ * A real interactive control, plus `.shoji-caption` — a click/drag starting
6
+ * there should select/scroll its text natively, not get captured as a
7
+ * gesture. Also used by `Gallery.ts`'s `isBackdropClick`, which used to have
8
+ * its own narrower list (missing select/input/textarea/a[href]) — a
9
+ * plugin-mounted non-button control got misread as a backdrop click and
10
+ * closed the gallery. One shared selector so that can't recur.
8
11
  */
9
- export declare const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag]";
12
+ export declare const INTERACTIVE_CONTROL_SELECTOR = "button, video, input, select, textarea, a[href], [data-shoji-no-drag], .shoji-caption";
10
13
  /** What `GestureController` needs from `Gallery` — narrow on purpose, so this module never reaches into Gallery internals beyond this contract. */
11
14
  export interface GestureControllerHost {
12
15
  dialog: HTMLElement;
@@ -16,6 +19,10 @@ export interface GestureControllerHost {
16
19
  next(): void;
17
20
  prev(): void;
18
21
  close(): void;
22
+ /** Same effect as `close()`, but for a completed vertical swipe — skips the separate controls-fade pause in front of the zoom-out (see `finishVerticalDrag`). */
23
+ closeFromSwipe(frozenDrag: FrozenDragTransform): void;
24
+ /** Live, reversible — crossing the close threshold mid-drag hides the toolbar/nav/counter/caption (a "let go now and this closes" cue); dragging back under it before release reveals them again. See `applyVerticalDragFeedback`. */
25
+ setControlsHiddenForDrag(hidden: boolean): void;
19
26
  /** `closable: false` (GalleryOptions) — false suspends vertical drag-to-close entirely: no live feedback, release never calls `close()`. */
20
27
  canClose(): boolean;
21
28
  onActivity(): void;
@@ -42,6 +49,10 @@ export interface GestureRelayCallbacks {
42
49
  export declare class GestureController {
43
50
  private readonly host;
44
51
  private readonly engine;
52
+ /** Same distance `GestureEngine` itself uses to decide a release would complete the close — reused here so the live controls-hide cue in `applyVerticalDragFeedback` actually means what it visually claims. */
53
+ private readonly controlsHideThreshold;
54
+ private controlsHiddenForDrag;
55
+ private lastDragDelta;
45
56
  constructor(host: GestureControllerHost, relay: GestureRelayCallbacks, options?: Partial<GestureEngineOptions>);
46
57
  destroy(): void;
47
58
  /** Horizontal follows the finger 1:1; vertical drives close-feedback instead. Axis-locked, one branch per drag. */
@@ -50,8 +61,33 @@ export declare class GestureController {
50
61
  /** `completed` decides intent; `canGoNext`/`canGoPrev` guard whether that's actually possible (`loop: false` at an end item). */
51
62
  private finishHorizontalDrag;
52
63
  private settleDragOffset;
53
- /** Purely presentational drag feedback — scales/fades the dialog toward `close()`'s target state as the viewer drags, without closing until release decides the outcome. */
64
+ /**
65
+ * Purely presentational — scales/fades the *image* as it's dragged away,
66
+ * without closing until release decides. Applied to `host.slides.element`,
67
+ * not `host.dialog` — requested directly: toolbar/nav/counter/caption are
68
+ * siblings, not descendants, so they stay anchored, not moving with it.
69
+ */
54
70
  private applyVerticalDragFeedback;
55
71
  private clearVerticalDragFeedback;
56
72
  private finishVerticalDrag;
73
+ /**
74
+ * Reads the drag's last live appearance, instantly resets `.shoji-slides`
75
+ * back to neutral (nothing left on it to visibly snap — `Gallery` bakes
76
+ * these same values onto the photo itself in the same synchronous tick,
77
+ * so the rendered result is unchanged, just re-homed), and returns them
78
+ * for that hand-off. Translate is the raw, unclamped drag distance —
79
+ * requested directly: the close animation must continue from exactly
80
+ * where the drag left off, full stop, not recenter first. (A previous
81
+ * version clamped this to the same 160px the dim/scale feedback ramps
82
+ * over, to bound how far away the close animation could start — but
83
+ * clamping is itself an instant correction: for any drag past that
84
+ * distance, release visibly snapped the photo from wherever it actually
85
+ * was back to the clamped point, before the real shrink-to-thumbnail
86
+ * motion continued from there. That snap — not the shrink itself — is
87
+ * what reads as "jumps to a small image in the middle of the screen."
88
+ * Reported from real usage, confirmed on video: released past the clamp,
89
+ * the photo visibly jumped from off-screen back to near-center in a
90
+ * single frame.)
91
+ */
92
+ private takeFrozenDragTransform;
57
93
  }
@@ -66,7 +66,33 @@ export declare class SlideManager {
66
66
  private moveIn;
67
67
  /** Starts decoding `item` for `index`, only if nothing already is (see `pending`). Resolves by looking up whichever slot currently wants this index, not the one active when the decode started. */
68
68
  private ensureImageDecoding;
69
- /** DESIGN.md §2.3 — swaps the spinner for the placeholder once *it* decodes, not immediately: `item.thumb` is often just `item.src` again, so an undecoded placeholder can leave as long a blank gap as the spinner it replaces. */
69
+ /**
70
+ * DESIGN.md §2.3 — swaps the spinner for the placeholder once *it*
71
+ * decodes, not immediately: `item.thumb` is often just `item.src` again,
72
+ * so an undecoded placeholder can leave as long a blank gap as the
73
+ * spinner it replaces.
74
+ *
75
+ * A real bug: `.shoji-slide-open-placeholder`'s CSS unconditionally
76
+ * force-fills the frame (deliberate default: a real photo is usually
77
+ * bigger than the dialog). Wrong for a genuinely small photo with known
78
+ * `item.width`/`item.height` — same "grows too big, snaps down once real"
79
+ * symptom the zoom-in transition itself had (§2.3b), but that fix only
80
+ * governs the animated *transform*, not this placeholder's own CSS box
81
+ * once it settles. When `naturalSize` is known, sizes the placeholder
82
+ * explicitly instead, reusing `containedBox` (`zoomTransition.ts`) —
83
+ * unknown-size items keep the original force-fill guess.
84
+ *
85
+ * A second real bug in that same fix: measured `slot.media`'s own rect as
86
+ * "the container" — but `slot.media` is the exact element the zoom-in
87
+ * transition (§2.3b) animates a `scale()` transform on. `getBoundingClientRect()`
88
+ * reflects whatever transform is *currently* applied, and the placeholder's
89
+ * own decode typically resolves before that ~300ms animation settles — so
90
+ * this measured a still-small, mid-animation rect (often the origin
91
+ * thumbnail's own tiny size) instead of the real dialog size. `slot.root`
92
+ * (`.shoji-slide`) fixes it: same box, only ever `translateX`'d for
93
+ * pool-offset positioning, never scaled — a stable read regardless of
94
+ * what its child is mid-animation through.
95
+ */
70
96
  private revealOpenPlaceholder;
71
97
  /** Native `<video controls>` — real playback, not just a poster. Not autoplayed/muted: a deliberate click-to-play, not a background loop. */
72
98
  private renderVideo;
@@ -7,6 +7,7 @@ export interface LightboxLabels {
7
7
  }
8
8
  export interface LightboxDom {
9
9
  outer: HTMLElement;
10
+ backdrop: HTMLElement;
10
11
  dialog: HTMLElement;
11
12
  counter: HTMLElement;
12
13
  caption: HTMLElement;