@thangdevalone/meet-layout-grid-vue 1.3.1 → 1.3.3

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, {
@@ -197,7 +219,7 @@ const GridItem = defineComponent({
197
219
  console.warn("GridItem must be used inside a GridContainer");
198
220
  return () => null;
199
221
  }
200
- const { grid, springPreset } = context;
222
+ const { grid, springPreset, dimensions: containerDimensions } = context;
201
223
  const position = computed(() => grid.value.getPosition(props.index));
202
224
  const dimensions = computed(() => grid.value.getItemDimensions(props.index));
203
225
  const contentDimensions = computed(
@@ -212,63 +234,211 @@ const GridItem = defineComponent({
212
234
  return true;
213
235
  return false;
214
236
  });
237
+ const isFloat = computed(() => grid.value.floatIndex === props.index);
238
+ const floatDims = computed(() => grid.value.floatDimensions ?? { width: 120, height: 160 });
239
+ const floatAnchor = ref(
240
+ "bottom-right"
241
+ );
242
+ const x = useMotionValue(0);
243
+ const y = useMotionValue(0);
244
+ const floatInitialized = ref(false);
245
+ const getFloatCornerPos = (corner) => {
246
+ const padding = 12;
247
+ const dims = containerDimensions.value;
248
+ const fw = floatDims.value.width;
249
+ const fh = floatDims.value.height;
250
+ switch (corner) {
251
+ case "top-left":
252
+ return { x: padding, y: padding };
253
+ case "top-right":
254
+ return { x: dims.width - fw - padding, y: padding };
255
+ case "bottom-left":
256
+ return { x: padding, y: dims.height - fh - padding };
257
+ case "bottom-right":
258
+ default:
259
+ return { x: dims.width - fw - padding, y: dims.height - fh - padding };
260
+ }
261
+ };
262
+ const findFloatNearestCorner = (posX, posY) => {
263
+ const fw = floatDims.value.width;
264
+ const fh = floatDims.value.height;
265
+ const centerX = posX + fw / 2;
266
+ const centerY = posY + fh / 2;
267
+ const dims = containerDimensions.value;
268
+ const isLeft = centerX < dims.width / 2;
269
+ const isTop = centerY < dims.height / 2;
270
+ if (isTop && isLeft)
271
+ return "top-left";
272
+ if (isTop && !isLeft)
273
+ return "top-right";
274
+ if (!isTop && isLeft)
275
+ return "bottom-left";
276
+ return "bottom-right";
277
+ };
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
+ );
215
306
  const isLastVisibleOther = computed(() => {
216
307
  const lastVisibleOthersIndex = grid.value.getLastVisibleOthersIndex();
217
308
  return props.index === lastVisibleOthersIndex;
218
309
  });
219
310
  const hiddenCount = computed(() => grid.value.hiddenCount);
220
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
+ );
221
344
  const slotProps = computed(() => ({
222
345
  contentDimensions: contentDimensions.value,
223
346
  isLastVisibleOther: isLastVisibleOther.value,
224
- hiddenCount: hiddenCount.value
347
+ hiddenCount: hiddenCount.value,
348
+ isFloat: isFloat.value
225
349
  }));
226
350
  return () => {
227
351
  if (isHidden.value) {
228
352
  return null;
229
353
  }
230
- const animateProps = {
231
- width: dimensions.value.width,
232
- height: dimensions.value.height,
233
- top: position.value.top,
234
- left: position.value.left
235
- };
354
+ if (isFloat.value) {
355
+ const dims = containerDimensions.value;
356
+ if (dims.width === 0 || dims.height === 0)
357
+ return null;
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
+ };
374
+ return h(
375
+ motion.div,
376
+ {
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,
383
+ style: {
384
+ position: "absolute",
385
+ width: `${floatDims.value.width}px`,
386
+ height: `${floatDims.value.height}px`,
387
+ borderRadius: "12px",
388
+ boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
389
+ overflow: "hidden",
390
+ cursor: "grab",
391
+ zIndex: 100,
392
+ touchAction: "none",
393
+ left: 0,
394
+ top: 0,
395
+ x,
396
+ y
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 },
400
+ "data-grid-index": props.index,
401
+ "data-grid-float": true,
402
+ onDragEnd: handleDragEnd
403
+ },
404
+ () => slots.default?.(slotProps.value)
405
+ );
406
+ }
407
+ const itemWidth = dimensions.value.width;
408
+ const itemHeight = dimensions.value.height;
236
409
  if (props.disableAnimation) {
237
410
  return h(
238
411
  props.tag,
239
412
  {
240
413
  style: {
241
414
  position: "absolute",
242
- ...animateProps,
243
- width: `${animateProps.width}px`,
244
- height: `${animateProps.height}px`,
245
- top: `${animateProps.top}px`,
246
- left: `${animateProps.left}px`
415
+ width: `${itemWidth}px`,
416
+ height: `${itemHeight}px`,
417
+ top: `${position.value.top}px`,
418
+ left: `${position.value.left}px`
247
419
  },
248
420
  "data-grid-index": props.index,
249
421
  "data-grid-main": isMain.value
250
422
  },
251
- // Pass all slot props
252
423
  slots.default?.(slotProps.value)
253
424
  );
254
425
  }
255
426
  return h(
256
427
  motion.div,
257
428
  {
258
- as: props.tag,
259
- animate: animateProps,
260
- transition: {
261
- type: springConfig.type,
262
- stiffness: springConfig.stiffness,
263
- damping: springConfig.damping
264
- },
429
+ key: `grid-${props.index}`,
265
430
  style: {
266
- 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`
267
438
  },
268
439
  "data-grid-index": props.index,
269
440
  "data-grid-main": isMain.value
270
441
  },
271
- // Pass all slot props
272
442
  () => slots.default?.(slotProps.value)
273
443
  );
274
444
  };
@@ -313,16 +483,26 @@ const GridOverlay = defineComponent({
313
483
  const FloatingGridItem = defineComponent({
314
484
  name: "FloatingGridItem",
315
485
  props: {
316
- /** Width of the floating item */
486
+ /** Width of the floating item (px). Overridden by `breakpoints` when provided. */
317
487
  width: {
318
488
  type: Number,
319
489
  default: 120
320
490
  },
321
- /** Height of the floating item */
491
+ /** Height of the floating item (px). Overridden by `breakpoints` when provided. */
322
492
  height: {
323
493
  type: Number,
324
494
  default: 160
325
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
+ },
326
506
  /** Initial position (x, y from container edges) */
327
507
  initialPosition: {
328
508
  type: Object,
@@ -363,10 +543,14 @@ const FloatingGridItem = defineComponent({
363
543
  }
364
544
  const { dimensions } = context;
365
545
  const currentAnchor = ref(props.anchor);
366
- const isDragging = ref(false);
367
- const dragOffset = ref({ x: 0, y: 0 });
368
- const startPos = ref({ x: 0, y: 0 });
369
- 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);
370
554
  const isInitialized = ref(false);
371
555
  const containerDimensions = computed(() => ({
372
556
  width: dimensions.value.width,
@@ -375,21 +559,23 @@ const FloatingGridItem = defineComponent({
375
559
  const getCornerPosition = (corner) => {
376
560
  const padding = props.edgePadding + props.initialPosition.x;
377
561
  const dims = containerDimensions.value;
562
+ const ew = effectiveSize.value.width;
563
+ const eh = effectiveSize.value.height;
378
564
  switch (corner) {
379
565
  case "top-left":
380
566
  return { x: padding, y: padding };
381
567
  case "top-right":
382
- return { x: dims.width - props.width - padding, y: padding };
568
+ return { x: dims.width - ew - padding, y: padding };
383
569
  case "bottom-left":
384
- return { x: padding, y: dims.height - props.height - padding };
570
+ return { x: padding, y: dims.height - eh - padding };
385
571
  case "bottom-right":
386
572
  default:
387
- 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 };
388
574
  }
389
575
  };
390
- const findNearestCorner = (x, y) => {
391
- const centerX = x + props.width / 2;
392
- 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;
393
579
  const dims = containerDimensions.value;
394
580
  const containerCenterX = dims.width / 2;
395
581
  const containerCenterY = dims.height / 2;
@@ -403,78 +589,95 @@ const FloatingGridItem = defineComponent({
403
589
  return "bottom-left";
404
590
  return "bottom-right";
405
591
  };
406
- const currentPos = computed(() => getCornerPosition(currentAnchor.value));
407
- const handleDragStart = (e) => {
408
- isDragging.value = true;
409
- const pos = "touches" in e ? e.touches[0] : e;
410
- startPos.value = { x: pos.clientX, y: pos.clientY };
411
- dragOffset.value = { x: 0, y: 0 };
412
- };
413
- const handleDragMove = (e) => {
414
- if (!isDragging.value)
415
- return;
416
- e.preventDefault();
417
- const pos = "touches" in e ? e.touches[0] : e;
418
- dragOffset.value = {
419
- x: pos.clientX - startPos.value.x,
420
- y: pos.clientY - startPos.value.y
421
- };
422
- displayPosition.value = {
423
- x: currentPos.value.x + dragOffset.value.x,
424
- y: currentPos.value.y + dragOffset.value.y
425
- };
426
- };
427
- const handleDragEnd = () => {
428
- if (!isDragging.value)
429
- return;
430
- isDragging.value = false;
431
- const finalX = displayPosition.value.x;
432
- const finalY = displayPosition.value.y;
433
- const nearestCorner = findNearestCorner(finalX, finalY);
434
- currentAnchor.value = nearestCorner;
435
- emit("anchorChange", nearestCorner);
436
- displayPosition.value = getCornerPosition(nearestCorner);
437
- dragOffset.value = { x: 0, y: 0 };
438
- };
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
+ );
439
631
  return () => {
440
632
  const dims = containerDimensions.value;
441
633
  if (!props.visible || dims.width === 0 || dims.height === 0) {
442
634
  return null;
443
635
  }
444
- if (!isInitialized.value) {
445
- displayPosition.value = currentPos.value;
446
- isInitialized.value = true;
447
- }
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
+ };
448
656
  return h(
449
657
  motion.div,
450
658
  {
451
- animate: {
452
- x: displayPosition.value.x,
453
- y: displayPosition.value.y,
454
- opacity: 1,
455
- scale: isDragging.value ? 1.05 : 1
456
- },
457
- transition: isDragging.value ? { duration: 0 } : { type: "spring", stiffness: 400, damping: 30 },
659
+ drag: true,
660
+ dragMomentum: false,
661
+ dragElastic: 0.1,
662
+ dragConstraints,
458
663
  style: {
459
664
  position: "absolute",
460
- width: `${props.width}px`,
461
- height: `${props.height}px`,
665
+ width: `${ew}px`,
666
+ height: `${eh}px`,
462
667
  borderRadius: `${props.borderRadius}px`,
463
- boxShadow: isDragging.value ? "0 8px 32px rgba(0,0,0,0.4)" : props.boxShadow,
668
+ boxShadow: props.boxShadow,
464
669
  overflow: "hidden",
465
- cursor: isDragging.value ? "grabbing" : "grab",
670
+ cursor: "grab",
466
671
  zIndex: 100,
467
672
  touchAction: "none",
468
673
  left: 0,
469
- top: 0
674
+ top: 0,
675
+ x,
676
+ y
470
677
  },
471
- onMousedown: handleDragStart,
472
- onMousemove: handleDragMove,
473
- onMouseup: handleDragEnd,
474
- onMouseleave: handleDragEnd,
475
- onTouchstart: handleDragStart,
476
- onTouchmove: handleDragMove,
477
- 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
478
681
  },
479
682
  slots.default?.()
480
683
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thangdevalone/meet-layout-grid-vue",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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.1"
46
+ "@thangdevalone/meet-layout-grid-core": "1.3.3"
47
47
  },
48
48
  "devDependencies": {
49
49
  "vue": "^3.4.0",