@wyxos/vibe 1.6.29 → 2.0.1

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.
Files changed (43) hide show
  1. package/README.md +29 -287
  2. package/lib/index.cjs +1 -0
  3. package/lib/index.js +778 -1792
  4. package/lib/logo-dark.svg +36 -36
  5. package/lib/logo-light.svg +29 -29
  6. package/lib/logo.svg +32 -32
  7. package/lib/manifest.json +1 -1
  8. package/package.json +82 -96
  9. package/LICENSE +0 -21
  10. package/lib/vibe.css +0 -1
  11. package/lib/vite.svg +0 -1
  12. package/src/App.vue +0 -35
  13. package/src/Masonry.vue +0 -1030
  14. package/src/archive/App.vue +0 -96
  15. package/src/archive/InfiniteMansonry.spec.ts +0 -10
  16. package/src/archive/InfiniteMasonry.vue +0 -218
  17. package/src/assets/vue.svg +0 -1
  18. package/src/calculateLayout.ts +0 -194
  19. package/src/components/CodeTabs.vue +0 -158
  20. package/src/components/MasonryItem.vue +0 -499
  21. package/src/components/examples/BasicExample.vue +0 -46
  22. package/src/components/examples/CustomItemExample.vue +0 -87
  23. package/src/components/examples/HeaderFooterExample.vue +0 -79
  24. package/src/components/examples/ManualInitExample.vue +0 -78
  25. package/src/components/examples/SwipeModeExample.vue +0 -40
  26. package/src/createMasonryTransitions.ts +0 -176
  27. package/src/main.ts +0 -6
  28. package/src/masonryUtils.ts +0 -96
  29. package/src/pages.json +0 -36402
  30. package/src/router/index.ts +0 -20
  31. package/src/style.css +0 -32
  32. package/src/types.ts +0 -101
  33. package/src/useMasonryDimensions.ts +0 -59
  34. package/src/useMasonryItems.ts +0 -231
  35. package/src/useMasonryLayout.ts +0 -164
  36. package/src/useMasonryPagination.ts +0 -539
  37. package/src/useMasonryScroll.ts +0 -61
  38. package/src/useMasonryVirtualization.ts +0 -140
  39. package/src/useSwipeMode.ts +0 -233
  40. package/src/utils/errorHandler.ts +0 -8
  41. package/src/views/Examples.vue +0 -323
  42. package/src/views/Home.vue +0 -321
  43. package/toggle-link.mjs +0 -92
package/README.md CHANGED
@@ -1,287 +1,29 @@
1
- # VIBE — Vue Infinite Block Engine
2
-
3
- [![npm](https://img.shields.io/npm/v/@wyxos/vibe?color=%2300c58e&label=npm)](https://www.npmjs.com/package/@wyxos/vibe)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Demo](https://img.shields.io/badge/Demo-Live%20Preview-blue?logo=githubpages)](https://wyxos.github.io/vibe/)
6
-
7
- A high-performance, responsive masonry layout engine for Vue 3 with built-in infinite scrolling and virtualization.
8
-
9
- VIBE (Vue Infinite Block Engine) is designed for applications that need to display large datasets in a masonry grid without compromising performance. Unlike other masonry libraries, VIBE leverages virtualization to render only what is visible on the screen, ensuring smooth scrolling even with thousands of items.
10
-
11
- ![VIBE Demo](demo.webp)
12
-
13
- ---
14
-
15
- ## Features
16
-
17
- - **High Performance Virtualization**: Efficiently renders thousands of items by only mounting elements currently in the viewport.
18
- - **Responsive Masonry Layout**: Automatically adjusts column counts and layout based on screen width and breakpoints.
19
- - **Mobile Swipe Feed**: Automatically switches to a vertical swipe feed on mobile devices for optimal mobile UX.
20
- - **Infinite Scrolling**: Seamlessly loads more content as the user scrolls, with built-in support for async data fetching.
21
- - **Dynamic Updates**: Supports adding, removing, and reflowing items with smooth FLIP animations.
22
- - **Scroll Position Maintenance**: Keeps the user's scroll position stable when new items are loaded or the layout changes.
23
- - **Built-in Item Component**: Includes a production-ready `MasonryItem` with lazy loading, image/video support, error handling, and hover effects.
24
- - **Customizable Rendering**: Full control over item markup via scoped slots.
25
-
26
- ---
27
-
28
- ## Installation
29
-
30
- ```bash
31
- npm install @wyxos/vibe
32
- ```
33
-
34
- ---
35
-
36
- ## Usage
37
-
38
- ### Basic Usage (Default Item)
39
-
40
- By default, VIBE uses the built-in `MasonryItem` component, which handles image loading and provides a clean UI. On mobile devices (screen width < 768px by default), it automatically switches to a vertical swipe feed mode where users can swipe through items one at a time.
41
-
42
- ```vue
43
- <script setup>
44
- import { ref } from 'vue'
45
- import { Masonry } from '@wyxos/vibe'
46
-
47
- const items = ref([])
48
-
49
- // Layout configuration
50
- const layout = {
51
- gutterX: 12,
52
- gutterY: 12,
53
- sizes: { base: 1, sm: 2, md: 3, lg: 4 }
54
- }
55
-
56
- async function getPage(page) {
57
- const response = await fetch(`/api/items?page=${page}`)
58
- const data = await response.json()
59
- // Items must have a 'src' property for the default MasonryItem
60
- // Optional: include 'type' ('image' or 'video') and 'notFound' (boolean)
61
- return {
62
- items: data.items,
63
- nextPage: page + 1
64
- }
65
- }
66
- </script>
67
-
68
- <template>
69
- <Masonry
70
- v-model:items="items"
71
- :get-page="getPage"
72
- :layout="layout"
73
- layout-mode="auto"
74
- :mobile-breakpoint="768"
75
- />
76
- </template>
77
- ```
78
-
79
- ### Initialization Modes
80
-
81
- VIBE supports two initialization modes:
82
-
83
- - **`'auto'`**: Automatically calls `loadPage` on mount to fetch the first page. Use this when you want the component to start loading immediately.
84
- - **`'manual'`**: Does nothing on mount. You must manually call `initialize()` to initialize the component with items. Use this when you need to restore items from saved state or have more control over when loading begins.
85
-
86
- ```vue
87
- <!-- Auto mode: loads first page automatically -->
88
- <Masonry
89
- v-model:items="items"
90
- :get-page="getPage"
91
- init="auto"
92
- :load-at-page="1"
93
- />
94
-
95
- <!-- Manual mode: you control when to initialize -->
96
- <Masonry
97
- ref="masonry"
98
- v-model:items="items"
99
- :get-page="getPage"
100
- init="manual"
101
- />
102
- <script setup>
103
- const masonry = ref(null)
104
-
105
- // Later, initialize manually
106
- masonry.value.items = []
107
- masonry.value.initialize(savedItems, savedPage, savedNextPage)
108
- </script>
109
- ```
110
-
111
- ### Layout Modes
112
-
113
- VIBE supports three layout modes:
114
-
115
- - **`'auto'`** (default): Automatically switches between masonry grid (desktop) and swipe feed (mobile) based on screen width
116
- - **`'masonry'`**: Always use masonry grid layout regardless of screen size
117
- - **`'swipe'`**: Always use swipe feed layout regardless of screen size
118
-
119
- ```vue
120
- <!-- Force masonry layout on all devices -->
121
- <Masonry layout-mode="masonry" ... />
122
-
123
- <!-- Force swipe feed on all devices -->
124
- <Masonry layout-mode="swipe" ... />
125
-
126
- <!-- Custom breakpoint (use Tailwind breakpoint name) -->
127
- <Masonry layout-mode="auto" mobile-breakpoint="lg" ... />
128
-
129
- <!-- Custom breakpoint (use pixel value) -->
130
- <Masonry layout-mode="auto" :mobile-breakpoint="1024" ... />
131
- ```
132
-
133
- ### Custom Item Rendering
134
-
135
- You can fully customize the item rendering using the `#item` slot. You can also import and use `MasonryItem` inside the slot if you want to wrap it or extend it.
136
-
137
- ```vue
138
- <script setup>
139
- import { Masonry, MasonryItem } from '@wyxos/vibe'
140
- // ... setup code ...
141
- </script>
142
-
143
- <template>
144
- <Masonry
145
- v-model:items="items"
146
- :get-page="getPage"
147
- :layout="layout"
148
- >
149
- <template #item="{ item, remove }">
150
- <!-- Custom container -->
151
- <div class="custom-card">
152
- <!-- You can use the built-in item or your own -->
153
- <MasonryItem :item="item" :remove="remove">
154
- <!-- Optional: MasonryItem also has a default slot for overlays -->
155
- <div class="absolute bottom-0 p-2 text-white">
156
- {{ item.title }}
157
- </div>
158
- </MasonryItem>
159
- </div>
160
- </template>
161
- </Masonry>
162
- </template>
163
- ```
164
-
165
- ### Item Data Structure
166
-
167
- Items can include the following properties:
168
-
169
- ```javascript
170
- {
171
- id: 'unique-id', // Required: unique identifier
172
- width: 300, // Required: original width
173
- height: 200, // Required: original height
174
- src: 'https://...', // Required: media source URL
175
- type: 'image' | 'video', // Optional: media type (defaults to 'image')
176
- notFound: false, // Optional: show "Not Found" state
177
- // ... any other custom properties
178
- }
179
- ```
180
-
181
- ### Slot Props
182
-
183
- The `MasonryItem` component exposes the following props to its default slot:
184
-
185
- - `item`: The item object
186
- - `remove`: The remove callback function
187
- - `imageLoaded`: Boolean indicating if image has loaded
188
- - `imageError`: Boolean indicating if image failed to load
189
- - `videoLoaded`: Boolean indicating if video has loaded
190
- - `videoError`: Boolean indicating if video failed to load
191
- - `showNotFound`: Boolean indicating if item is in "not found" state
192
- - `isLoading`: Boolean indicating if media is currently loading
193
- - `mediaType`: String indicating the media type ('image' or 'video')
194
-
195
- ---
196
-
197
- ## Props
198
-
199
- | Prop | Type | Required | Description |
200
- |------|------|----------|-------------|
201
- | `items` | `Array` | Yes | Two-way bound item array. Each item must include `width`, `height`, and `id`. |
202
- | `getPage` | `Function(page: Number)` | Yes | Async function to load a page. Must return `{ items, nextPage }`. |
203
- | `layout` | `Object` | No | Configuration object for layout, including sizes and gutters. |
204
- | `loadAtPage` | `Number` | No | The starting page number (default: `1`). |
205
- | `init` | `String` | No | Initialization mode: `'auto'` (automatically loads first page on mount) or `'manual'` (user must call `initialize()` manually) (default: `'manual'`). |
206
- | `paginationType` | `String` | No | `'page'` or `'cursor'` (default: `'page'`). |
207
- | `pageSize` | `Number` | No | Number of items per page, used for backfilling (default: `40`). |
208
- | `layoutMode` | `String` | No | Layout mode: `'auto'` (detect from screen size), `'masonry'`, or `'swipe'` (default: `'auto'`). |
209
- | `mobileBreakpoint` | `Number \| String` | No | Breakpoint for switching to swipe mode in pixels or Tailwind breakpoint name (default: `768`). |
210
-
211
- ### Layout Configuration Example
212
-
213
- ```js
214
- {
215
- gutterX: 10,
216
- gutterY: 10,
217
- sizes: {
218
- base: 1,
219
- sm: 2,
220
- md: 3,
221
- lg: 4,
222
- xl: 5,
223
- '2xl': 6
224
- }
225
- }
226
- ```
227
-
228
- ---
229
-
230
- ## MasonryItem Component
231
-
232
- The built-in `MasonryItem` component is available for use within the `#item` slot or as a standalone component. It provides intelligent lazy loading, media type detection, and comprehensive error handling.
233
-
234
- ### Props
235
-
236
- | Prop | Type | Description |
237
- |------|------|-------------|
238
- | `item` | `Object` | The item object. Must contain `src` for media loading. Can include `type` (`'image'` or `'video'`), `notFound` (boolean), and other custom properties. |
239
- | `remove` | `Function` | Optional callback to remove the item. If provided, a remove button is shown on hover. |
240
- | `type` | `'image' \| 'video'` | Optional. Overrides `item.type`. Defaults to `'image'`. |
241
- | `notFound` | `Boolean` | Optional. Overrides `item.notFound`. When `true`, displays a "Not Found" state instead of loading media. |
242
-
243
- ### Features
244
-
245
- - **Lazy Loading with Intersection Observer**: Only starts preloading media when the item comes into view (50%+ visible), significantly improving initial page load performance.
246
- - **Image & Video Support**: Automatically handles both images and videos with appropriate loading strategies.
247
- - **Media Type Indicator**: Shows a badge icon (image/video) on hover to indicate the media type.
248
- - **Smart Spinner**: Displays a loading spinner underneath the media (not covering it) during preload.
249
- - **Error Handling**: Displays user-friendly error states if media fails to load.
250
- - **Not Found State**: Special visual state for items that cannot be located.
251
- - **Hover Effects**: Includes subtle zoom, overlay gradient, and smooth transitions.
252
- - **Performance Optimized**: Properly cleans up Intersection Observers to prevent memory leaks.
253
-
254
- ---
255
-
256
- ## Slots
257
-
258
- | Slot Name | Props | Description |
259
- |-----------|-------|-------------|
260
- | `item` | `{ item, remove }` | Scoped slot for custom rendering of each masonry block. |
261
-
262
- ---
263
-
264
- ## Run Locally
265
-
266
- To run the demo project locally:
267
-
268
- ```bash
269
- git clone https://github.com/wyxos/vibe
270
- cd vibe
271
- npm install
272
- npm run dev
273
- ```
274
-
275
- Visit `http://localhost:5173` to view the demo.
276
-
277
- ---
278
-
279
- ## Live Demo
280
-
281
- [View Live Demo on GitHub Pages](https://wyxos.github.io/vibe/)
282
-
283
- ---
284
-
285
- ## License
286
-
287
- MIT © [@wyxos](https://github.com/wyxos)
1
+ # @wyxos/vibe
2
+
3
+ VIBE (Vue Infinite Block Engine) is a high-performance masonry feed component for Vue 3.
4
+
5
+ It’s built for large, scroll-heavy media feeds and aims to stay smooth with thousands of items.
6
+
7
+ ## Highlights
8
+
9
+ - Virtualized rendering (keeps DOM small)
10
+ - Infinite loading via an async `getContent(pageToken) => { items, nextPage }`
11
+ - Optional backfill mode to reach a target `pageSize`
12
+ - Smooth removal + reorder transitions, with `remove` / `undo` / `restore`
13
+ - Preserves a single DOM sequence (no column re-parenting)
14
+ - Optional `itemHeader` / `itemFooter` slots per card
15
+
16
+ ## Demo
17
+
18
+ - Local: `npm run dev`
19
+ - Live: https://vibe.wyxos.com/
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm i @wyxos/vibe
25
+ ```
26
+ ## Local library build
27
+
28
+ - JS bundles: `npm run build:lib`
29
+ - Types: `npm run build:types`
package/lib/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("vue"),ze={mode:"default",pageSize:20,backfillRequestDelayMs:2e3,page:1,itemWidth:300,prefetchThresholdPx:200,gapX:16,gapY:16,headerHeight:0,footerHeight:0,overscanPx:600};function Ge(l,y){return!l||l<=0||!y||y<=0?1:Math.max(1,Math.floor(l/y))}function Ke(l,y,B,o=0){if(!l||l<=0||!y||y<=0)return B;const i=typeof o=="number"&&o>0?o:0,b=Math.max(0,y-1)*i,w=l-b;return!w||w<=0?B:w/y}function Je(l,y){const B=l?.width,o=l?.height;return typeof B=="number"&&typeof o=="number"&&B>0&&o>0?o/B*y:y}function He(l){return Number.isFinite(l)&&l>0?Math.floor(l):1}function fe(l){return Number.isFinite(l)&&l>0?Math.floor(l):0}function Qe(l){async function y(o){const i=fe(o);if(i<=0)return;l.stats.value={...l.stats.value,cooldownMsTotal:i,cooldownMsRemaining:i};const b=Date.now(),w=100;await new Promise(h=>{const m=setInterval(()=>{const g=Date.now()-b,k=Math.max(0,i-g);l.stats.value={...l.stats.value,cooldownMsTotal:i,cooldownMsRemaining:k},k<=0&&(clearInterval(m),h())},w)})}async function B(o){const i=He(l.getPageSize()),b=l.isEnabled(),w=fe(l.getRequestDelayMs()),h=[];let m=0;l.buffer.value.length&&(m=l.buffer.value.length,h.push(...l.buffer.value),l.buffer.value=[]),l.stats.value={...l.stats.value,enabled:b,isBackfillActive:!1,isRequestInFlight:!1,requestPage:null,cooldownMsTotal:w,cooldownMsRemaining:0,progress:{collected:0,target:0},pageSize:i,bufferSize:0};const g=[];let k=o,F=0,I=!1;for(;h.length<i&&k!=null;){const N=k;I&&(l.stats.value={...l.stats.value,enabled:b,isBackfillActive:!0,isRequestInFlight:!0,requestPage:N,progress:{collected:Math.min(h.length,i),target:i},cooldownMsTotal:w,cooldownMsRemaining:0,pageSize:i});const x=await l.getContent(N);g.push(N),I&&(l.stats.value={...l.stats.value,enabled:b,isBackfillActive:!0,isRequestInFlight:!1,requestPage:null}),F+=x.items.length,l.markEnterFromLeft(x.items),h.push(...x.items),k=x.nextPage,!I&&h.length<i&&k!=null?(I=!0,l.stats.value={...l.stats.value,enabled:b,isBackfillActive:!0,isRequestInFlight:!1,requestPage:null,progress:{collected:Math.min(h.length,i),target:i},cooldownMsTotal:w,cooldownMsRemaining:0,pageSize:i}):I&&(l.stats.value={...l.stats.value,enabled:b,isBackfillActive:!0,progress:{collected:Math.min(h.length,i),target:i}}),I&&h.length<i&&k!=null&&await y(w)}const E=h.slice(0,i),S=h.slice(i);return l.buffer.value=S,l.stats.value={...l.stats.value,enabled:b,isBackfillActive:!1,isRequestInFlight:!1,requestPage:null,progress:{collected:0,target:0},cooldownMsTotal:w,cooldownMsRemaining:0,pageSize:i,bufferSize:S.length,lastBatch:{startPage:o,pages:g,usedFromBuffer:m,fetchedFromNetwork:F,collectedTotal:h.length,emitted:E.length,carried:S.length},totals:{pagesFetched:l.stats.value.totals.pagesFetched+g.length,itemsFetchedFromNetwork:l.stats.value.totals.itemsFetchedFromNetwork+F}},{batchItems:E,pages:g,nextPage:k}}return{loadBackfillBatch:B}}function Ze(l){const y=l.columnCount,B=l.columnWidth,o=l.gapX,i=l.gapY,b=l.headerHeight,w=l.footerHeight,h=l.bucketPx,m=Array.from({length:y},()=>0),g=new Array(l.items.length),k=new Array(l.items.length),F=new Map,I=new Map;let E=0;for(let S=0;S<l.items.length;S+=1){const N=l.items[S];N?.id&&I.set(N.id,S);let x=0;for(let M=1;M<m.length;M+=1)m[M]<m[x]&&(x=M);const z=x*(B+o),H=m[x],C=Je(N,B)+b+w;g[S]={x:z,y:H},k[S]=C,m[x]=H+C+i,E=Math.max(E,H+C);const G=Math.floor(H/h),K=Math.floor((H+C)/h);for(let M=G;M<=K;M+=1){const A=F.get(M);A?A.push(S):F.set(M,[S])}}return{positions:g,heights:k,buckets:F,contentHeight:E,indexById:I}}function et(l){const y=l.itemCount;if(!y)return[];if(l.viewportHeight<=0)return Array.from({length:y},(m,g)=>g);const B=Math.max(0,l.scrollTop-l.overscanPx),o=l.scrollTop+l.viewportHeight+l.overscanPx,i=Math.floor(B/l.bucketPx),b=Math.floor(o/l.bucketPx),w=new Set;for(let m=i;m<=b;m+=1){const g=l.buckets.get(m);if(g)for(const k of g)w.add(k)}const h=Array.from(w);return h.sort((m,g)=>m-g),h}const tt={key:0,class:"flex h-full items-center justify-center"},nt={key:1,class:"text-sm font-medium text-red-700"},lt=["src","width","height","alt"],at=["poster"],ot=["src"],st=["src","width","height","alt"],rt=["poster"],it=["src"],ct={class:"mt-4 pb-2 text-center text-xs text-slate-600"},ut={key:0,class:"inline-flex items-center justify-center gap-2"},ft={key:1},dt={key:2},mt=200,Ne=600,U=300,Ce=e.defineComponent({inheritAttrs:!1,__name:"Masonry",props:e.mergeDefaults({getContent:{},mode:{},pageSize:{},backfillRequestDelayMs:{},items:{},page:{},itemWidth:{},prefetchThresholdPx:{},gapX:{},gapY:{},headerHeight:{},footerHeight:{},overscanPx:{}},ze),emits:["update:items"],setup(l,{expose:y,emit:B}){const o=l,i=B,b=e.useAttrs(),w=e.useSlots(),h=e.computed(()=>{const{class:t,...a}=b;return a}),m=e.ref(null),g=e.ref(0),k=e.ref(0),F=e.ref(0);let I;const E=e.computed(()=>o.gapX),S=e.computed(()=>o.gapY);function N(t){if(!t)return 0;const a=Math.max(0,E.value);return Math.max(0,t.clientWidth-a)}const x=e.computed(()=>o.headerHeight),z=e.computed(()=>o.footerHeight),H=e.computed(()=>!!w.itemHeader),C=e.computed(()=>!!w.itemFooter),G=e.computed(()=>{if(x.value>0)return{height:`${x.value}px`}}),K=e.computed(()=>{if(z.value>0)return{height:`${z.value}px`}}),M=e.ref([]),A=e.ref([]),de=e.ref(new Map),me=e.ref(0),J=e.ref(new Map),q=e.ref(new Set),O=e.ref(new Set),ee=new Set,te=e.ref(new Map),R=e.ref(new Set),V=e.ref([]);function Re(t){const a=te.value.get(t);return a||{dx:0,dy:0}}function Ve(t){return O.value.has(t)||R.value.has(t)?`transform ${U}ms ease-out`:void 0}function _e(t){const n=r.value[t]?.id,s=M.value[t]??{x:0,y:0},c=A.value[t]??0,v=c>0?c:j.value,p=s.x,d=n&&q.value.has(n)?s.y-v:s.y,f=n?Re(n):{dx:0,dy:0};return`translate3d(${p+f.dx}px,${d+f.dy}px,0)`}function L(t){(typeof requestAnimationFrame=="function"?requestAnimationFrame:n=>setTimeout(()=>n(0),0))(()=>t())}function Ae(t){L(()=>L(t))}function ne(t){if(!Array.isArray(t)||t.length===0)return;const a=new Set(q.value);let n=!1;for(const s of t){const c=s?.id;c&&(a.has(c)||(a.add(c),n=!0))}n&&(q.value=a)}function ve(){const t=new Map;for(const a of ce.value){const s=r.value[a]?.id;if(!s)continue;const c=M.value[a];c&&t.set(s,{x:c.x,y:c.y})}return t}function he(t,a){if(!t.size)return;const n=new Map,s=[];for(const[v,p]of t.entries()){if(a?.has(v))continue;const d=J.value.get(v);if(d==null)continue;const f=M.value[d];if(!f)continue;const u=p.x-f.x,P=p.y-f.y;(u||P)&&(n.set(v,{dx:u,dy:P}),s.push(v))}if(!n.size)return;te.value=n;const c=new Set(R.value);for(const v of s)c.delete(v);R.value=c,L(()=>{R.value=new Set([...R.value,...s]),L(()=>{te.value=new Map})}),setTimeout(()=>{const v=new Set(R.value);for(const p of s)v.delete(p);R.value=v},U)}const Q=e.ref(!0),X=e.ref(!1),D=e.ref(""),_=e.ref([]),ge=e.ref([]),le=e.ref([]),T=e.ref(o.page),ae=e.ref([]);let oe=0;function $(t){for(const a of t)!a||typeof a!="object"||a.id&&a.originalIndex==null&&(a.originalIndex=oe,oe+=1)}const Y=new Map,se=[];function Z(t){return typeof t=="number"&&Number.isFinite(t)}function qe(t,a){if(!a.length)return t;const n=new Set;for(const p of t){const d=p?.id;d&&n.add(d)}const s=[];for(const p of a){const d=p?.id;d&&(n.has(d)||(s.push(p),n.add(d)))}if(!s.length)return t;const c=s.slice().sort((p,d)=>{const f=Z(p.originalIndex)?p.originalIndex:Number.POSITIVE_INFINITY,u=Z(d.originalIndex)?d.originalIndex:Number.POSITIVE_INFINITY;return f-u}),v=t.slice();for(const p of c){const d=p.originalIndex;if(!Z(d)){v.push(p);continue}let f=0,u=v.length;for(;f<u;){const P=f+u>>1,W=v[P]?.originalIndex;(Z(W)?W:Number.POSITIVE_INFINITY)<=d?f=P+1:u=P}v.splice(f,0,p)}return v}async function pe(t){if(!t.length)return;ne(t);const a=ve();r.value=qe(r.value,t),await e.nextTick(),he(a)}async function ye(t){const n=(Array.isArray(t)?t:[t]).map(xe).filter(Boolean);if(!n.length)return;const s=[];for(const c of n){const v=Y.get(c);v&&s.push(v)}if(s.length){await pe(s);for(const c of s)c?.id&&Y.delete(c.id)}}async function we(){const t=se.pop();if(!t?.length)return;const a=[];for(const n of t){const s=Y.get(n);s&&a.push(s)}if(a.length){await pe(a);for(const n of a)n?.id&&Y.delete(n.id)}}async function Le(t){return ye(t)}async function De(){return we()}const re=e.shallowRef({enabled:!1,isBackfillActive:!1,isRequestInFlight:!1,requestPage:null,progress:{collected:0,target:0},cooldownMsRemaining:0,cooldownMsTotal:2e3,pageSize:20,bufferSize:0,lastBatch:null,totals:{pagesFetched:0,itemsFetchedFromNetwork:0}}),ke=Qe({getContent:t=>o.getContent(t),markEnterFromLeft:ne,buffer:ae,stats:re,isEnabled:()=>o.mode==="backfill",getPageSize:()=>o.pageSize,getRequestDelayMs:()=>o.backfillRequestDelayMs}),ie=e.computed(()=>o.items!==void 0);e.watch(()=>o.items,t=>{ie.value&&(le.value=Array.isArray(t)?t:[])},{immediate:!0});const r=e.computed({get(){return ie.value?le.value:ge.value},set(t){ie.value?(le.value=t,i("update:items",t)):ge.value=t}});async function be(t){const a=await o.getContent(t);return $(a.items),ne(a.items),{items:a.items,nextPage:a.nextPage}}function xe(t){return t?typeof t=="string"?t:t?.id:null}async function Be(t){const n=(Array.isArray(t)?t:[t]).map(xe).filter(Boolean);if(!n.length)return;const s=new Set(n),c=[];for(const f of s){const u=J.value.get(f);if(u==null)continue;const P=r.value[u];P&&(Y.set(f,P),c.push(f))}c.length&&se.push(c);const v=ve(),p=j.value,d=[];for(const f of s){const u=J.value.get(f);if(u==null)continue;const P=r.value[u];if(!P)continue;const W=M.value[u]??{x:0,y:0},Te=A.value[u]??p;d.push({id:f,item:P,fromX:W.x,fromY:W.y,width:p,height:Te,leaving:!0})}if(d.length&&(V.value=[...V.value,...d]),r.value=r.value.filter(f=>{const u=f?.id;return!u||!s.has(u)}),await e.nextTick(),he(v,s),d.length){const f=new Set(d.map(u=>u.id));L(()=>{V.value=V.value.map(u=>f.has(u.id)?{...u,leaving:!1}:u),setTimeout(()=>{V.value=V.value.filter(u=>!f.has(u.id))},U)})}}async function Se(t){return Be(t)}y({remove:Be,restore:Le,undo:De,restoreRemoved:ye,undoLastRemoval:we,backfillStats:re});function Me(){const t=Ze({items:r.value,columnCount:ue.value,columnWidth:j.value,gapX:E.value,gapY:S.value,headerHeight:x.value,footerHeight:z.value,bucketPx:Ne});M.value=t.positions,A.value=t.heights,de.value=t.buckets,me.value=t.contentHeight,J.value=t.indexById}const Ye=e.computed(()=>Math.max(me.value,k.value)+mt),ce=e.computed(()=>et({itemCount:r.value.length,viewportHeight:k.value,scrollTop:F.value,overscanPx:o.overscanPx,bucketPx:Ne,buckets:de.value}));e.watch(ce,t=>{if(!t?.length)return;const a=[];for(const n of t){const s=r.value[n]?.id;s&&q.value.has(s)&&(ee.has(s)||(ee.add(s),a.push(s)))}a.length&&(L(()=>{const n=new Set(O.value);for(const s of a)n.add(s);O.value=n}),Ae(()=>{const n=new Set(q.value);for(const s of a)n.delete(s);q.value=n,setTimeout(()=>{const s=new Set(O.value);for(const c of a)s.delete(c),ee.delete(c);O.value=s},U)}))},{flush:"post"});async function Oe(){if(!(Q.value||X.value)&&!(o.mode!=="backfill"&&T.value==null)&&!(o.mode==="backfill"&&T.value==null&&ae.value.length===0))try{if(X.value=!0,D.value="",o.mode==="backfill"){const n=await ke.loadBackfillBatch(T.value);n.pages.length&&(_.value=[..._.value,...n.pages]),$(n.batchItems),r.value=[...r.value,...n.batchItems],T.value=n.nextPage;return}const t=T.value;if(t==null)return;const a=await be(t);_.value=[..._.value,t],$(a.items),r.value=[...r.value,...a.items],T.value=a.nextPage}catch(t){D.value=t instanceof Error?t.message:String(t)}finally{X.value=!1}}function Xe(){const t=m.value;if(!t)return;F.value=t.scrollTop,k.value=t.clientHeight,t.scrollHeight-(t.scrollTop+t.clientHeight)<=o.prefetchThresholdPx&&Oe()}function Ie(){return m.value}function Ee(t){g.value=N(t),k.value=t.clientHeight}function $e(){typeof ResizeObserver>"u"||(I=new ResizeObserver(()=>{const t=Ie();t&&Ee(t)}))}function je(){return{enabled:o.mode==="backfill",isBackfillActive:!1,isRequestInFlight:!1,requestPage:null,progress:{collected:0,target:0},cooldownMsRemaining:0,cooldownMsTotal:fe(o.backfillRequestDelayMs),pageSize:He(o.pageSize),bufferSize:0,lastBatch:null,totals:{pagesFetched:0,itemsFetchedFromNetwork:0}}}function Pe(t){oe=0,Y.clear(),se.length=0,_.value=[],r.value=[],T.value=t,ae.value=[],re.value=je(),Q.value=!0,X.value=!1,D.value=""}async function Fe(t){try{if(o.mode==="backfill"){const a=await ke.loadBackfillBatch(t);_.value=a.pages.length?a.pages:[t],$(a.batchItems),r.value=a.batchItems,T.value=a.nextPage}else{const a=await be(t);_.value=[t],$(a.items),r.value=a.items,T.value=a.nextPage}}catch(a){D.value=a instanceof Error?a.message:String(a)}finally{Q.value=!1}}function We(){const t=Ie();t&&(Ee(t),F.value=t.scrollTop,I?.observe(t))}e.onMounted(async()=>{$e(),Pe(o.page),await Fe(o.page),await e.nextTick(),We()}),e.onUnmounted(()=>{I?.disconnect()}),e.watch(()=>o.page,async t=>{Pe(t),await Fe(t)}),e.watch(E,()=>{const t=m.value;t&&(g.value=N(t))},{immediate:!1});const ue=e.computed(()=>Ge(g.value,o.itemWidth)),j=e.computed(()=>Ke(g.value,ue.value,o.itemWidth,E.value));e.watch([ue,j,E,S,x,z],()=>{Me()},{immediate:!0}),e.watch(()=>[r.value,r.value.length],()=>Me(),{immediate:!0});const Ue=e.computed(()=>["mt-8 flex min-h-0 flex-1 flex-col rounded-2xl border border-slate-200/70 bg-white/70 p-5 shadow-sm backdrop-blur",b.class]);return(t,a)=>(e.openBlock(),e.createElementBlock("section",e.mergeProps(h.value,{class:Ue.value}),[e.createElementVNode("div",{ref_key:"scrollViewportRef",ref:m,"data-testid":"items-scroll-container",class:"mt-4 min-h-0 flex-1 overflow-auto",style:e.normalizeStyle({paddingRight:E.value+"px"}),onScroll:Xe},[Q.value?(e.openBlock(),e.createElementBlock("div",tt,a[0]||(a[0]=[e.createElementVNode("div",{class:"inline-flex items-center gap-3 text-sm text-slate-600"},[e.createElementVNode("svg",{class:"h-5 w-5 animate-spin text-slate-500",viewBox:"0 0 24 24","aria-hidden":"true"},[e.createElementVNode("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",fill:"none",stroke:"currentColor","stroke-width":"4"}),e.createElementVNode("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"})]),e.createElementVNode("span",null,"Loading…")],-1)]))):D.value?(e.openBlock(),e.createElementBlock("p",nt,"Error: "+e.toDisplayString(D.value),1)):(e.openBlock(),e.createElementBlock("div",{key:2,class:"relative",style:e.normalizeStyle({height:Ye.value+"px"})},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(ce.value,n=>(e.openBlock(),e.createElementBlock("article",{key:r.value[n].id,"data-testid":"item-card",class:"absolute overflow-hidden rounded-xl border border-slate-200/60 bg-white shadow-sm",style:e.normalizeStyle({width:j.value+"px",transition:Ve(r.value[n].id),transform:_e(n)})},[H.value||x.value>0?(e.openBlock(),e.createElementBlock("div",{key:0,"data-testid":"item-header-container",class:"w-full",style:e.normalizeStyle(G.value)},[e.renderSlot(t.$slots,"itemHeader",{item:r.value[n],remove:()=>Se(r.value[n])})],4)):e.createCommentVNode("",!0),e.createElementVNode("div",{class:"bg-slate-100",style:e.normalizeStyle({aspectRatio:r.value[n].width+" / "+r.value[n].height})},[r.value[n].type==="image"?(e.openBlock(),e.createElementBlock("img",{key:0,class:"h-full w-full object-cover",src:r.value[n].preview,width:r.value[n].width,height:r.value[n].height,loading:"lazy",alt:r.value[n].id},null,8,lt)):(e.openBlock(),e.createElementBlock("video",{key:1,class:"h-full w-full object-cover",poster:r.value[n].preview,controls:"",preload:"metadata"},[e.createElementVNode("source",{src:r.value[n].original,type:"video/mp4"},null,8,ot)],8,at))],4),C.value||z.value>0?(e.openBlock(),e.createElementBlock("div",{key:1,"data-testid":"item-footer-container",class:"w-full",style:e.normalizeStyle(K.value)},[e.renderSlot(t.$slots,"itemFooter",{item:r.value[n],remove:()=>Se(r.value[n])})],4)):e.createCommentVNode("",!0)],4))),128)),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(V.value,n=>(e.openBlock(),e.createElementBlock("article",{key:n.id+":leaving","data-testid":"item-card-leaving",class:"pointer-events-none absolute overflow-hidden rounded-xl border border-slate-200/60 bg-white shadow-sm",style:e.normalizeStyle({width:n.width+"px",transition:"transform "+U+"ms ease-out",transform:n.leaving?"translate3d("+n.fromX+"px,"+n.fromY+"px,0)":"translate3d("+n.fromX+"px,"+(n.fromY-n.height)+"px,0)"})},[H.value||x.value>0?(e.openBlock(),e.createElementBlock("div",{key:0,"data-testid":"item-header-container",class:"w-full",style:e.normalizeStyle(G.value)},[e.renderSlot(t.$slots,"itemHeader",{item:n.item,remove:()=>{}})],4)):e.createCommentVNode("",!0),e.createElementVNode("div",{class:"bg-slate-100",style:e.normalizeStyle({aspectRatio:n.item.width+" / "+n.item.height})},[n.item.type==="image"?(e.openBlock(),e.createElementBlock("img",{key:0,class:"h-full w-full object-cover",src:n.item.preview,width:n.item.width,height:n.item.height,loading:"lazy",alt:n.item.id},null,8,st)):(e.openBlock(),e.createElementBlock("video",{key:1,class:"h-full w-full object-cover",poster:n.item.preview,controls:"",preload:"metadata"},[e.createElementVNode("source",{src:n.item.original,type:"video/mp4"},null,8,it)],8,rt))],4),C.value||z.value>0?(e.openBlock(),e.createElementBlock("div",{key:1,"data-testid":"item-footer-container",class:"w-full",style:e.normalizeStyle(K.value)},[e.renderSlot(t.$slots,"itemFooter",{item:n.item,remove:()=>{}})],4)):e.createCommentVNode("",!0)],4))),128))],4)),e.createElementVNode("div",ct,[X.value?(e.openBlock(),e.createElementBlock("span",ut,a[1]||(a[1]=[e.createElementVNode("svg",{class:"h-4 w-4 animate-spin text-slate-500",viewBox:"0 0 24 24","aria-hidden":"true"},[e.createElementVNode("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",fill:"none",stroke:"currentColor","stroke-width":"4"}),e.createElementVNode("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"})],-1),e.createElementVNode("span",null,"Loading more…",-1)]))):T.value==null?(e.openBlock(),e.createElementBlock("span",ft,"End of list")):(e.openBlock(),e.createElementBlock("span",dt,"Scroll to load page "+e.toDisplayString(T.value),1))])],36)],16))}}),vt={install(l){l.component("Masonry",Ce)}};exports.Masonry=Ce;exports.VibePlugin=vt;exports.masonryDefaults=ze;