@wyxos/vibe 1.6.28 → 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.
- package/README.md +29 -287
- package/lib/index.cjs +1 -0
- package/lib/index.js +778 -1781
- package/lib/logo-dark.svg +36 -36
- package/lib/logo-light.svg +29 -29
- package/lib/logo.svg +32 -32
- package/lib/manifest.json +1 -1
- package/package.json +82 -96
- package/LICENSE +0 -21
- package/lib/vibe.css +0 -1
- package/lib/vite.svg +0 -1
- package/src/App.vue +0 -35
- package/src/Masonry.vue +0 -1020
- package/src/archive/App.vue +0 -96
- package/src/archive/InfiniteMansonry.spec.ts +0 -10
- package/src/archive/InfiniteMasonry.vue +0 -218
- package/src/assets/vue.svg +0 -1
- package/src/calculateLayout.ts +0 -194
- package/src/components/CodeTabs.vue +0 -158
- package/src/components/MasonryItem.vue +0 -499
- package/src/components/examples/BasicExample.vue +0 -46
- package/src/components/examples/CustomItemExample.vue +0 -87
- package/src/components/examples/HeaderFooterExample.vue +0 -79
- package/src/components/examples/ManualInitExample.vue +0 -78
- package/src/components/examples/SwipeModeExample.vue +0 -40
- package/src/createMasonryTransitions.ts +0 -176
- package/src/main.ts +0 -6
- package/src/masonryUtils.ts +0 -96
- package/src/pages.json +0 -36402
- package/src/router/index.ts +0 -20
- package/src/style.css +0 -32
- package/src/types.ts +0 -101
- package/src/useMasonryDimensions.ts +0 -59
- package/src/useMasonryItems.ts +0 -231
- package/src/useMasonryLayout.ts +0 -164
- package/src/useMasonryPagination.ts +0 -529
- package/src/useMasonryScroll.ts +0 -61
- package/src/useMasonryVirtualization.ts +0 -140
- package/src/useSwipeMode.ts +0 -233
- package/src/utils/errorHandler.ts +0 -8
- package/src/views/Examples.vue +0 -323
- package/src/views/Home.vue +0 -321
- package/toggle-link.mjs +0 -92
package/README.md
CHANGED
|
@@ -1,287 +1,29 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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;
|