@thangdevalone/meet-layout-grid-vue 1.3.2 → 1.3.4

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/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { ref, onMounted, computed, defineComponent, provide, h, inject } from 'vue';
1
+ import { ref, onMounted, computed, defineComponent, provide, h, inject, watch } from 'vue';
2
2
  import { useResizeObserver } from '@vueuse/core';
3
- import { createMeetGrid, getSpringConfig } from '@thangdevalone/meet-layout-grid-core';
4
- export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, springPresets } from '@thangdevalone/meet-layout-grid-core';
5
- import { motion } from 'motion-v';
3
+ import { createMeetGrid, getSpringConfig, resolveFloatSize } from '@thangdevalone/meet-layout-grid-core';
4
+ export { DEFAULT_FLOAT_BREAKPOINTS, createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, resolveFloatSize, springPresets } from '@thangdevalone/meet-layout-grid-core';
5
+ import { useMotionValue, animate, motion } from 'motion-v';
6
6
 
7
7
  function useGridDimensions(elementRef) {
8
8
  const width = ref(0);
@@ -123,6 +123,25 @@ const GridContainer = defineComponent({
123
123
  type: Array,
124
124
  default: void 0
125
125
  },
126
+ /** Custom width for the floating PiP item in 2-person mode */
127
+ floatWidth: {
128
+ type: Number,
129
+ default: void 0
130
+ },
131
+ /** Custom height for the floating PiP item in 2-person mode */
132
+ floatHeight: {
133
+ type: Number,
134
+ default: void 0
135
+ },
136
+ /**
137
+ * Responsive breakpoints for the floating PiP in 2-person mode.
138
+ * When provided, PiP size auto-adjusts based on container width.
139
+ * Use `DEFAULT_FLOAT_BREAKPOINTS` for a ready-made 5-level responsive config.
140
+ */
141
+ floatBreakpoints: {
142
+ type: Array,
143
+ default: void 0
144
+ },
126
145
  /** HTML tag to render */
127
146
  tag: {
128
147
  type: String,
@@ -144,7 +163,10 @@ const GridContainer = defineComponent({
144
163
  currentPage: props.currentPage,
145
164
  maxVisible: props.maxVisible,
146
165
  currentVisiblePage: props.currentVisiblePage,
147
- itemAspectRatios: props.itemAspectRatios
166
+ itemAspectRatios: props.itemAspectRatios,
167
+ floatWidth: props.floatWidth,
168
+ floatHeight: props.floatHeight,
169
+ floatBreakpoints: props.floatBreakpoints
148
170
  }));
149
171
  const grid = useMeetGrid(gridOptions);
150
172
  provide(GridContextKey, {
@@ -214,11 +236,11 @@ const GridItem = defineComponent({
214
236
  });
215
237
  const isFloat = computed(() => grid.value.floatIndex === props.index);
216
238
  const floatDims = computed(() => grid.value.floatDimensions ?? { width: 120, height: 160 });
217
- const floatAnchor = ref("bottom-right");
218
- const floatDragging = ref(false);
219
- const floatDragOffset = ref({ x: 0, y: 0 });
220
- const floatStartPos = ref({ x: 0, y: 0 });
221
- const floatDisplayPos = ref({ x: 0, y: 0 });
239
+ const floatAnchor = ref(
240
+ "bottom-right"
241
+ );
242
+ const x = useMotionValue(0);
243
+ const y = useMotionValue(0);
222
244
  const floatInitialized = ref(false);
223
245
  const getFloatCornerPos = (corner) => {
224
246
  const padding = 12;
@@ -237,11 +259,11 @@ const GridItem = defineComponent({
237
259
  return { x: dims.width - fw - padding, y: dims.height - fh - padding };
238
260
  }
239
261
  };
240
- const findFloatNearestCorner = (x, y) => {
262
+ const findFloatNearestCorner = (posX, posY) => {
241
263
  const fw = floatDims.value.width;
242
264
  const fh = floatDims.value.height;
243
- const centerX = x + fw / 2;
244
- const centerY = y + fh / 2;
265
+ const centerX = posX + fw / 2;
266
+ const centerY = posY + fh / 2;
245
267
  const dims = containerDimensions.value;
246
268
  const isLeft = centerX < dims.width / 2;
247
269
  const isTop = centerY < dims.height / 2;
@@ -253,46 +275,77 @@ const GridItem = defineComponent({
253
275
  return "bottom-left";
254
276
  return "bottom-right";
255
277
  };
256
- const floatCornerPos = computed(() => getFloatCornerPos(floatAnchor.value));
257
- const handleFloatDragStart = (e) => {
258
- floatDragging.value = true;
259
- const pos = "touches" in e ? e.touches[0] : e;
260
- floatStartPos.value = { x: pos.clientX, y: pos.clientY };
261
- floatDragOffset.value = { x: 0, y: 0 };
262
- };
263
- const handleFloatDragMove = (e) => {
264
- if (!floatDragging.value)
265
- return;
266
- e.preventDefault();
267
- const pos = "touches" in e ? e.touches[0] : e;
268
- floatDragOffset.value = {
269
- x: pos.clientX - floatStartPos.value.x,
270
- y: pos.clientY - floatStartPos.value.y
271
- };
272
- floatDisplayPos.value = {
273
- x: floatCornerPos.value.x + floatDragOffset.value.x,
274
- y: floatCornerPos.value.y + floatDragOffset.value.y
275
- };
276
- };
277
- const handleFloatDragEnd = () => {
278
- if (!floatDragging.value)
279
- return;
280
- floatDragging.value = false;
281
- const nearest = findFloatNearestCorner(floatDisplayPos.value.x, floatDisplayPos.value.y);
282
- floatAnchor.value = nearest;
283
- floatDisplayPos.value = getFloatCornerPos(nearest);
284
- floatDragOffset.value = { x: 0, y: 0 };
285
- };
278
+ watch(isFloat, (floating) => {
279
+ if (!floating) {
280
+ floatInitialized.value = false;
281
+ }
282
+ });
283
+ watch(
284
+ [isFloat, () => containerDimensions.value.width, () => containerDimensions.value.height],
285
+ ([floating, w, h2]) => {
286
+ if (floating && w > 0 && h2 > 0 && !floatInitialized.value) {
287
+ const pos = getFloatCornerPos(floatAnchor.value);
288
+ x.set(pos.x);
289
+ y.set(pos.y);
290
+ floatInitialized.value = true;
291
+ }
292
+ },
293
+ { immediate: true }
294
+ );
295
+ watch(
296
+ [floatAnchor, () => containerDimensions.value.width, () => containerDimensions.value.height],
297
+ ([, w, h2]) => {
298
+ if (isFloat.value && floatInitialized.value && w > 0 && h2 > 0) {
299
+ const pos = getFloatCornerPos(floatAnchor.value);
300
+ const springCfg = { type: "spring", stiffness: 400, damping: 30 };
301
+ animate(x, pos.x, springCfg);
302
+ animate(y, pos.y, springCfg);
303
+ }
304
+ }
305
+ );
286
306
  const isLastVisibleOther = computed(() => {
287
307
  const lastVisibleOthersIndex = grid.value.getLastVisibleOthersIndex();
288
308
  return props.index === lastVisibleOthersIndex;
289
309
  });
290
310
  const hiddenCount = computed(() => grid.value.hiddenCount);
291
311
  const springConfig = getSpringConfig(springPreset);
312
+ const gridX = useMotionValue(0);
313
+ const gridY = useMotionValue(0);
314
+ const gridAnimReady = ref(false);
315
+ watch(
316
+ [
317
+ () => position.value.top,
318
+ () => position.value.left,
319
+ isFloat,
320
+ isHidden
321
+ ],
322
+ ([, , floating, hidden]) => {
323
+ if (floating || hidden) {
324
+ gridAnimReady.value = false;
325
+ return;
326
+ }
327
+ const pos = position.value;
328
+ if (!gridAnimReady.value) {
329
+ gridX.set(pos.left);
330
+ gridY.set(pos.top);
331
+ gridAnimReady.value = true;
332
+ } else {
333
+ const cfg = {
334
+ type: "spring",
335
+ stiffness: springConfig.stiffness,
336
+ damping: springConfig.damping
337
+ };
338
+ animate(gridX, pos.left, cfg);
339
+ animate(gridY, pos.top, cfg);
340
+ }
341
+ },
342
+ { immediate: true }
343
+ );
292
344
  const slotProps = computed(() => ({
293
345
  contentDimensions: contentDimensions.value,
294
346
  isLastVisibleOther: isLastVisibleOther.value,
295
- hiddenCount: hiddenCount.value
347
+ hiddenCount: hiddenCount.value,
348
+ isFloat: isFloat.value
296
349
  }));
297
350
  return () => {
298
351
  if (isHidden.value) {
@@ -302,88 +355,90 @@ const GridItem = defineComponent({
302
355
  const dims = containerDimensions.value;
303
356
  if (dims.width === 0 || dims.height === 0)
304
357
  return null;
305
- if (!floatInitialized.value) {
306
- floatDisplayPos.value = floatCornerPos.value;
307
- floatInitialized.value = true;
308
- }
358
+ const dragConstraints = {
359
+ left: 12,
360
+ right: dims.width - floatDims.value.width - 12,
361
+ top: 12,
362
+ bottom: dims.height - floatDims.value.height - 12
363
+ };
364
+ const handleDragEnd = () => {
365
+ const currentX = x.get();
366
+ const currentY = y.get();
367
+ const nearestCorner = findFloatNearestCorner(currentX, currentY);
368
+ floatAnchor.value = nearestCorner;
369
+ const snapPos = getFloatCornerPos(nearestCorner);
370
+ const springCfg = { type: "spring", stiffness: 400, damping: 30 };
371
+ animate(x, snapPos.x, springCfg);
372
+ animate(y, snapPos.y, springCfg);
373
+ };
309
374
  return h(
310
375
  motion.div,
311
376
  {
312
- animate: {
313
- x: floatDisplayPos.value.x,
314
- y: floatDisplayPos.value.y,
315
- opacity: 1,
316
- scale: floatDragging.value ? 1.05 : 1
317
- },
318
- transition: floatDragging.value ? { duration: 0 } : { type: "spring", stiffness: 400, damping: 30 },
377
+ // Key forces Vue to recreate this element when switching float↔grid
378
+ key: `float-${props.index}`,
379
+ drag: true,
380
+ dragMomentum: false,
381
+ dragElastic: 0.1,
382
+ dragConstraints,
319
383
  style: {
320
384
  position: "absolute",
321
385
  width: `${floatDims.value.width}px`,
322
386
  height: `${floatDims.value.height}px`,
323
387
  borderRadius: "12px",
324
- boxShadow: floatDragging.value ? "0 8px 32px rgba(0,0,0,0.4)" : "0 4px 20px rgba(0,0,0,0.3)",
388
+ boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
325
389
  overflow: "hidden",
326
- cursor: floatDragging.value ? "grabbing" : "grab",
390
+ cursor: "grab",
327
391
  zIndex: 100,
328
392
  touchAction: "none",
329
393
  left: 0,
330
- top: 0
394
+ top: 0,
395
+ x,
396
+ y
331
397
  },
398
+ whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
399
+ transition: { type: "spring", stiffness: 400, damping: 30 },
332
400
  "data-grid-index": props.index,
333
401
  "data-grid-float": true,
334
- onMousedown: handleFloatDragStart,
335
- onMousemove: handleFloatDragMove,
336
- onMouseup: handleFloatDragEnd,
337
- onMouseleave: handleFloatDragEnd,
338
- onTouchstart: handleFloatDragStart,
339
- onTouchmove: handleFloatDragMove,
340
- onTouchend: handleFloatDragEnd
402
+ onDragEnd: handleDragEnd
341
403
  },
342
404
  () => slots.default?.(slotProps.value)
343
405
  );
344
406
  }
345
- const animateProps = {
346
- width: dimensions.value.width,
347
- height: dimensions.value.height,
348
- top: position.value.top,
349
- left: position.value.left
350
- };
407
+ const itemWidth = dimensions.value.width;
408
+ const itemHeight = dimensions.value.height;
351
409
  if (props.disableAnimation) {
352
410
  return h(
353
411
  props.tag,
354
412
  {
355
413
  style: {
356
414
  position: "absolute",
357
- ...animateProps,
358
- width: `${animateProps.width}px`,
359
- height: `${animateProps.height}px`,
360
- top: `${animateProps.top}px`,
361
- left: `${animateProps.left}px`
415
+ width: `${itemWidth}px`,
416
+ height: `${itemHeight}px`,
417
+ top: `${position.value.top}px`,
418
+ left: `${position.value.left}px`
362
419
  },
363
420
  "data-grid-index": props.index,
364
421
  "data-grid-main": isMain.value
365
422
  },
366
- // Pass all slot props
367
423
  slots.default?.(slotProps.value)
368
424
  );
369
425
  }
370
426
  return h(
371
427
  motion.div,
372
428
  {
373
- as: props.tag,
374
- animate: animateProps,
375
- transition: {
376
- type: springConfig.type,
377
- stiffness: springConfig.stiffness,
378
- damping: springConfig.damping
379
- },
429
+ key: `grid-${props.index}`,
380
430
  style: {
381
- position: "absolute"
431
+ position: "absolute",
432
+ top: 0,
433
+ left: 0,
434
+ x: gridX,
435
+ y: gridY,
436
+ width: `${itemWidth}px`,
437
+ height: `${itemHeight}px`
382
438
  },
383
439
  "data-grid-index": props.index,
384
440
  "data-grid-main": isMain.value
385
441
  },
386
- // Pass all slot props
387
442
  () => slots.default?.(slotProps.value)
388
443
  );
389
444
  };
@@ -428,16 +483,26 @@ const GridOverlay = defineComponent({
428
483
  const FloatingGridItem = defineComponent({
429
484
  name: "FloatingGridItem",
430
485
  props: {
431
- /** Width of the floating item */
486
+ /** Width of the floating item (px). Overridden by `breakpoints` when provided. */
432
487
  width: {
433
488
  type: Number,
434
489
  default: 120
435
490
  },
436
- /** Height of the floating item */
491
+ /** Height of the floating item (px). Overridden by `breakpoints` when provided. */
437
492
  height: {
438
493
  type: Number,
439
494
  default: 160
440
495
  },
496
+ /**
497
+ * Responsive breakpoints for PiP sizing.
498
+ * When provided, width/height auto-adjust based on container width.
499
+ * Overrides the fixed `width`/`height` props.
500
+ * Use `DEFAULT_FLOAT_BREAKPOINTS` for a ready-made 5-level responsive config.
501
+ */
502
+ breakpoints: {
503
+ type: Array,
504
+ default: void 0
505
+ },
441
506
  /** Initial position (x, y from container edges) */
442
507
  initialPosition: {
443
508
  type: Object,
@@ -478,10 +543,14 @@ const FloatingGridItem = defineComponent({
478
543
  }
479
544
  const { dimensions } = context;
480
545
  const currentAnchor = ref(props.anchor);
481
- const isDragging = ref(false);
482
- const dragOffset = ref({ x: 0, y: 0 });
483
- const startPos = ref({ x: 0, y: 0 });
484
- const displayPosition = ref({ x: 0, y: 0 });
546
+ const effectiveSize = computed(() => {
547
+ if (props.breakpoints && props.breakpoints.length > 0 && dimensions.value.width > 0) {
548
+ return resolveFloatSize(dimensions.value.width, props.breakpoints);
549
+ }
550
+ return { width: props.width, height: props.height };
551
+ });
552
+ const x = useMotionValue(0);
553
+ const y = useMotionValue(0);
485
554
  const isInitialized = ref(false);
486
555
  const containerDimensions = computed(() => ({
487
556
  width: dimensions.value.width,
@@ -490,21 +559,23 @@ const FloatingGridItem = defineComponent({
490
559
  const getCornerPosition = (corner) => {
491
560
  const padding = props.edgePadding + props.initialPosition.x;
492
561
  const dims = containerDimensions.value;
562
+ const ew = effectiveSize.value.width;
563
+ const eh = effectiveSize.value.height;
493
564
  switch (corner) {
494
565
  case "top-left":
495
566
  return { x: padding, y: padding };
496
567
  case "top-right":
497
- return { x: dims.width - props.width - padding, y: padding };
568
+ return { x: dims.width - ew - padding, y: padding };
498
569
  case "bottom-left":
499
- return { x: padding, y: dims.height - props.height - padding };
570
+ return { x: padding, y: dims.height - eh - padding };
500
571
  case "bottom-right":
501
572
  default:
502
- return { x: dims.width - props.width - padding, y: dims.height - props.height - padding };
573
+ return { x: dims.width - ew - padding, y: dims.height - eh - padding };
503
574
  }
504
575
  };
505
- const findNearestCorner = (x, y) => {
506
- const centerX = x + props.width / 2;
507
- const centerY = y + props.height / 2;
576
+ const findNearestCorner = (posX, posY) => {
577
+ const centerX = posX + effectiveSize.value.width / 2;
578
+ const centerY = posY + effectiveSize.value.height / 2;
508
579
  const dims = containerDimensions.value;
509
580
  const containerCenterX = dims.width / 2;
510
581
  const containerCenterY = dims.height / 2;
@@ -518,78 +589,95 @@ const FloatingGridItem = defineComponent({
518
589
  return "bottom-left";
519
590
  return "bottom-right";
520
591
  };
521
- const currentPos = computed(() => getCornerPosition(currentAnchor.value));
522
- const handleDragStart = (e) => {
523
- isDragging.value = true;
524
- const pos = "touches" in e ? e.touches[0] : e;
525
- startPos.value = { x: pos.clientX, y: pos.clientY };
526
- dragOffset.value = { x: 0, y: 0 };
527
- };
528
- const handleDragMove = (e) => {
529
- if (!isDragging.value)
530
- return;
531
- e.preventDefault();
532
- const pos = "touches" in e ? e.touches[0] : e;
533
- dragOffset.value = {
534
- x: pos.clientX - startPos.value.x,
535
- y: pos.clientY - startPos.value.y
536
- };
537
- displayPosition.value = {
538
- x: currentPos.value.x + dragOffset.value.x,
539
- y: currentPos.value.y + dragOffset.value.y
540
- };
541
- };
542
- const handleDragEnd = () => {
543
- if (!isDragging.value)
544
- return;
545
- isDragging.value = false;
546
- const finalX = displayPosition.value.x;
547
- const finalY = displayPosition.value.y;
548
- const nearestCorner = findNearestCorner(finalX, finalY);
549
- currentAnchor.value = nearestCorner;
550
- emit("anchorChange", nearestCorner);
551
- displayPosition.value = getCornerPosition(nearestCorner);
552
- dragOffset.value = { x: 0, y: 0 };
553
- };
592
+ watch(
593
+ [() => containerDimensions.value.width, () => containerDimensions.value.height],
594
+ ([w, h2]) => {
595
+ if (w > 0 && h2 > 0 && !isInitialized.value) {
596
+ const pos = getCornerPosition(currentAnchor.value);
597
+ x.set(pos.x);
598
+ y.set(pos.y);
599
+ isInitialized.value = true;
600
+ }
601
+ },
602
+ { immediate: true }
603
+ );
604
+ watch(
605
+ [
606
+ () => props.anchor,
607
+ () => containerDimensions.value.width,
608
+ () => containerDimensions.value.height
609
+ ],
610
+ ([newAnchor, w, h2]) => {
611
+ if (isInitialized.value && w > 0 && h2 > 0 && newAnchor !== currentAnchor.value) {
612
+ currentAnchor.value = newAnchor;
613
+ const pos = getCornerPosition(newAnchor);
614
+ const springCfg = { type: "spring", stiffness: 400, damping: 30 };
615
+ animate(x, pos.x, springCfg);
616
+ animate(y, pos.y, springCfg);
617
+ }
618
+ }
619
+ );
620
+ watch(
621
+ [() => effectiveSize.value.width, () => effectiveSize.value.height],
622
+ () => {
623
+ if (isInitialized.value && containerDimensions.value.width > 0 && containerDimensions.value.height > 0) {
624
+ const pos = getCornerPosition(currentAnchor.value);
625
+ const springCfg = { type: "spring", stiffness: 400, damping: 30 };
626
+ animate(x, pos.x, springCfg);
627
+ animate(y, pos.y, springCfg);
628
+ }
629
+ }
630
+ );
554
631
  return () => {
555
632
  const dims = containerDimensions.value;
556
633
  if (!props.visible || dims.width === 0 || dims.height === 0) {
557
634
  return null;
558
635
  }
559
- if (!isInitialized.value) {
560
- displayPosition.value = currentPos.value;
561
- isInitialized.value = true;
562
- }
636
+ const ew = effectiveSize.value.width;
637
+ const eh = effectiveSize.value.height;
638
+ const padding = props.edgePadding + props.initialPosition.x;
639
+ const dragConstraints = {
640
+ left: padding,
641
+ right: dims.width - ew - padding,
642
+ top: padding,
643
+ bottom: dims.height - eh - padding
644
+ };
645
+ const handleDragEnd = () => {
646
+ const currentX = x.get();
647
+ const currentY = y.get();
648
+ const nearestCorner = findNearestCorner(currentX, currentY);
649
+ currentAnchor.value = nearestCorner;
650
+ emit("anchorChange", nearestCorner);
651
+ const snapPos = getCornerPosition(nearestCorner);
652
+ const springCfg = { type: "spring", stiffness: 400, damping: 30 };
653
+ animate(x, snapPos.x, springCfg);
654
+ animate(y, snapPos.y, springCfg);
655
+ };
563
656
  return h(
564
657
  motion.div,
565
658
  {
566
- animate: {
567
- x: displayPosition.value.x,
568
- y: displayPosition.value.y,
569
- opacity: 1,
570
- scale: isDragging.value ? 1.05 : 1
571
- },
572
- transition: isDragging.value ? { duration: 0 } : { type: "spring", stiffness: 400, damping: 30 },
659
+ drag: true,
660
+ dragMomentum: false,
661
+ dragElastic: 0.1,
662
+ dragConstraints,
573
663
  style: {
574
664
  position: "absolute",
575
- width: `${props.width}px`,
576
- height: `${props.height}px`,
665
+ width: `${ew}px`,
666
+ height: `${eh}px`,
577
667
  borderRadius: `${props.borderRadius}px`,
578
- boxShadow: isDragging.value ? "0 8px 32px rgba(0,0,0,0.4)" : props.boxShadow,
668
+ boxShadow: props.boxShadow,
579
669
  overflow: "hidden",
580
- cursor: isDragging.value ? "grabbing" : "grab",
670
+ cursor: "grab",
581
671
  zIndex: 100,
582
672
  touchAction: "none",
583
673
  left: 0,
584
- top: 0
674
+ top: 0,
675
+ x,
676
+ y
585
677
  },
586
- onMousedown: handleDragStart,
587
- onMousemove: handleDragMove,
588
- onMouseup: handleDragEnd,
589
- onMouseleave: handleDragEnd,
590
- onTouchstart: handleDragStart,
591
- onTouchmove: handleDragMove,
592
- onTouchend: handleDragEnd
678
+ whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
679
+ transition: { type: "spring", stiffness: 400, damping: 30 },
680
+ onDragEnd: handleDragEnd
593
681
  },
594
682
  slots.default?.()
595
683
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thangdevalone/meet-layout-grid-vue",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Vue 3 integration for meet-layout-grid with Motion animations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@vueuse/core": "^10.7.0",
45
45
  "motion-v": "^1.0.0",
46
- "@thangdevalone/meet-layout-grid-core": "1.3.2"
46
+ "@thangdevalone/meet-layout-grid-core": "1.3.4"
47
47
  },
48
48
  "devDependencies": {
49
49
  "vue": "^3.4.0",