@wyxos/vibe 1.3.1 → 1.4.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/package.json CHANGED
@@ -1,20 +1,32 @@
1
1
  {
2
2
  "name": "@wyxos/vibe",
3
- "version": "1.3.1",
4
- "main": "index.js",
5
- "module": "index.js",
3
+ "version": "1.4.0",
4
+ "main": "lib/index.js",
5
+ "module": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./lib/index.js",
10
+ "types": "./lib/index.d.ts"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
6
14
  "type": "module",
7
15
  "files": [
8
- "src",
9
- "index.js"
16
+ "lib",
17
+ "src"
10
18
  ],
11
19
  "scripts": {
12
20
  "dev": "vite",
13
- "build": "vite build && node write-cname.js",
21
+ "build": "vue-tsc --noEmit && vite build && node write-cname.js",
22
+ "build:demo": "vue-tsc --noEmit && vite build && node write-cname.js",
23
+ "build:lib": "vite build --config vite.lib.config.ts && vue-tsc --declaration --emitDeclarationOnly --outDir lib",
14
24
  "preview": "vite preview",
15
25
  "watch": "vite build --watch",
16
26
  "release": "node release.mjs",
17
- "test": "vitest"
27
+ "test": "vitest",
28
+ "type-check": "vue-tsc --noEmit",
29
+ "prepublishOnly": "npm run build:lib"
18
30
  },
19
31
  "dependencies": {
20
32
  "lodash": "^4.17.21",
@@ -28,13 +40,17 @@
28
40
  "@tailwindcss/vite": "^4.0.15",
29
41
  "@vitejs/plugin-vue": "^5.2.1",
30
42
  "@vue/test-utils": "^2.4.6",
43
+ "@types/node": "^24.5.2",
31
44
  "chalk": "^5.3.0",
32
45
  "inquirer": "^10.1.8",
33
46
  "jsdom": "^26.0.0",
34
47
  "simple-git": "^3.27.0",
35
48
  "tailwindcss": "^4.0.15",
49
+ "typescript": "^5.9.2",
36
50
  "uuid": "^11.1.0",
37
51
  "vite": "^6.2.0",
38
- "vitest": "^3.0.9"
52
+ "vite-plugin-vue-mcp": "^0.3.2",
53
+ "vitest": "^3.0.9",
54
+ "vue-tsc": "^3.1.0"
39
55
  }
40
56
  }
package/src/App.vue CHANGED
@@ -1,17 +1,18 @@
1
- <script setup>
1
+ <script setup lang="ts">
2
2
  import Masonry from "./Masonry.vue";
3
- import {ref} from "vue";
3
+ import { ref } from "vue";
4
4
  import fixture from "./pages.json";
5
+ import type { MasonryItem, GetPageResult } from "./types";
5
6
 
6
- const items = ref([])
7
+ const items = ref<MasonryItem[]>([]);
7
8
 
8
- const masonry = ref(null)
9
+ const masonry = ref<InstanceType<typeof Masonry> | null>(null);
9
10
 
10
- const getPage = async (page) => {
11
+ const getPage = async (page: number): Promise<GetPageResult> => {
11
12
  return new Promise((resolve) => {
12
13
  setTimeout(() => {
13
14
  // Check if the page exists in the fixture
14
- const pageData = fixture[page - 1];
15
+ const pageData = (fixture as any[])[page - 1] as { items: MasonryItem[] } | undefined;
15
16
 
16
17
  if (!pageData) {
17
18
  // Return empty items if page doesn't exist
@@ -22,15 +23,15 @@ const getPage = async (page) => {
22
23
  return;
23
24
  }
24
25
 
25
- let output = {
26
+ const output: GetPageResult = {
26
27
  items: pageData.items,
27
- nextPage: page < fixture.length ? page + 1 : null
28
+ nextPage: page < (fixture as any[]).length ? page + 1 : null
28
29
  };
29
30
 
30
- resolve(output)
31
- }, 1000)
32
- })
33
- }
31
+ resolve(output);
32
+ }, 1000);
33
+ });
34
+ };
34
35
  </script>
35
36
  <template>
36
37
  <main class="flex flex-col items-center p-4 bg-slate-100 h-screen overflow-hidden">
@@ -49,16 +50,12 @@ const getPage = async (page) => {
49
50
  </div>
50
51
  </header>
51
52
  <masonry class="bg-blue-500 " v-model:items="items" :get-next-page="getPage" :load-at-page="1" ref="masonry">
52
- <template #item="{item, onRemove}">
53
+ <template #item="{item, remove}">
53
54
  <img :src="item.src" class="w-full"/>
54
- <button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="onRemove(item)">
55
+ <button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer" @click="remove(item)">
55
56
  <i class="fas fa-trash"></i>
56
57
  </button>
57
58
  </template>
58
59
  </masonry>
59
60
  </main>
60
61
  </template>
61
-
62
-
63
-
64
-
package/src/Masonry.vue CHANGED
@@ -1,21 +1,20 @@
1
- <script setup>
2
- import {computed, nextTick, onMounted, onUnmounted, ref} from "vue";
3
- import calculateLayout from "./calculateLayout.js";
1
+ <script setup lang="ts">
2
+ import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
3
+ import calculateLayout from "./calculateLayout";
4
4
  import { debounce } from 'lodash-es'
5
5
  import {
6
6
  getColumnCount,
7
7
  calculateContainerHeight,
8
8
  getItemAttributes,
9
9
  calculateColumnHeights
10
- } from './masonryUtils.js'
11
- import { useMasonryTransitions } from './useMasonryTransitions.js'
12
- import { useMasonryScroll } from './useMasonryScroll.js'
10
+ } from './masonryUtils'
11
+ import { useMasonryTransitions } from './useMasonryTransitions'
12
+ import { useMasonryScroll } from './useMasonryScroll'
13
13
 
14
14
  const props = defineProps({
15
15
  getNextPage: {
16
16
  type: Function,
17
- default: () => {
18
- }
17
+ default: () => {}
19
18
  },
20
19
  loadAtPage: {
21
20
  type: [Number, String],
@@ -31,7 +30,7 @@ const props = defineProps({
31
30
  paginationType: {
32
31
  type: String,
33
32
  default: 'page', // or 'cursor'
34
- validator: v => ['page', 'cursor'].includes(v)
33
+ validator: (v: string) => ['page', 'cursor'].includes(v)
35
34
  },
36
35
  skipInitialLoad: {
37
36
  type: Boolean,
@@ -45,10 +44,41 @@ const props = defineProps({
45
44
  type: Number,
46
45
  default: 40
47
46
  },
47
+ // Backfill configuration
48
+ backfillEnabled: {
49
+ type: Boolean,
50
+ default: true
51
+ },
52
+ backfillDelayMs: {
53
+ type: Number,
54
+ default: 2000
55
+ },
56
+ backfillMaxCalls: {
57
+ type: Number,
58
+ default: 10
59
+ },
60
+ // Retry configuration
61
+ retryMaxAttempts: {
62
+ type: Number,
63
+ default: 3
64
+ },
65
+ retryInitialDelayMs: {
66
+ type: Number,
67
+ default: 2000
68
+ },
69
+ retryBackoffStepMs: {
70
+ type: Number,
71
+ default: 2000
72
+ },
48
73
  transitionDurationMs: {
49
74
  type: Number,
50
75
  default: 450
51
76
  },
77
+ // Shorter, snappier duration specifically for item removal (leave)
78
+ leaveDurationMs: {
79
+ type: Number,
80
+ default: 160
81
+ },
52
82
  transitionEasing: {
53
83
  type: String,
54
84
  default: 'cubic-bezier(.22,.61,.36,1)'
@@ -56,13 +86,14 @@ const props = defineProps({
56
86
  })
57
87
 
58
88
  const defaultLayout = {
59
- sizes: { base: 1, sm: 2, md: 3, lg: 4, xl: 5, '2xl': 6 },
89
+ sizes: {base: 1, sm: 2, md: 3, lg: 4, xl: 5, '2xl': 6},
60
90
  gutterX: 10,
61
91
  gutterY: 10,
62
92
  header: 0,
63
93
  footer: 0,
64
94
  paddingLeft: 0,
65
- paddingRight: 0
95
+ paddingRight: 0,
96
+ placement: 'masonry'
66
97
  }
67
98
 
68
99
  const layout = computed(() => ({
@@ -74,27 +105,30 @@ const layout = computed(() => ({
74
105
  }
75
106
  }))
76
107
 
77
- const emits = defineEmits(['update:items'])
78
-
79
- const masonry = computed({
108
+ const emits = defineEmits([
109
+ 'update:items',
110
+ 'backfill:start',
111
+ 'backfill:tick',
112
+ 'backfill:stop',
113
+ 'retry:start',
114
+ 'retry:tick',
115
+ 'retry:stop'
116
+ ])
117
+
118
+ const masonry = computed<any>({
80
119
  get: () => props.items,
81
120
  set: (val) => emits('update:items', val)
82
121
  })
83
122
 
84
- const columns = ref(7)
85
-
86
- const container = ref(null)
87
-
88
- const paginationHistory = ref([])
89
-
90
- const nextPage = ref(null)
91
-
92
- const isLoading = ref(false)
93
-
94
- const containerHeight = ref(0)
123
+ const columns = ref<number>(7)
124
+ const container = ref<HTMLElement | null>(null)
125
+ const paginationHistory = ref<any[]>([])
126
+ const nextPage = ref<number | null>(null)
127
+ const isLoading = ref<boolean>(false)
128
+ const containerHeight = ref<number>(0)
95
129
 
96
130
  // Scroll progress tracking
97
- const scrollProgress = ref({
131
+ const scrollProgress = ref<{ distanceToTrigger: number; isNearTrigger: boolean }>({
98
132
  distanceToTrigger: 0,
99
133
  isNearTrigger: false
100
134
  })
@@ -102,13 +136,12 @@ const scrollProgress = ref({
102
136
  const updateScrollProgress = () => {
103
137
  if (!container.value) return
104
138
 
105
- const { scrollTop, clientHeight } = container.value
139
+ const {scrollTop, clientHeight} = container.value
106
140
  const visibleBottom = scrollTop + clientHeight
107
141
 
108
- const columnHeights = calculateColumnHeights(masonry.value, columns.value)
109
- // Use longest column to match the trigger logic in useMasonryScroll.js
142
+ const columnHeights = calculateColumnHeights(masonry.value as any, columns.value)
110
143
  const longestColumn = Math.max(...columnHeights)
111
- const triggerPoint = longestColumn + 300 // Match: longestColumn + 300 < visibleBottom
144
+ const triggerPoint = longestColumn + 300
112
145
 
113
146
  const distanceToTrigger = Math.max(0, triggerPoint - visibleBottom)
114
147
  const isNearTrigger = distanceToTrigger <= 100
@@ -120,57 +153,75 @@ const updateScrollProgress = () => {
120
153
  }
121
154
 
122
155
  // Setup composables
123
- const { onEnter, onBeforeEnter, onBeforeLeave, onLeave } = useMasonryTransitions(masonry)
156
+ const {onEnter, onBeforeEnter, onBeforeLeave, onLeave} = useMasonryTransitions(masonry)
124
157
 
125
- const { handleScroll } = useMasonryScroll({
158
+ const {handleScroll} = useMasonryScroll({
126
159
  container,
127
- masonry,
160
+ masonry: masonry as any,
128
161
  columns,
129
162
  containerHeight,
130
163
  isLoading,
131
164
  maxItems: props.maxItems,
132
165
  pageSize: props.pageSize,
133
166
  refreshLayout,
134
- // Allow scroll composable to set items without recalculating layout (phase-1 cleanup)
135
- setItemsRaw: (items) => { masonry.value = items },
136
- loadNext
167
+ setItemsRaw: (items: any[]) => {
168
+ masonry.value = items
169
+ },
170
+ loadNext,
171
+ leaveEstimateMs: props.leaveDurationMs
137
172
  })
138
173
 
139
174
  defineExpose({
140
175
  isLoading,
141
176
  refreshLayout,
142
177
  containerHeight,
143
- onRemove,
178
+ remove,
144
179
  removeMany,
145
180
  loadNext,
146
181
  loadPage,
147
182
  reset,
183
+ init,
148
184
  paginationHistory
149
185
  })
150
186
 
151
- function calculateHeight(content) {
152
- const newHeight = calculateContainerHeight(content)
187
+ function calculateHeight(content: any[]) {
188
+ const newHeight = calculateContainerHeight(content as any)
153
189
  let floor = 0
154
190
  if (container.value) {
155
- const { scrollTop, clientHeight } = container.value
156
- // Ensure the container never shrinks below the visible viewport bottom + small buffer
191
+ const {scrollTop, clientHeight} = container.value
157
192
  floor = scrollTop + clientHeight + 100
158
193
  }
159
194
  containerHeight.value = Math.max(newHeight, floor)
160
195
  }
161
196
 
162
- function refreshLayout(items) {
163
- const content = calculateLayout(items, container.value, columns.value, layout.value);
164
-
165
- calculateHeight(content)
166
-
197
+ function refreshLayout(items: any[]) {
198
+ if (!container.value) return
199
+ const content = calculateLayout(items as any, container.value as HTMLElement, columns.value, layout.value as any)
200
+ calculateHeight(content as any)
167
201
  masonry.value = content
168
202
  }
169
203
 
170
- async function getContent(page) {
204
+ function waitWithProgress(totalMs: number, onTick: (remaining: number, total: number) => void) {
205
+ return new Promise<void>((resolve) => {
206
+ const total = Math.max(0, totalMs | 0)
207
+ const start = Date.now()
208
+ onTick(total, total)
209
+ const id = setInterval(() => {
210
+ const elapsed = Date.now() - start
211
+ const remaining = Math.max(0, total - elapsed)
212
+ onTick(remaining, total)
213
+ if (remaining <= 0) {
214
+ clearInterval(id)
215
+ resolve()
216
+ }
217
+ }, 100)
218
+ })
219
+ }
220
+
221
+ async function getContent(page: number) {
171
222
  try {
172
- const response = await props.getNextPage(page)
173
- refreshLayout([...masonry.value, ...response.items])
223
+ const response = await fetchWithRetry(() => props.getNextPage(page))
224
+ refreshLayout([...(masonry.value as any[]), ...response.items])
174
225
  return response
175
226
  } catch (error) {
176
227
  console.error('Error in getContent:', error)
@@ -178,14 +229,41 @@ async function getContent(page) {
178
229
  }
179
230
  }
180
231
 
181
- async function loadPage(page) {
182
- if (isLoading.value) return // Prevent concurrent loading
232
+ async function fetchWithRetry<T = any>(fn: () => Promise<T>): Promise<T> {
233
+ let attempt = 0
234
+ const max = props.retryMaxAttempts
235
+ let delay = props.retryInitialDelayMs
236
+ // eslint-disable-next-line no-constant-condition
237
+ while (true) {
238
+ try {
239
+ const res = await fn()
240
+ if (attempt > 0) {
241
+ emits('retry:stop', {attempt, success: true})
242
+ }
243
+ return res
244
+ } catch (err) {
245
+ attempt++
246
+ if (attempt > max) {
247
+ emits('retry:stop', {attempt: attempt - 1, success: false})
248
+ throw err
249
+ }
250
+ emits('retry:start', {attempt, max, totalMs: delay})
251
+ await waitWithProgress(delay, (remaining, total) => {
252
+ emits('retry:tick', {attempt, remainingMs: remaining, totalMs: total})
253
+ })
254
+ delay += props.retryBackoffStepMs
255
+ }
256
+ }
257
+ }
183
258
 
259
+ async function loadPage(page: number) {
260
+ if (isLoading.value) return
184
261
  isLoading.value = true
185
-
186
262
  try {
263
+ const baseline = (masonry.value as any[]).length
187
264
  const response = await getContent(page)
188
265
  paginationHistory.value.push(response.nextPage)
266
+ await maybeBackfillToTarget(baseline)
189
267
  return response
190
268
  } catch (error) {
191
269
  console.error('Error loading page:', error)
@@ -196,14 +274,14 @@ async function loadPage(page) {
196
274
  }
197
275
 
198
276
  async function loadNext() {
199
- if (isLoading.value) return // Prevent concurrent loading
200
-
277
+ if (isLoading.value) return
201
278
  isLoading.value = true
202
-
203
279
  try {
280
+ const baseline = (masonry.value as any[]).length
204
281
  const currentPage = paginationHistory.value[paginationHistory.value.length - 1]
205
282
  const response = await getContent(currentPage)
206
283
  paginationHistory.value.push(response.nextPage)
284
+ await maybeBackfillToTarget(baseline)
207
285
  return response
208
286
  } catch (error) {
209
287
  console.error('Error loading next page:', error)
@@ -213,24 +291,88 @@ async function loadNext() {
213
291
  }
214
292
  }
215
293
 
216
- function onRemove(item) {
217
- refreshLayout(masonry.value.filter(i => i.id !== item.id))
294
+ async function remove(item: any) {
295
+ const next = (masonry.value as any[]).filter(i => i.id !== item.id)
296
+ masonry.value = next
297
+ await nextTick()
298
+ requestAnimationFrame(() => {
299
+ requestAnimationFrame(() => {
300
+ refreshLayout(next)
301
+ })
302
+ })
218
303
  }
219
304
 
220
- function removeMany(items) {
305
+ async function removeMany(items: any[]) {
221
306
  if (!items || items.length === 0) return
222
307
  const ids = new Set(items.map(i => i.id))
223
- const next = masonry.value.filter(i => !ids.has(i.id))
224
- refreshLayout(next)
308
+ const next = (masonry.value as any[]).filter(i => !ids.has(i.id))
309
+ masonry.value = next
310
+ await nextTick()
311
+ requestAnimationFrame(() => {
312
+ requestAnimationFrame(() => {
313
+ refreshLayout(next)
314
+ })
315
+ })
225
316
  }
226
317
 
227
318
  function onResize() {
228
- columns.value = getColumnCount(layout.value)
229
- refreshLayout(masonry.value)
319
+ columns.value = getColumnCount(layout.value as any)
320
+ refreshLayout(masonry.value as any)
321
+ }
322
+
323
+ let backfillActive = false
324
+
325
+ async function maybeBackfillToTarget(baselineCount: number) {
326
+ if (!props.backfillEnabled) return
327
+ if (backfillActive) return
328
+
329
+ const targetCount = (baselineCount || 0) + (props.pageSize || 0)
330
+ if (!props.pageSize || props.pageSize <= 0) return
331
+
332
+ const lastNext = paginationHistory.value[paginationHistory.value.length - 1]
333
+ if (lastNext == null) return
334
+
335
+ if ((masonry.value as any[]).length >= targetCount) return
336
+
337
+ backfillActive = true
338
+ try {
339
+ let calls = 0
340
+ emits('backfill:start', {target: targetCount, fetched: (masonry.value as any[]).length, calls})
341
+
342
+ while (
343
+ (masonry.value as any[]).length < targetCount &&
344
+ calls < props.backfillMaxCalls &&
345
+ paginationHistory.value[paginationHistory.value.length - 1] != null
346
+ ) {
347
+ await waitWithProgress(props.backfillDelayMs, (remaining, total) => {
348
+ emits('backfill:tick', {
349
+ fetched: (masonry.value as any[]).length,
350
+ target: targetCount,
351
+ calls,
352
+ remainingMs: remaining,
353
+ totalMs: total
354
+ })
355
+ })
356
+
357
+ const currentPage = paginationHistory.value[paginationHistory.value.length - 1]
358
+ try {
359
+ isLoading.value = true
360
+ const response = await getContent(currentPage)
361
+ paginationHistory.value.push(response.nextPage)
362
+ } finally {
363
+ isLoading.value = false
364
+ }
365
+
366
+ calls++
367
+ }
368
+
369
+ emits('backfill:stop', {fetched: (masonry.value as any[]).length, calls})
370
+ } finally {
371
+ backfillActive = false
372
+ }
230
373
  }
231
374
 
232
375
  function reset() {
233
- // Scroll back to top first (while items still exist to scroll through)
234
376
  if (container.value) {
235
377
  container.value.scrollTo({
236
378
  top: 0,
@@ -238,23 +380,16 @@ function reset() {
238
380
  })
239
381
  }
240
382
 
241
- // Clear all items
242
383
  masonry.value = []
243
-
244
- // Reset container height
245
384
  containerHeight.value = 0
246
-
247
- // Reset pagination history to initial state
248
385
  paginationHistory.value = [props.loadAtPage]
249
386
 
250
- // Reset scroll progress
251
387
  scrollProgress.value = {
252
388
  distanceToTrigger: 0,
253
389
  isNearTrigger: false
254
390
  }
255
391
  }
256
392
 
257
- // Create debounced functions with stable references
258
393
  const debouncedScrollHandler = debounce(() => {
259
394
  handleScroll()
260
395
  updateScrollProgress()
@@ -262,28 +397,28 @@ const debouncedScrollHandler = debounce(() => {
262
397
 
263
398
  const debouncedResizeHandler = debounce(onResize, 200)
264
399
 
400
+ function init(items: any[], page: any, next: any) {
401
+ paginationHistory.value = [page]
402
+ paginationHistory.value.push(next)
403
+ refreshLayout([...(masonry.value as any[]), ...items])
404
+ updateScrollProgress()
405
+ }
406
+
265
407
  onMounted(async () => {
266
408
  try {
267
- columns.value = getColumnCount(layout.value)
409
+ columns.value = getColumnCount(layout.value as any)
268
410
 
269
- // For cursor-based pagination, loadAtPage can be null for the first request
270
- const initialPage = props.loadAtPage
411
+ const initialPage = props.loadAtPage as any
271
412
  paginationHistory.value = [initialPage]
272
413
 
273
- // Skip initial load if skipInitialLoad prop is true
274
414
  if (!props.skipInitialLoad) {
275
- await loadPage(paginationHistory.value[0]) // loadPage manages its own loading state
276
- } else {
277
- await nextTick()
278
- // Just refresh the layout with any existing items
279
- refreshLayout(masonry.value)
415
+ await loadPage(paginationHistory.value[0] as any)
280
416
  }
281
417
 
282
418
  updateScrollProgress()
283
419
 
284
420
  } catch (error) {
285
421
  console.error('Error during component initialization:', error)
286
- // Ensure loading state is reset if error occurs during initialization
287
422
  isLoading.value = false
288
423
  }
289
424
 
@@ -298,18 +433,19 @@ onUnmounted(() => {
298
433
  </script>
299
434
 
300
435
  <template>
301
- <div class="overflow-auto w-full flex-1 masonry-container" ref="container"
302
- > <div class="relative" :style="{height: `${containerHeight}px`, '--masonry-duration': `${transitionDurationMs}ms`, '--masonry-ease': transitionEasing}">
436
+ <div class="overflow-auto w-full flex-1 masonry-container" ref="container">
437
+ <div class="relative"
438
+ :style="{height: `${containerHeight}px`, '--masonry-duration': `${transitionDurationMs}ms`, '--masonry-leave-duration': `${leaveDurationMs}ms`, '--masonry-ease': transitionEasing}">
303
439
  <transition-group name="masonry" :css="false" @enter="onEnter" @before-enter="onBeforeEnter"
304
440
  @leave="onLeave"
305
441
  @before-leave="onBeforeLeave">
306
442
  <div v-for="(item, i) in masonry" :key="`${item.page}-${item.id}`"
307
443
  class="absolute masonry-item"
308
444
  v-bind="getItemAttributes(item, i)">
309
- <slot name="item" v-bind="{item, onRemove}">
445
+ <slot name="item" v-bind="{item, remove}">
310
446
  <img :src="item.src" class="w-full"/>
311
447
  <button class="absolute bottom-0 right-0 bg-red-500 text-white p-2 rounded cursor-pointer"
312
- @click="onRemove(item)">
448
+ @click="remove(item)">
313
449
  <i class="fas fa-trash"></i>
314
450
  </button>
315
451
  </slot>
@@ -329,21 +465,26 @@ onUnmounted(() => {
329
465
  </template>
330
466
 
331
467
  <style scoped>
332
- /* Prevent browser scroll anchoring from adjusting scroll on content changes */
333
468
  .masonry-container {
334
469
  overflow-anchor: none;
335
470
  }
336
471
 
337
- /* Items animate transform only for smooth, compositor-driven motion */
338
472
  .masonry-item {
339
473
  will-change: transform, opacity;
340
- transition: transform var(--masonry-duration, 450ms) var(--masonry-ease, cubic-bezier(.22,.61,.36,1)),
341
- opacity 200ms linear;
474
+ contain: layout paint;
475
+ transition: transform var(--masonry-duration, 450ms) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1)),
476
+ opacity var(--masonry-leave-duration, 160ms) ease-out var(--masonry-opacity-delay, 0ms);
342
477
  backface-visibility: hidden;
343
478
  }
344
479
 
345
- /* TransitionGroup move-class for FLIP reordering */
346
480
  .masonry-move {
347
- transition: transform var(--masonry-duration, 450ms) var(--masonry-ease, cubic-bezier(.22,.61,.36,1));
481
+ transition: transform var(--masonry-duration, 450ms) var(--masonry-ease, cubic-bezier(.22, .61, .36, 1));
482
+ }
483
+
484
+ @media (prefers-reduced-motion: reduce) {
485
+ .masonry-item,
486
+ .masonry-move {
487
+ transition-duration: 1ms !important;
488
+ }
348
489
  }
349
490
  </style>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { describe, it, expect } from 'vitest'
5
+
6
+ describe('Example Test', () => {
7
+ it('should pass a basic truthiness check', () => {
8
+ expect(true).toBe(true)
9
+ })
10
+ })