@wyxos/vibe 5.10.1 → 5.12.0

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/README.md CHANGED
@@ -170,8 +170,12 @@ need to select exact grouped media independently of its array position.
170
170
  ## Media lifecycle hooks
171
171
 
172
172
  Use `onMediaReady` when an image has loaded or a video has enough metadata for
173
- Vibe to render it, and `onReelMediaChange` when a reel first selects media or
174
- moves to another parent, nested, or single-item post:
173
+ Vibe to render it. `onMediaVisible` reports each ready masonry media item once
174
+ per feed visit when it first intersects the viewport. `onMediaFullyVisible`
175
+ reports each ready media item once per layout and feed visit: after the masonry
176
+ card reaches its full visibility threshold, or after the media is active and
177
+ ready in a reel.
178
+ `onReelMediaChange` reports reel selection changes:
175
179
 
176
180
  ```ts
177
181
  const vibe = createVibe({
@@ -180,19 +184,32 @@ const vibe = createVibe({
180
184
  onMediaReady: ({ postId, mediaId, layout, origin }) => {
181
185
  recordMediaReady({ postId, mediaId, layout, origin })
182
186
  },
187
+ onMediaVisible: ({ postId, mediaIndex }) => {
188
+ recordMediaPreviewed({ postId, mediaIndex })
189
+ },
190
+ onMediaFullyVisible: ({ postId, mediaIndex, layout }) => {
191
+ recordMediaViewed({ postId, mediaIndex, layout })
192
+ },
183
193
  onReelMediaChange: ({ postId, mediaId, postIndex, mediaIndex }) => {
184
194
  updateActiveMedia({ postId, mediaId, postIndex, mediaIndex })
185
195
  },
186
196
  })
187
197
  ```
188
198
 
189
- Both optional callbacks receive `VibeMediaLifecycleContext`: stable `postId`
199
+ All lifecycle callbacks receive `VibeMediaLifecycleContext`: stable `postId`
190
200
  and `mediaId` identity, zero-based `postIndex` and `mediaIndex`, the complete
191
201
  `item` and selected `media`, the rendered `layout`, and `origin`. `mediaId` is
192
202
  `null` when the source asset does not define one. `origin` is `null` for the
193
203
  masonry grid, `reel` for the base reel, and `masonry` for a reel opened from a
194
204
  masonry card. The readiness hook may run again if media remounts and becomes
195
- ready again; reel-change events describe selection changes, not load state.
205
+ ready again. A feed visit begins when Vibe mounts and restarts whenever
206
+ `refresh()` or `reload()` replaces the feed. Visibility callbacks stay
207
+ deduplicated while scrolling, loading more, or retrying pagination within that
208
+ visit.
209
+ Normal masonry cards require their complete height; cards taller than the
210
+ viewport require 80% of their maximum attainable visible height. Reel fully
211
+ visible events depend on active-and-ready state rather than intersection.
212
+ Reel-change events describe selection changes, not load state.
196
213
 
197
214
  ## Reel URLs with Vue Router
198
215
 
@@ -790,6 +807,7 @@ vibe.destroy()
790
807
  `refresh()` replaces the visible feed from its current continuation cursor. At
791
808
  the end of the feed, it requests the cursor that produced the last loaded page
792
809
  again. Use `reload()` to replace the feed from its initial `null` cursor instead.
810
+ Both operations begin a new feed visit for media visibility callbacks.
793
811
 
794
812
  Destroying an instance unmounts its Vue tree and aborts its active page request.
795
813
 
@@ -12,6 +12,7 @@ declare const __VLS_export: import("vue").DefineComponent<MasonryFeedProps, {
12
12
  retryEnd: () => any;
13
13
  activate: (postId: VibeItemId, input: "keyboard" | "pointer") => any;
14
14
  mediaChange: (postId: VibeItemId, mediaIndex: number) => any;
15
+ fullyVisible: (postId: VibeItemId, mediaIndex: number) => any;
15
16
  visible: (postId: VibeItemId, mediaIndex: number) => any;
16
17
  }, string, import("vue").PublicProps, Readonly<MasonryFeedProps> & Readonly<{
17
18
  onReady?: ((postId: VibeItemId, mediaIndex: number) => any) | undefined;
@@ -20,6 +21,7 @@ declare const __VLS_export: import("vue").DefineComponent<MasonryFeedProps, {
20
21
  onRetryEnd?: (() => any) | undefined;
21
22
  onActivate?: ((postId: VibeItemId, input: "keyboard" | "pointer") => any) | undefined;
22
23
  onMediaChange?: ((postId: VibeItemId, mediaIndex: number) => any) | undefined;
24
+ onFullyVisible?: ((postId: VibeItemId, mediaIndex: number) => any) | undefined;
23
25
  onVisible?: ((postId: VibeItemId, mediaIndex: number) => any) | undefined;
24
26
  }>, {
25
27
  leavingPostIds: ReadonlySet<VibeItemId>;
@@ -26,6 +26,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
26
26
  loadIfNearBottom: typeof loadIfNearBottom;
27
27
  moveActiveReelPost: typeof moveActiveReelPost;
28
28
  navigateToReelItem: typeof navigateToReelItem;
29
+ resetMediaLifecycle: () => void;
29
30
  startItemRemoval: typeof startItemRemoval;
30
31
  transitionActiveReelMedia: (direction: -1 | 1) => Promise<boolean>;
31
32
  transitionActiveReelPost: (postId: VibeItemId) => Promise<boolean>;
@@ -36,6 +37,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
36
37
  retryForward: () => any;
37
38
  activeReelChange: (postId: VibeItemId) => any;
38
39
  closeReel: () => any;
40
+ mediaFullyVisible: (context: VibeMediaLifecycleContext) => any;
39
41
  mediaReady: (context: VibeMediaLifecycleContext) => any;
40
42
  mediaVisible: (context: VibeMediaLifecycleContext) => any;
41
43
  openReel: (postId: VibeItemId) => any;
@@ -48,6 +50,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
48
50
  onRetryForward?: (() => any) | undefined;
49
51
  onActiveReelChange?: ((postId: VibeItemId) => any) | undefined;
50
52
  onCloseReel?: (() => any) | undefined;
53
+ onMediaFullyVisible?: ((context: VibeMediaLifecycleContext) => any) | undefined;
51
54
  onMediaReady?: ((context: VibeMediaLifecycleContext) => any) | undefined;
52
55
  onMediaVisible?: ((context: VibeMediaLifecycleContext) => any) | undefined;
53
56
  onOpenReel?: ((postId: VibeItemId) => any) | undefined;
@@ -0,0 +1,13 @@
1
+ import type { MediaPreviewState } from '../core/mediaPreview';
2
+ import type { VibeItem, VibeItemId } from '../types';
3
+ interface MasonryMediaVisibilityOptions {
4
+ fullyVisibleIndices: () => readonly number[];
5
+ items: () => readonly VibeItem[];
6
+ mediaIndices: () => ReadonlyMap<VibeItemId, number>;
7
+ onFullyVisible: (postId: VibeItemId, mediaIndex: number) => void;
8
+ onVisible: (postId: VibeItemId, mediaIndex: number) => void;
9
+ previewStates: () => ReadonlyMap<string, MediaPreviewState>;
10
+ visibleIndices: () => readonly number[];
11
+ }
12
+ export declare function useMasonryMediaVisibility(options: MasonryMediaVisibilityOptions): void;
13
+ export {};
@@ -2,11 +2,13 @@ import type { MediaPreviewState } from '../core/mediaPreview';
2
2
  import type { VibeRuntimeState } from '../core/runtime';
3
3
  import type { VibeItemId, VibeMediaLifecycleContext } from '../types';
4
4
  interface MediaLifecycleEvents {
5
+ fullyVisible: (context: VibeMediaLifecycleContext) => void;
5
6
  ready: (context: VibeMediaLifecycleContext) => void;
6
7
  reelChange: (context: VibeMediaLifecycleContext) => void;
7
8
  visible: (context: VibeMediaLifecycleContext) => void;
8
9
  }
9
10
  export declare function useMediaLifecycle(state: VibeRuntimeState, events: MediaLifecycleEvents): {
11
+ markMasonryFullyVisible: (postId: VibeItemId, mediaIndex: number) => void;
10
12
  markMasonryVisible: (postId: VibeItemId, mediaIndex: number) => void;
11
13
  markPreviewError: (postId: VibeItemId, mediaIndex: number) => void;
12
14
  markPreviewReady: (postId: VibeItemId, mediaIndex: number) => void;
@@ -15,5 +17,6 @@ export declare function useMediaLifecycle(state: VibeRuntimeState, events: Media
15
17
  originalStates: import("vue").ShallowRef<ReadonlyMap<string, MediaPreviewState>, ReadonlyMap<string, MediaPreviewState>>;
16
18
  previewStates: import("vue").ShallowRef<ReadonlyMap<string, MediaPreviewState>, ReadonlyMap<string, MediaPreviewState>>;
17
19
  reelStates: import("vue").ComputedRef<ReadonlyMap<string, MediaPreviewState>>;
20
+ resetMediaLifecycle: () => void;
18
21
  };
19
22
  export {};
@@ -2,6 +2,7 @@ import { type Ref } from 'vue';
2
2
  import type { VibeLayout } from '../types';
3
3
  interface ReelTimedMediaActivityOptions {
4
4
  active: () => boolean | undefined;
5
+ inViewport: () => boolean;
5
6
  layout: () => VibeLayout;
6
7
  mediaElement: Ref<HTMLMediaElement | null>;
7
8
  mediaIsMuted: Ref<boolean>;
@@ -2,6 +2,7 @@ import { type Ref } from 'vue';
2
2
  import type { VibeLayout, VibeMediaCardOptions, VibeReelAudioState } from '../types';
3
3
  interface TimedMediaCardOptions {
4
4
  active: () => boolean | undefined;
5
+ inViewport: () => boolean;
5
6
  layout: () => VibeLayout;
6
7
  mediaCard: () => VibeMediaCardOptions | undefined;
7
8
  mediaElement: Ref<HTMLMediaElement | null>;
@@ -58,6 +58,7 @@ export interface VibeSurfaceExpose {
58
58
  loadIfNearBottom: () => Promise<void>;
59
59
  moveActiveReelPost: (direction: -1 | 1) => boolean;
60
60
  navigateToReelItem: (target: VibeReelItemTarget) => VibeReelNavigationResult;
61
+ resetMediaLifecycle: () => void;
61
62
  startItemRemoval: (postIds: readonly VibeItemId[], options?: VibeItemRemovalOptions) => number;
62
63
  transitionActiveReelMedia: (direction: -1 | 1) => Promise<boolean>;
63
64
  transitionActiveReelPost: (postId: VibeItemId) => Promise<boolean>;
@@ -26,6 +26,10 @@ export interface MasonryViewportOptions {
26
26
  viewportHeight: number;
27
27
  overscan: number;
28
28
  }
29
+ export interface MasonryVisibilityOptions {
30
+ scrollTop: number;
31
+ viewportHeight: number;
32
+ }
29
33
  interface MasonryOptions {
30
34
  additionalHeight?: number;
31
35
  gap: number;
@@ -33,5 +37,6 @@ interface MasonryOptions {
33
37
  }
34
38
  export declare function calculateMasonryEntryOffset(options: MasonryEntryOptions): number;
35
39
  export declare function calculateVisibleMasonryIndices(items: MasonryPosition[], options: MasonryViewportOptions): number[];
40
+ export declare function calculateFullyVisibleMasonryIndices(items: readonly MasonryPosition[], candidates: readonly number[], options: MasonryVisibilityOptions): number[];
36
41
  export declare function calculateMasonryLayout(media: readonly MasonryMediaDimensions[], containerWidth: number, options: MasonryOptions): MasonryLayout;
37
42
  export {};