@wyxos/vibe 3.1.5 → 3.1.7

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
@@ -1,29 +1,47 @@
1
1
  # Vibe
2
2
 
3
- Vibe is a Vue 3 media viewer for large feeds of mixed image, video, audio, and other items.
3
+ Vibe is an opinionated Vue 3 mixed-media feed and viewer engine for large datasets.
4
4
 
5
- It ships two built-in surfaces:
5
+ It is built for apps that need a virtualized desktop feed, fullscreen browsing, library-owned paging, removals, retries, and asset-failure handling without stitching together a masonry grid, lightbox, and feed state by hand.
6
6
 
7
- - Desktop: a virtualized masonry grid that opens into fullscreen
8
- - Mobile and tablet: fullscreen only
7
+ ![Vibe desktop feed surface](./docs/readme-hero.jpg)
9
8
 
10
- The current `3.0.0` rebuild focuses on a strict item contract, library-owned loading, strong demo coverage, and a small customization surface.
9
+ [Live demo](https://vibe.wyxos.com/) · [In-app docs](https://vibe.wyxos.com/documentation)
11
10
 
12
- ## Install
11
+ ## Built for
12
+
13
+ Vibe is built for applications that browse large mixed-media collections and need feed state to stay coherent as items load, fail, disappear, and move between desktop and fullscreen surfaces.
14
+
15
+ It owns paging, fullscreen browsing, removals, retries, and asset-failure handling as part of one viewer model so those behaviors stay consistent under real feed pressure.
16
+
17
+ Vibe is hardened in production through [Atlas](https://github.com/wyxos/atlas) as a downstream consumer.
18
+
19
+ ## When Vibe fits
20
+
21
+ Use Vibe when your app needs:
22
+
23
+ - a large mixed-media feed of images, video, audio, and document-like items
24
+ - library-owned paging driven by opaque cursors
25
+ - a desktop masonry surface that opens into fullscreen
26
+ - fullscreen-first behavior on mobile and tablet
27
+ - built-in remove, restore, undo, retry, and asset-failure handling
28
+ - a small customization surface through slots instead of a headless rendering API
29
+
30
+ ## Quick start
31
+
32
+ Install the package and import the bundled stylesheet once:
13
33
 
14
34
  ```bash
15
35
  npm i @wyxos/vibe
16
36
  ```
17
37
 
18
- Import the bundled stylesheet once:
19
-
20
38
  ```ts
21
39
  import '@wyxos/vibe/style.css'
22
40
  ```
23
41
 
24
42
  Tailwind scanning is not required for the package UI.
25
43
 
26
- ## Plugin install
44
+ ### Plugin install
27
45
 
28
46
  The default export is the plugin:
29
47
 
@@ -41,7 +59,7 @@ createApp(App)
41
59
 
42
60
  That registers the global component as `VibeLayout`.
43
61
 
44
- ## Direct import
62
+ ### Direct import
45
63
 
46
64
  If you prefer local registration, import `VibeLayout` directly:
47
65
 
@@ -65,70 +83,11 @@ async function resolve({ cursor, pageSize }: VibeResolveParams): Promise<VibeRes
65
83
  </template>
66
84
  ```
67
85
 
68
- Optional pacing props:
69
-
70
- ```vue
71
- <VibeLayout
72
- :resolve="resolve"
73
- :fill-delay-ms="2000"
74
- :fill-delay-step-ms="1000"
75
- :show-end-badge="false"
76
- :show-status-badges="false"
77
- surface-mode="fullscreen"
78
- />
79
- ```
80
-
81
- - `fill-delay-ms`: base delay before the first chained fill request
82
- - `fill-delay-step-ms`: extra delay added for each additional chained fill request in the same fill cycle
83
- - defaults: `2000` and `1000`
84
- - `show-end-badge`: controls the fullscreen `End reached` badge when the feed is exhausted
85
- - `show-status-badges`: controls the built-in lifecycle status overlays in list and fullscreen
86
- - `surface-mode`: optionally lets the parent drive the desktop fullscreen/list surface explicitly
87
-
88
- Optional feed strategy:
89
-
90
- ```vue
91
- <VibeLayout
92
- :resolve="resolve"
93
- mode="dynamic"
94
- />
95
- ```
86
+ ## Core concepts
96
87
 
97
- - `dynamic` is the default
98
- - `static` reloads the current boundary cursor before advancing when the currently visible boundary page is underfilled
88
+ ### Item contract
99
89
 
100
- Optional seeded hydration:
101
-
102
- ```vue
103
- <VibeLayout
104
- :resolve="resolve"
105
- :initial-state="{
106
- cursor: 'page-10',
107
- items: restoredItems,
108
- nextCursor: 'page-11',
109
- previousCursor: 'page-9',
110
- activeIndex: 4,
111
- }"
112
- />
113
- ```
114
-
115
- - use `initialState` when the app already knows a restored slice of the feed
116
- - `resolve` is optional if you only need a seeded snapshot
117
- - when `resolve` is present, Vibe continues paging from the seeded cursors
118
-
119
- ## What Vibe does
120
-
121
- - Desktop masonry list with virtualization and staged page growth
122
- - Fullscreen viewer with swipe, wheel, keyboard, and custom media controls
123
- - Library-owned loading and pagination, optionally seeded from `initialState`
124
- - Remove, restore, and undo by item `id`
125
- - Grid customization through slots for icons, overlays, and footer UI
126
- - Built-in loading and preload error states, including explicit `404` when known
127
- - Built-in retry UI for non-404 asset failures in grid and fullscreen
128
-
129
- ## Item contract
130
-
131
- Vibe only requires a minimal item shape:
90
+ Vibe keeps the item contract deliberately small:
132
91
 
133
92
  ```ts
134
93
  type VibeViewerItem = {
@@ -136,25 +95,31 @@ type VibeViewerItem = {
136
95
  type: 'image' | 'video' | 'audio' | 'other'
137
96
  title?: string
138
97
  url: string
139
- width?: number
140
- height?: number
141
98
  preview?: {
142
99
  url: string
143
100
  width?: number
144
101
  height?: number
102
+ mediaType?: 'image' | 'video'
145
103
  }
104
+ healthCheck?: {
105
+ url: string
106
+ kind?: 'playback'
107
+ } | null
108
+ width?: number
109
+ height?: number
146
110
  [key: string]: unknown
147
111
  }
148
112
  ```
149
113
 
150
114
  Notes:
151
115
 
152
- - Grid mode prefers `preview.url`, then falls back to `url`
153
- - Fullscreen mode uses `url`
154
- - Grid layout prefers `preview.width/height`, then root `width/height`, then a square fallback tile
155
- - `other` is intentionally broad so the consuming app can layer its own subtypes and icon logic on top
116
+ - grid mode prefers `preview.url`, then falls back to `url`
117
+ - fullscreen mode uses `url`
118
+ - grid layout prefers preview dimensions, then root dimensions, then a square fallback tile
119
+ - `other` stays intentionally broad so the consuming app can layer its own file subtypes and icon logic on top
120
+ - `healthCheck` is optional and lets the app provide an explicit asset probe, for example when preview URLs are not enough to classify playback health reliably
156
121
 
157
- ## Loading
122
+ ### Resolve and feed state
158
123
 
159
124
  Use `resolve` when you want Vibe to own the paging loop:
160
125
 
@@ -162,6 +127,7 @@ Use `resolve` when you want Vibe to own the paging loop:
162
127
  type VibeResolveParams = {
163
128
  cursor: string | null
164
129
  pageSize: number
130
+ signal?: AbortSignal
165
131
  }
166
132
 
167
133
  type VibeResolveResult = {
@@ -171,25 +137,6 @@ type VibeResolveResult = {
171
137
  }
172
138
  ```
173
139
 
174
- ```vue
175
- <script setup lang="ts">
176
- import {
177
- VibeLayout,
178
- type VibeResolveParams,
179
- type VibeResolveResult,
180
- } from '@wyxos/vibe'
181
-
182
- async function resolve({ cursor, pageSize }: VibeResolveParams): Promise<VibeResolveResult> {
183
- const response = await fetch(`/api/feed?cursor=${cursor ?? ''}&pageSize=${pageSize}`)
184
- return await response.json()
185
- }
186
- </script>
187
-
188
- <template>
189
- <VibeLayout :resolve="resolve" />
190
- </template>
191
- ```
192
-
193
140
  Vibe owns:
194
141
 
195
142
  - loaded items
@@ -198,8 +145,11 @@ Vibe owns:
198
145
  - optional previous-page loading
199
146
  - duplicate cursor protection
200
147
  - initial retry state
148
+ - removal-aware feed navigation
149
+
150
+ ### Feed strategies
201
151
 
202
- Vibe also supports two feed strategies:
152
+ Vibe supports two feed strategies:
203
153
 
204
154
  - `dynamic`:
205
155
  - default behavior
@@ -213,7 +163,23 @@ Vibe also supports two feed strategies:
213
163
  - if it is, Vibe reloads that same cursor in place first
214
164
  - only once that boundary page is full again will the next edge hit advance to the next or previous cursor
215
165
 
216
- You can also seed the viewer from a known snapshot:
166
+ Example:
167
+
168
+ ```vue
169
+ <VibeLayout
170
+ :resolve="resolve"
171
+ mode="dynamic"
172
+ :page-size="25"
173
+ :fill-delay-ms="2000"
174
+ :fill-delay-step-ms="1000"
175
+ :show-end-badge="false"
176
+ :show-status-badges="false"
177
+ />
178
+ ```
179
+
180
+ ### Seeded hydration
181
+
182
+ Use `initialState` when the app already knows a restored slice of the feed and wants Vibe to hydrate from that snapshot immediately:
217
183
 
218
184
  ```vue
219
185
  <VibeLayout
@@ -228,58 +194,59 @@ You can also seed the viewer from a known snapshot:
228
194
  />
229
195
  ```
230
196
 
231
- ## Slots
197
+ Notes:
232
198
 
233
- `VibeLayout` exposes three customization slots:
199
+ - `resolve` is optional if you only need a seeded snapshot
200
+ - when `resolve` is present, Vibe continues paging from the seeded cursors
234
201
 
235
- ### `#item-icon`
202
+ ### Surface behavior
236
203
 
237
- Lets the app replace the fallback icon, especially useful for `other` items.
204
+ - desktop starts in the masonry list surface
205
+ - clicking a grid tile opens fullscreen
206
+ - `Escape` returns from fullscreen to the list on desktop
207
+ - mobile and tablet always force fullscreen
208
+ - grid uses preview assets and in-view loading
209
+ - fullscreen uses the original asset and shows a spinner until ready
238
210
 
239
- Slot props:
211
+ Common behavior props:
240
212
 
241
- - `item`
242
- - `icon`
213
+ - `surfaceMode`: lets the parent drive the desktop list/fullscreen surface explicitly
214
+ - `emptyStateMode`: controls whether empty states render inline, as a badge, or stay hidden
215
+ - `paginationDetail`: lets the parent attach app-owned cursor or page context to the built-in status UI
216
+ - `showDominantImageTone`, `loopFullscreenVideo`, `showEndBadge`, and `showStatusBadges`: tune the built-in fullscreen and status behavior without taking over the surfaces
243
217
 
244
- ```vue
245
- <VibeLayout :resolve="resolve">
246
- <template #item-icon="{ item, icon }">
247
- <component :is="item.type === 'other' ? MyCustomIcon : icon" />
248
- </template>
249
- </VibeLayout>
250
- ```
218
+ ## Customization
251
219
 
252
- ### `#grid-item-overlay`
220
+ ### Surface slots
253
221
 
254
- Desktop grid-only overlay content for reactions, menus, badges, and similar controls.
222
+ `VibeLayout` exposes a small set of app-owned surfaces instead of a headless render API:
255
223
 
256
- Slot props:
224
+ - `empty-state`
225
+ - `fullscreen-aside`
226
+ - `fullscreen-header-actions`
227
+ - `fullscreen-overlay`
228
+ - `fullscreen-status`
229
+ - `grid-footer`
230
+ - `grid-item-overlay`
231
+ - `grid-status`
232
+ - `item-icon`
257
233
 
258
- - `item`
259
- - `index`
260
- - `active`
261
- - `hovered`
262
- - `focused`
263
- - `openFullscreen`
234
+ Example:
264
235
 
265
236
  ```vue
266
237
  <VibeLayout :resolve="resolve">
238
+ <template #item-icon="{ item, icon }">
239
+ <component :is="item.type === 'other' ? MyCustomIcon : icon" />
240
+ </template>
241
+
267
242
  <template #grid-item-overlay="{ item, hovered }">
268
243
  <div v-if="hovered" class="absolute inset-x-0 bottom-0 p-3">
269
244
  <div class="pointer-events-auto bg-black/45 px-3 py-2 backdrop-blur">
270
- {{ item.title }}
245
+ Like {{ item.title }}
271
246
  </div>
272
247
  </div>
273
248
  </template>
274
- </VibeLayout>
275
- ```
276
249
 
277
- ### `#grid-footer`
278
-
279
- Desktop grid-only footer surface for app-owned status bars or controls.
280
-
281
- ```vue
282
- <VibeLayout :resolve="resolve">
283
250
  <template #grid-footer>
284
251
  <div class="bg-black/55 px-4 py-3 backdrop-blur">
285
252
  Custom footer UI
@@ -288,7 +255,7 @@ Desktop grid-only footer surface for app-owned status bars or controls.
288
255
  </VibeLayout>
289
256
  ```
290
257
 
291
- ## Exposed handle
258
+ ### Exposed handle
292
259
 
293
260
  Get a component ref and use `VibeHandle`:
294
261
 
@@ -332,9 +299,11 @@ type VibeStatus = {
332
299
  itemCount: number
333
300
  loadState: 'failed' | 'loaded' | 'loading'
334
301
  mode: 'dynamic' | 'static'
302
+ nextBoundaryLoadProgress: number
335
303
  nextCursor: string | null
336
304
  pageLoadingLocked: boolean
337
305
  phase: 'failed' | 'filling' | 'idle' | 'initializing' | 'loading' | 'refreshing'
306
+ previousBoundaryLoadProgress: number
338
307
  previousCursor: string | null
339
308
  removedCount: number
340
309
  removedIds: readonly string[]
@@ -351,10 +320,11 @@ vibe.value?.undo()
351
320
  vibe.value?.unlockPageLoading()
352
321
  console.log(vibe.value?.status.itemCount)
353
322
  console.log(vibe.value?.status.pageLoadingLocked)
323
+ console.log(vibe.value?.status.nextBoundaryLoadProgress)
354
324
  console.log(vibe.value?.status.removedIds)
355
325
  ```
356
326
 
357
- ## Events
327
+ ### Events
358
328
 
359
329
  `VibeLayout` emits:
360
330
 
@@ -420,15 +390,6 @@ Notes:
420
390
  - if the same item fails again later, it can emit again in a later batch
421
391
  - built-in Vibe error surfaces allow retrying `generic` failures, but not `not-found`
422
392
 
423
- ## Surface behavior
424
-
425
- - Desktop starts in the masonry grid
426
- - Clicking a grid tile opens fullscreen
427
- - `Escape` returns from fullscreen to grid on desktop
428
- - Mobile and tablet always force fullscreen
429
- - Grid uses preview assets and in-view loading
430
- - Fullscreen uses the original asset and shows a spinner until ready
431
-
432
393
  ## Local demo routes
433
394
 
434
395
  Run:
@@ -439,11 +400,11 @@ npm run dev
439
400
 
440
401
  Routes:
441
402
 
442
- - `/` clean default demo
443
- - `/documentation` in-app documentation
444
- - `/demo/dynamic-feed` dynamic feed fill-loop demo
445
- - `/demo/advanced-integration` advanced static integration demo
446
- - `/debug/fake-server` fake-server inspection route
403
+ - `/` - default feed surface
404
+ - `/documentation` - in-app documentation
405
+ - `/demo/dynamic-feed` - dynamic feed fill-loop demo
406
+ - `/demo/advanced-integration` - static feed, removals, and paging-lock demo
407
+ - `/debug/fake-server` - fake-server inspection route
447
408
 
448
409
  ## Local development
449
410
 
@@ -459,5 +420,5 @@ npm run test:e2e
459
420
  ## Notes
460
421
 
461
422
  - `lib/` is generated output
462
- - Source of truth is under `src/`
463
- - The package ships compiled CSS at `@wyxos/vibe/style.css`
423
+ - source of truth is under `src/`
424
+ - the package ships compiled CSS at `@wyxos/vibe/style.css`
@@ -28,10 +28,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
28
28
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
29
29
  onOpen?: (() => any) | undefined;
30
30
  }>, {
31
+ index: number;
31
32
  active: boolean;
32
33
  reportAssetError: VibeAssetErrorReporter | null;
33
34
  reportAssetLoad: VibeAssetLoadReporter | null;
34
- index: number;
35
35
  surfaceActive: boolean;
36
36
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
37
37
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -0,0 +1,15 @@
1
+ import { type Ref } from 'vue';
2
+ import type { VibeViewerItem } from '../viewer';
3
+ export declare function useFullscreenPreloadController(options: {
4
+ active: Ref<boolean | undefined>;
5
+ items: Ref<VibeViewerItem[]>;
6
+ resolvedActiveIndex: Ref<number>;
7
+ getItemKey: (item: VibeViewerItem) => string;
8
+ onResetAssetState: (id: string) => void;
9
+ }): {
10
+ clearBackgroundPreloads: () => void;
11
+ registerImageElement: (id: string, element: unknown) => void;
12
+ registerMediaElement: (id: string, element: unknown) => void;
13
+ settleBackgroundPreload: (id: string) => void;
14
+ shouldAttachSlideAsset: (index: number) => boolean;
15
+ };
@@ -2,6 +2,7 @@ import type { Ref } from 'vue';
2
2
  import type { VibeViewerItem } from '../viewer';
3
3
  export declare function useFullscreenSurfaceMedia(options: {
4
4
  active: Ref<boolean | undefined>;
5
+ items: Ref<VibeViewerItem[]>;
5
6
  resolvedActiveIndex: Ref<number>;
6
7
  viewer: {
7
8
  getAssetErrorKind: (id: string) => unknown;
@@ -9,15 +10,20 @@ export declare function useFullscreenSurfaceMedia(options: {
9
10
  getImageSource: (item: VibeViewerItem) => string | undefined;
10
11
  isImageReady: (id: string) => boolean;
11
12
  isMediaReady: (id: string) => boolean;
13
+ resetAssetState: (id: string) => void;
12
14
  };
13
15
  }): {
14
16
  getAssetErrorKind: (item: VibeViewerItem) => unknown;
15
17
  getAssetErrorLabel: (item: VibeViewerItem) => string;
16
18
  getFullscreenImageSource: (index: number, item: VibeViewerItem) => string | undefined;
19
+ getFullscreenMediaPreload: (index: number) => "none" | "metadata";
17
20
  getFullscreenMediaSource: (index: number, item: VibeViewerItem) => string | undefined;
18
21
  getItemKey: (item: VibeViewerItem) => string;
19
22
  getMediaActionLabel: (action: "Play" | "Pause", item: VibeViewerItem) => string;
20
23
  isAssetErrored: (index: number, item: VibeViewerItem) => boolean;
21
24
  isAssetLoading: (index: number, item: VibeViewerItem) => boolean;
25
+ registerImageElement: (id: string, element: unknown) => void;
26
+ registerMediaElement: (id: string, element: unknown) => void;
27
+ settleBackgroundPreload: (id: string) => void;
22
28
  shouldPreloadSlideAsset: (index: number) => boolean;
23
29
  };
@@ -37,6 +37,7 @@ export declare function useMedia(options: {
37
37
  registerAudioElement: (id: string, element: unknown) => void;
38
38
  registerImageElement: (id: string, element: unknown) => void;
39
39
  registerVideoElement: (id: string, element: unknown) => void;
40
+ resetAssetState: (id: string) => void;
40
41
  resetMediaState: () => void;
41
42
  retryAsset: (id: string) => Promise<void>;
42
43
  syncMediaPlayback: () => Promise<void>;
@@ -67,6 +67,7 @@ export declare function useViewer(props: Readonly<VibeViewerProps>, emit: (event
67
67
  renderedItems: import("vue").ComputedRef<import("./virtualization").VibeRenderedItem[]>;
68
68
  renderedRange: import("vue").ComputedRef<import("./virtualization").VibeRenderedRange>;
69
69
  resolvedActiveIndex: import("vue").ComputedRef<number>;
70
+ resetAssetState: (id: string) => void;
70
71
  retryInitialLoad: () => Promise<void>;
71
72
  retryAsset: (id: string) => Promise<void>;
72
73
  stageRef: Ref<HTMLElement | null, HTMLElement | null>;