@thangdevalone/meeting-grid-layout-vue 1.4.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/LICENSE +44 -0
- package/README.md +175 -0
- package/dist/index.cjs +704 -0
- package/dist/index.d.cts +471 -0
- package/dist/index.d.mts +471 -0
- package/dist/index.d.ts +471 -0
- package/dist/index.mjs +688 -0
- package/package.json +61 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const vue = require('vue');
|
|
4
|
+
const core = require('@vueuse/core');
|
|
5
|
+
const meetingGridLayoutCore = require('@thangdevalone/meeting-grid-layout-core');
|
|
6
|
+
const motionV = require('motion-v');
|
|
7
|
+
|
|
8
|
+
function useGridDimensions(elementRef) {
|
|
9
|
+
const width = vue.ref(0);
|
|
10
|
+
const height = vue.ref(0);
|
|
11
|
+
core.useResizeObserver(elementRef, (entries) => {
|
|
12
|
+
const entry = entries[0];
|
|
13
|
+
if (entry) {
|
|
14
|
+
width.value = entry.contentRect.width;
|
|
15
|
+
height.value = entry.contentRect.height;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
vue.onMounted(() => {
|
|
19
|
+
if (elementRef.value) {
|
|
20
|
+
width.value = elementRef.value.clientWidth;
|
|
21
|
+
height.value = elementRef.value.clientHeight;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return vue.computed(() => ({
|
|
25
|
+
width: width.value,
|
|
26
|
+
height: height.value
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
function useMeetGrid(options) {
|
|
30
|
+
const getOptions = typeof options === "function" ? options : () => options.value;
|
|
31
|
+
return vue.computed(() => {
|
|
32
|
+
const opts = getOptions();
|
|
33
|
+
return meetingGridLayoutCore.createMeetGrid(opts);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function useGridAnimation(preset = "smooth") {
|
|
37
|
+
return vue.computed(() => meetingGridLayoutCore.getSpringConfig(preset));
|
|
38
|
+
}
|
|
39
|
+
function useGridItemPosition(grid, index) {
|
|
40
|
+
const getIndex = () => typeof index === "number" ? index : index.value;
|
|
41
|
+
const position = vue.computed(() => grid.value.getPosition(getIndex()));
|
|
42
|
+
const dimensions = vue.computed(() => grid.value.getItemDimensions(getIndex()));
|
|
43
|
+
const isMain = vue.computed(() => grid.value.isMainItem(getIndex()));
|
|
44
|
+
const isHidden = vue.computed(() => {
|
|
45
|
+
return grid.value.layoutMode === "spotlight" && !isMain.value;
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
position,
|
|
49
|
+
dimensions,
|
|
50
|
+
isMain,
|
|
51
|
+
isHidden
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const GridContextKey = Symbol("MeetGridContext");
|
|
56
|
+
const GridContainer = vue.defineComponent({
|
|
57
|
+
name: "GridContainer",
|
|
58
|
+
props: {
|
|
59
|
+
/** Aspect ratio in format "width:height" */
|
|
60
|
+
aspectRatio: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: "16:9"
|
|
63
|
+
},
|
|
64
|
+
/** Gap between items in pixels */
|
|
65
|
+
gap: {
|
|
66
|
+
type: Number,
|
|
67
|
+
default: 8
|
|
68
|
+
},
|
|
69
|
+
/** Number of items */
|
|
70
|
+
count: {
|
|
71
|
+
type: Number,
|
|
72
|
+
required: true
|
|
73
|
+
},
|
|
74
|
+
/** Layout mode */
|
|
75
|
+
layoutMode: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: "gallery"
|
|
78
|
+
},
|
|
79
|
+
/** Index of pinned/focused item (main participant for pin/spotlight modes) */
|
|
80
|
+
pinnedIndex: {
|
|
81
|
+
type: Number,
|
|
82
|
+
default: void 0
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Position of "others" thumbnails when a participant is pinned.
|
|
86
|
+
* In portrait containers, this is forced to 'bottom'.
|
|
87
|
+
* @default 'right'
|
|
88
|
+
*/
|
|
89
|
+
othersPosition: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: "right"
|
|
92
|
+
},
|
|
93
|
+
/** Spring animation preset */
|
|
94
|
+
springPreset: {
|
|
95
|
+
type: String,
|
|
96
|
+
default: "smooth"
|
|
97
|
+
},
|
|
98
|
+
/** Maximum items per page for pagination (0 = no pagination) */
|
|
99
|
+
maxItemsPerPage: {
|
|
100
|
+
type: Number,
|
|
101
|
+
default: 0
|
|
102
|
+
},
|
|
103
|
+
/** Current page index (0-based) for pagination */
|
|
104
|
+
currentPage: {
|
|
105
|
+
type: Number,
|
|
106
|
+
default: 0
|
|
107
|
+
},
|
|
108
|
+
/** Maximum visible items (0 = show all). In gallery without pin: limits all items. With pin: limits "others". */
|
|
109
|
+
maxVisible: {
|
|
110
|
+
type: Number,
|
|
111
|
+
default: 0
|
|
112
|
+
},
|
|
113
|
+
/** Current page for visible items (0-based), used when maxVisible > 0 */
|
|
114
|
+
currentVisiblePage: {
|
|
115
|
+
type: Number,
|
|
116
|
+
default: 0
|
|
117
|
+
},
|
|
118
|
+
/**
|
|
119
|
+
* Per-item aspect ratio configurations.
|
|
120
|
+
* Use different ratios for mobile (9:16), desktop (16:9).
|
|
121
|
+
* @example ['16:9', '9:16', undefined]
|
|
122
|
+
*/
|
|
123
|
+
itemAspectRatios: {
|
|
124
|
+
type: Array,
|
|
125
|
+
default: void 0
|
|
126
|
+
},
|
|
127
|
+
/** Custom width for the floating PiP item in 2-person mode */
|
|
128
|
+
floatWidth: {
|
|
129
|
+
type: Number,
|
|
130
|
+
default: void 0
|
|
131
|
+
},
|
|
132
|
+
/** Custom height for the floating PiP item in 2-person mode */
|
|
133
|
+
floatHeight: {
|
|
134
|
+
type: Number,
|
|
135
|
+
default: void 0
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* Responsive breakpoints for the floating PiP in 2-person mode.
|
|
139
|
+
* When provided, PiP size auto-adjusts based on container width.
|
|
140
|
+
* Use `DEFAULT_FLOAT_BREAKPOINTS` for a ready-made 5-level responsive config.
|
|
141
|
+
*/
|
|
142
|
+
floatBreakpoints: {
|
|
143
|
+
type: Array,
|
|
144
|
+
default: void 0
|
|
145
|
+
},
|
|
146
|
+
/** HTML tag to render */
|
|
147
|
+
tag: {
|
|
148
|
+
type: String,
|
|
149
|
+
default: "div"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
setup(props, { slots }) {
|
|
153
|
+
const containerRef = vue.ref(null);
|
|
154
|
+
const dimensions = useGridDimensions(containerRef);
|
|
155
|
+
const gridOptions = vue.computed(() => ({
|
|
156
|
+
dimensions: dimensions.value,
|
|
157
|
+
count: props.count,
|
|
158
|
+
aspectRatio: props.aspectRatio,
|
|
159
|
+
gap: props.gap,
|
|
160
|
+
layoutMode: props.layoutMode,
|
|
161
|
+
pinnedIndex: props.pinnedIndex,
|
|
162
|
+
othersPosition: props.othersPosition,
|
|
163
|
+
maxItemsPerPage: props.maxItemsPerPage,
|
|
164
|
+
currentPage: props.currentPage,
|
|
165
|
+
maxVisible: props.maxVisible,
|
|
166
|
+
currentVisiblePage: props.currentVisiblePage,
|
|
167
|
+
itemAspectRatios: props.itemAspectRatios,
|
|
168
|
+
floatWidth: props.floatWidth,
|
|
169
|
+
floatHeight: props.floatHeight,
|
|
170
|
+
floatBreakpoints: props.floatBreakpoints
|
|
171
|
+
}));
|
|
172
|
+
const grid = useMeetGrid(gridOptions);
|
|
173
|
+
vue.provide(GridContextKey, {
|
|
174
|
+
grid,
|
|
175
|
+
springPreset: props.springPreset,
|
|
176
|
+
dimensions
|
|
177
|
+
});
|
|
178
|
+
return () => vue.h(
|
|
179
|
+
props.tag,
|
|
180
|
+
{
|
|
181
|
+
ref: containerRef,
|
|
182
|
+
style: {
|
|
183
|
+
position: "relative",
|
|
184
|
+
width: "100%",
|
|
185
|
+
height: "100%",
|
|
186
|
+
overflow: "hidden"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
slots.default?.()
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
const GridItem = vue.defineComponent({
|
|
194
|
+
name: "GridItem",
|
|
195
|
+
props: {
|
|
196
|
+
/** Index of this item in the grid */
|
|
197
|
+
index: {
|
|
198
|
+
type: Number,
|
|
199
|
+
required: true
|
|
200
|
+
},
|
|
201
|
+
/** Whether to disable animations */
|
|
202
|
+
disableAnimation: {
|
|
203
|
+
type: Boolean,
|
|
204
|
+
default: false
|
|
205
|
+
},
|
|
206
|
+
/** Optional item-specific aspect ratio (overrides itemAspectRatios from container) */
|
|
207
|
+
itemAspectRatio: {
|
|
208
|
+
type: String,
|
|
209
|
+
default: void 0
|
|
210
|
+
},
|
|
211
|
+
/** HTML tag to render */
|
|
212
|
+
tag: {
|
|
213
|
+
type: String,
|
|
214
|
+
default: "div"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
setup(props, { slots }) {
|
|
218
|
+
const context = vue.inject(GridContextKey);
|
|
219
|
+
if (!context) {
|
|
220
|
+
console.warn("GridItem must be used inside a GridContainer");
|
|
221
|
+
return () => null;
|
|
222
|
+
}
|
|
223
|
+
const { grid, springPreset, dimensions: containerDimensions } = context;
|
|
224
|
+
const position = vue.computed(() => grid.value.getPosition(props.index));
|
|
225
|
+
const dimensions = vue.computed(() => grid.value.getItemDimensions(props.index));
|
|
226
|
+
const contentDimensions = vue.computed(
|
|
227
|
+
() => grid.value.getItemContentDimensions(props.index, props.itemAspectRatio)
|
|
228
|
+
);
|
|
229
|
+
const isMain = vue.computed(() => grid.value.isMainItem(props.index));
|
|
230
|
+
const isVisible = vue.computed(() => grid.value.isItemVisible(props.index));
|
|
231
|
+
const isHidden = vue.computed(() => {
|
|
232
|
+
if (grid.value.layoutMode === "spotlight" && !isMain.value)
|
|
233
|
+
return true;
|
|
234
|
+
if (!isVisible.value)
|
|
235
|
+
return true;
|
|
236
|
+
return false;
|
|
237
|
+
});
|
|
238
|
+
const isFloat = vue.computed(() => grid.value.floatIndex === props.index);
|
|
239
|
+
const floatDims = vue.computed(() => grid.value.floatDimensions ?? { width: 120, height: 160 });
|
|
240
|
+
const floatAnchor = vue.ref(
|
|
241
|
+
"bottom-right"
|
|
242
|
+
);
|
|
243
|
+
const x = motionV.useMotionValue(0);
|
|
244
|
+
const y = motionV.useMotionValue(0);
|
|
245
|
+
const floatInitialized = vue.ref(false);
|
|
246
|
+
const getFloatCornerPos = (corner) => {
|
|
247
|
+
const padding = 12;
|
|
248
|
+
const dims = containerDimensions.value;
|
|
249
|
+
const fw = floatDims.value.width;
|
|
250
|
+
const fh = floatDims.value.height;
|
|
251
|
+
switch (corner) {
|
|
252
|
+
case "top-left":
|
|
253
|
+
return { x: padding, y: padding };
|
|
254
|
+
case "top-right":
|
|
255
|
+
return { x: dims.width - fw - padding, y: padding };
|
|
256
|
+
case "bottom-left":
|
|
257
|
+
return { x: padding, y: dims.height - fh - padding };
|
|
258
|
+
case "bottom-right":
|
|
259
|
+
default:
|
|
260
|
+
return { x: dims.width - fw - padding, y: dims.height - fh - padding };
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const findFloatNearestCorner = (posX, posY) => {
|
|
264
|
+
const fw = floatDims.value.width;
|
|
265
|
+
const fh = floatDims.value.height;
|
|
266
|
+
const centerX = posX + fw / 2;
|
|
267
|
+
const centerY = posY + fh / 2;
|
|
268
|
+
const dims = containerDimensions.value;
|
|
269
|
+
const isLeft = centerX < dims.width / 2;
|
|
270
|
+
const isTop = centerY < dims.height / 2;
|
|
271
|
+
if (isTop && isLeft)
|
|
272
|
+
return "top-left";
|
|
273
|
+
if (isTop && !isLeft)
|
|
274
|
+
return "top-right";
|
|
275
|
+
if (!isTop && isLeft)
|
|
276
|
+
return "bottom-left";
|
|
277
|
+
return "bottom-right";
|
|
278
|
+
};
|
|
279
|
+
vue.watch(isFloat, (floating) => {
|
|
280
|
+
if (!floating) {
|
|
281
|
+
floatInitialized.value = false;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
vue.watch(
|
|
285
|
+
[isFloat, () => containerDimensions.value.width, () => containerDimensions.value.height],
|
|
286
|
+
([floating, w, h2]) => {
|
|
287
|
+
if (floating && w > 0 && h2 > 0 && !floatInitialized.value) {
|
|
288
|
+
const pos = getFloatCornerPos(floatAnchor.value);
|
|
289
|
+
x.set(pos.x);
|
|
290
|
+
y.set(pos.y);
|
|
291
|
+
floatInitialized.value = true;
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{ immediate: true }
|
|
295
|
+
);
|
|
296
|
+
vue.watch(
|
|
297
|
+
[floatAnchor, () => containerDimensions.value.width, () => containerDimensions.value.height],
|
|
298
|
+
([, w, h2]) => {
|
|
299
|
+
if (isFloat.value && floatInitialized.value && w > 0 && h2 > 0) {
|
|
300
|
+
const pos = getFloatCornerPos(floatAnchor.value);
|
|
301
|
+
const springCfg = { type: "spring", stiffness: 400, damping: 30 };
|
|
302
|
+
motionV.animate(x, pos.x, springCfg);
|
|
303
|
+
motionV.animate(y, pos.y, springCfg);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
const isLastVisibleOther = vue.computed(() => {
|
|
308
|
+
const lastVisibleOthersIndex = grid.value.getLastVisibleOthersIndex();
|
|
309
|
+
return props.index === lastVisibleOthersIndex;
|
|
310
|
+
});
|
|
311
|
+
const hiddenCount = vue.computed(() => grid.value.hiddenCount);
|
|
312
|
+
const springConfig = meetingGridLayoutCore.getSpringConfig(springPreset);
|
|
313
|
+
const gridX = motionV.useMotionValue(0);
|
|
314
|
+
const gridY = motionV.useMotionValue(0);
|
|
315
|
+
const gridAnimReady = vue.ref(false);
|
|
316
|
+
vue.watch(
|
|
317
|
+
[
|
|
318
|
+
() => position.value.top,
|
|
319
|
+
() => position.value.left,
|
|
320
|
+
isFloat,
|
|
321
|
+
isHidden
|
|
322
|
+
],
|
|
323
|
+
([, , floating, hidden]) => {
|
|
324
|
+
if (floating || hidden) {
|
|
325
|
+
gridAnimReady.value = false;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const pos = position.value;
|
|
329
|
+
if (!gridAnimReady.value) {
|
|
330
|
+
gridX.set(pos.left);
|
|
331
|
+
gridY.set(pos.top);
|
|
332
|
+
gridAnimReady.value = true;
|
|
333
|
+
} else {
|
|
334
|
+
const cfg = {
|
|
335
|
+
type: "spring",
|
|
336
|
+
stiffness: springConfig.stiffness,
|
|
337
|
+
damping: springConfig.damping
|
|
338
|
+
};
|
|
339
|
+
motionV.animate(gridX, pos.left, cfg);
|
|
340
|
+
motionV.animate(gridY, pos.top, cfg);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{ immediate: true }
|
|
344
|
+
);
|
|
345
|
+
const slotProps = vue.computed(() => ({
|
|
346
|
+
contentDimensions: contentDimensions.value,
|
|
347
|
+
isLastVisibleOther: isLastVisibleOther.value,
|
|
348
|
+
hiddenCount: hiddenCount.value,
|
|
349
|
+
isFloat: isFloat.value
|
|
350
|
+
}));
|
|
351
|
+
return () => {
|
|
352
|
+
if (isHidden.value) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
if (isFloat.value) {
|
|
356
|
+
const dims = containerDimensions.value;
|
|
357
|
+
if (dims.width === 0 || dims.height === 0)
|
|
358
|
+
return null;
|
|
359
|
+
const dragConstraints = {
|
|
360
|
+
left: 12,
|
|
361
|
+
right: dims.width - floatDims.value.width - 12,
|
|
362
|
+
top: 12,
|
|
363
|
+
bottom: dims.height - floatDims.value.height - 12
|
|
364
|
+
};
|
|
365
|
+
const handleDragEnd = () => {
|
|
366
|
+
const currentX = x.get();
|
|
367
|
+
const currentY = y.get();
|
|
368
|
+
const nearestCorner = findFloatNearestCorner(currentX, currentY);
|
|
369
|
+
floatAnchor.value = nearestCorner;
|
|
370
|
+
const snapPos = getFloatCornerPos(nearestCorner);
|
|
371
|
+
const springCfg = { type: "spring", stiffness: 400, damping: 30 };
|
|
372
|
+
motionV.animate(x, snapPos.x, springCfg);
|
|
373
|
+
motionV.animate(y, snapPos.y, springCfg);
|
|
374
|
+
};
|
|
375
|
+
return vue.h(
|
|
376
|
+
motionV.motion.div,
|
|
377
|
+
{
|
|
378
|
+
// Key forces Vue to recreate this element when switching float↔grid
|
|
379
|
+
key: `float-${props.index}`,
|
|
380
|
+
drag: true,
|
|
381
|
+
dragMomentum: false,
|
|
382
|
+
dragElastic: 0.1,
|
|
383
|
+
dragConstraints,
|
|
384
|
+
style: {
|
|
385
|
+
position: "absolute",
|
|
386
|
+
width: `${floatDims.value.width}px`,
|
|
387
|
+
height: `${floatDims.value.height}px`,
|
|
388
|
+
borderRadius: "12px",
|
|
389
|
+
boxShadow: "0 4px 20px rgba(0,0,0,0.3)",
|
|
390
|
+
overflow: "hidden",
|
|
391
|
+
cursor: "grab",
|
|
392
|
+
zIndex: 100,
|
|
393
|
+
touchAction: "none",
|
|
394
|
+
left: 0,
|
|
395
|
+
top: 0,
|
|
396
|
+
x,
|
|
397
|
+
y
|
|
398
|
+
},
|
|
399
|
+
whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
|
|
400
|
+
transition: { type: "spring", stiffness: 400, damping: 30 },
|
|
401
|
+
"data-grid-index": props.index,
|
|
402
|
+
"data-grid-float": true,
|
|
403
|
+
onDragEnd: handleDragEnd
|
|
404
|
+
},
|
|
405
|
+
() => slots.default?.(slotProps.value)
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const itemWidth = dimensions.value.width;
|
|
409
|
+
const itemHeight = dimensions.value.height;
|
|
410
|
+
if (props.disableAnimation) {
|
|
411
|
+
return vue.h(
|
|
412
|
+
props.tag,
|
|
413
|
+
{
|
|
414
|
+
style: {
|
|
415
|
+
position: "absolute",
|
|
416
|
+
width: `${itemWidth}px`,
|
|
417
|
+
height: `${itemHeight}px`,
|
|
418
|
+
top: `${position.value.top}px`,
|
|
419
|
+
left: `${position.value.left}px`
|
|
420
|
+
},
|
|
421
|
+
"data-grid-index": props.index,
|
|
422
|
+
"data-grid-main": isMain.value
|
|
423
|
+
},
|
|
424
|
+
slots.default?.(slotProps.value)
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return vue.h(
|
|
428
|
+
motionV.motion.div,
|
|
429
|
+
{
|
|
430
|
+
key: `grid-${props.index}`,
|
|
431
|
+
style: {
|
|
432
|
+
position: "absolute",
|
|
433
|
+
top: 0,
|
|
434
|
+
left: 0,
|
|
435
|
+
x: gridX,
|
|
436
|
+
y: gridY,
|
|
437
|
+
width: `${itemWidth}px`,
|
|
438
|
+
height: `${itemHeight}px`
|
|
439
|
+
},
|
|
440
|
+
"data-grid-index": props.index,
|
|
441
|
+
"data-grid-main": isMain.value
|
|
442
|
+
},
|
|
443
|
+
() => slots.default?.(slotProps.value)
|
|
444
|
+
);
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const GridOverlay = vue.defineComponent({
|
|
449
|
+
name: "GridOverlay",
|
|
450
|
+
props: {
|
|
451
|
+
/** Whether to show the overlay */
|
|
452
|
+
visible: {
|
|
453
|
+
type: Boolean,
|
|
454
|
+
default: true
|
|
455
|
+
},
|
|
456
|
+
/** Background color */
|
|
457
|
+
backgroundColor: {
|
|
458
|
+
type: String,
|
|
459
|
+
default: "rgba(0,0,0,0.5)"
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
setup(props, { slots }) {
|
|
463
|
+
return () => {
|
|
464
|
+
if (!props.visible) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
return vue.h(
|
|
468
|
+
"div",
|
|
469
|
+
{
|
|
470
|
+
style: {
|
|
471
|
+
position: "absolute",
|
|
472
|
+
inset: 0,
|
|
473
|
+
display: "flex",
|
|
474
|
+
alignItems: "center",
|
|
475
|
+
justifyContent: "center",
|
|
476
|
+
backgroundColor: props.backgroundColor
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
slots.default?.()
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const FloatingGridItem = vue.defineComponent({
|
|
485
|
+
name: "FloatingGridItem",
|
|
486
|
+
props: {
|
|
487
|
+
/** Width of the floating item (px). Overridden by `breakpoints` when provided. */
|
|
488
|
+
width: {
|
|
489
|
+
type: Number,
|
|
490
|
+
default: 120
|
|
491
|
+
},
|
|
492
|
+
/** Height of the floating item (px). Overridden by `breakpoints` when provided. */
|
|
493
|
+
height: {
|
|
494
|
+
type: Number,
|
|
495
|
+
default: 160
|
|
496
|
+
},
|
|
497
|
+
/**
|
|
498
|
+
* Responsive breakpoints for PiP sizing.
|
|
499
|
+
* When provided, width/height auto-adjust based on container width.
|
|
500
|
+
* Overrides the fixed `width`/`height` props.
|
|
501
|
+
* Use `DEFAULT_FLOAT_BREAKPOINTS` for a ready-made 5-level responsive config.
|
|
502
|
+
*/
|
|
503
|
+
breakpoints: {
|
|
504
|
+
type: Array,
|
|
505
|
+
default: void 0
|
|
506
|
+
},
|
|
507
|
+
/** Initial position (x, y from container edges) */
|
|
508
|
+
initialPosition: {
|
|
509
|
+
type: Object,
|
|
510
|
+
default: () => ({ x: 16, y: 16 })
|
|
511
|
+
},
|
|
512
|
+
/** Which corner to anchor */
|
|
513
|
+
anchor: {
|
|
514
|
+
type: String,
|
|
515
|
+
default: "bottom-right"
|
|
516
|
+
},
|
|
517
|
+
/** Whether the item is visible */
|
|
518
|
+
visible: {
|
|
519
|
+
type: Boolean,
|
|
520
|
+
default: true
|
|
521
|
+
},
|
|
522
|
+
/** Padding from container edges */
|
|
523
|
+
edgePadding: {
|
|
524
|
+
type: Number,
|
|
525
|
+
default: 12
|
|
526
|
+
},
|
|
527
|
+
/** Border radius */
|
|
528
|
+
borderRadius: {
|
|
529
|
+
type: Number,
|
|
530
|
+
default: 12
|
|
531
|
+
},
|
|
532
|
+
/** Box shadow */
|
|
533
|
+
boxShadow: {
|
|
534
|
+
type: String,
|
|
535
|
+
default: "0 4px 20px rgba(0,0,0,0.3)"
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
emits: ["anchorChange"],
|
|
539
|
+
setup(props, { slots, emit }) {
|
|
540
|
+
const context = vue.inject(GridContextKey);
|
|
541
|
+
if (!context) {
|
|
542
|
+
console.warn("FloatingGridItem must be used inside a GridContainer");
|
|
543
|
+
return () => null;
|
|
544
|
+
}
|
|
545
|
+
const { dimensions } = context;
|
|
546
|
+
const currentAnchor = vue.ref(props.anchor);
|
|
547
|
+
const effectiveSize = vue.computed(() => {
|
|
548
|
+
if (props.breakpoints && props.breakpoints.length > 0 && dimensions.value.width > 0) {
|
|
549
|
+
return meetingGridLayoutCore.resolveFloatSize(dimensions.value.width, props.breakpoints);
|
|
550
|
+
}
|
|
551
|
+
return { width: props.width, height: props.height };
|
|
552
|
+
});
|
|
553
|
+
const x = motionV.useMotionValue(0);
|
|
554
|
+
const y = motionV.useMotionValue(0);
|
|
555
|
+
const isInitialized = vue.ref(false);
|
|
556
|
+
const containerDimensions = vue.computed(() => ({
|
|
557
|
+
width: dimensions.value.width,
|
|
558
|
+
height: dimensions.value.height
|
|
559
|
+
}));
|
|
560
|
+
const getCornerPosition = (corner) => {
|
|
561
|
+
const padding = props.edgePadding + props.initialPosition.x;
|
|
562
|
+
const dims = containerDimensions.value;
|
|
563
|
+
const ew = effectiveSize.value.width;
|
|
564
|
+
const eh = effectiveSize.value.height;
|
|
565
|
+
switch (corner) {
|
|
566
|
+
case "top-left":
|
|
567
|
+
return { x: padding, y: padding };
|
|
568
|
+
case "top-right":
|
|
569
|
+
return { x: dims.width - ew - padding, y: padding };
|
|
570
|
+
case "bottom-left":
|
|
571
|
+
return { x: padding, y: dims.height - eh - padding };
|
|
572
|
+
case "bottom-right":
|
|
573
|
+
default:
|
|
574
|
+
return { x: dims.width - ew - padding, y: dims.height - eh - padding };
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
const findNearestCorner = (posX, posY) => {
|
|
578
|
+
const centerX = posX + effectiveSize.value.width / 2;
|
|
579
|
+
const centerY = posY + effectiveSize.value.height / 2;
|
|
580
|
+
const dims = containerDimensions.value;
|
|
581
|
+
const containerCenterX = dims.width / 2;
|
|
582
|
+
const containerCenterY = dims.height / 2;
|
|
583
|
+
const isLeft = centerX < containerCenterX;
|
|
584
|
+
const isTop = centerY < containerCenterY;
|
|
585
|
+
if (isTop && isLeft)
|
|
586
|
+
return "top-left";
|
|
587
|
+
if (isTop && !isLeft)
|
|
588
|
+
return "top-right";
|
|
589
|
+
if (!isTop && isLeft)
|
|
590
|
+
return "bottom-left";
|
|
591
|
+
return "bottom-right";
|
|
592
|
+
};
|
|
593
|
+
vue.watch(
|
|
594
|
+
[() => containerDimensions.value.width, () => containerDimensions.value.height],
|
|
595
|
+
([w, h2]) => {
|
|
596
|
+
if (w > 0 && h2 > 0 && !isInitialized.value) {
|
|
597
|
+
const pos = getCornerPosition(currentAnchor.value);
|
|
598
|
+
x.set(pos.x);
|
|
599
|
+
y.set(pos.y);
|
|
600
|
+
isInitialized.value = true;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{ immediate: true }
|
|
604
|
+
);
|
|
605
|
+
vue.watch(
|
|
606
|
+
[
|
|
607
|
+
() => props.anchor,
|
|
608
|
+
() => containerDimensions.value.width,
|
|
609
|
+
() => containerDimensions.value.height
|
|
610
|
+
],
|
|
611
|
+
([newAnchor, w, h2]) => {
|
|
612
|
+
if (isInitialized.value && w > 0 && h2 > 0 && newAnchor !== currentAnchor.value) {
|
|
613
|
+
currentAnchor.value = newAnchor;
|
|
614
|
+
const pos = getCornerPosition(newAnchor);
|
|
615
|
+
const springCfg = { type: "spring", stiffness: 400, damping: 30 };
|
|
616
|
+
motionV.animate(x, pos.x, springCfg);
|
|
617
|
+
motionV.animate(y, pos.y, springCfg);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
vue.watch(
|
|
622
|
+
[() => effectiveSize.value.width, () => effectiveSize.value.height],
|
|
623
|
+
() => {
|
|
624
|
+
if (isInitialized.value && containerDimensions.value.width > 0 && containerDimensions.value.height > 0) {
|
|
625
|
+
const pos = getCornerPosition(currentAnchor.value);
|
|
626
|
+
const springCfg = { type: "spring", stiffness: 400, damping: 30 };
|
|
627
|
+
motionV.animate(x, pos.x, springCfg);
|
|
628
|
+
motionV.animate(y, pos.y, springCfg);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
);
|
|
632
|
+
return () => {
|
|
633
|
+
const dims = containerDimensions.value;
|
|
634
|
+
if (!props.visible || dims.width === 0 || dims.height === 0) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
const ew = effectiveSize.value.width;
|
|
638
|
+
const eh = effectiveSize.value.height;
|
|
639
|
+
const padding = props.edgePadding + props.initialPosition.x;
|
|
640
|
+
const dragConstraints = {
|
|
641
|
+
left: padding,
|
|
642
|
+
right: dims.width - ew - padding,
|
|
643
|
+
top: padding,
|
|
644
|
+
bottom: dims.height - eh - padding
|
|
645
|
+
};
|
|
646
|
+
const handleDragEnd = () => {
|
|
647
|
+
const currentX = x.get();
|
|
648
|
+
const currentY = y.get();
|
|
649
|
+
const nearestCorner = findNearestCorner(currentX, currentY);
|
|
650
|
+
currentAnchor.value = nearestCorner;
|
|
651
|
+
emit("anchorChange", nearestCorner);
|
|
652
|
+
const snapPos = getCornerPosition(nearestCorner);
|
|
653
|
+
const springCfg = { type: "spring", stiffness: 400, damping: 30 };
|
|
654
|
+
motionV.animate(x, snapPos.x, springCfg);
|
|
655
|
+
motionV.animate(y, snapPos.y, springCfg);
|
|
656
|
+
};
|
|
657
|
+
return vue.h(
|
|
658
|
+
motionV.motion.div,
|
|
659
|
+
{
|
|
660
|
+
drag: true,
|
|
661
|
+
dragMomentum: false,
|
|
662
|
+
dragElastic: 0.1,
|
|
663
|
+
dragConstraints,
|
|
664
|
+
style: {
|
|
665
|
+
position: "absolute",
|
|
666
|
+
width: `${ew}px`,
|
|
667
|
+
height: `${eh}px`,
|
|
668
|
+
borderRadius: `${props.borderRadius}px`,
|
|
669
|
+
boxShadow: props.boxShadow,
|
|
670
|
+
overflow: "hidden",
|
|
671
|
+
cursor: "grab",
|
|
672
|
+
zIndex: 100,
|
|
673
|
+
touchAction: "none",
|
|
674
|
+
left: 0,
|
|
675
|
+
top: 0,
|
|
676
|
+
x,
|
|
677
|
+
y
|
|
678
|
+
},
|
|
679
|
+
whileDrag: { cursor: "grabbing", scale: 1.05, boxShadow: "0 8px 32px rgba(0,0,0,0.4)" },
|
|
680
|
+
transition: { type: "spring", stiffness: 400, damping: 30 },
|
|
681
|
+
onDragEnd: handleDragEnd
|
|
682
|
+
},
|
|
683
|
+
slots.default?.()
|
|
684
|
+
);
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
exports.createGrid = meetingGridLayoutCore.createGrid;
|
|
690
|
+
exports.createGridItemPositioner = meetingGridLayoutCore.createGridItemPositioner;
|
|
691
|
+
exports.createMeetGrid = meetingGridLayoutCore.createMeetGrid;
|
|
692
|
+
exports.getAspectRatio = meetingGridLayoutCore.getAspectRatio;
|
|
693
|
+
exports.getGridItemDimensions = meetingGridLayoutCore.getGridItemDimensions;
|
|
694
|
+
exports.getSpringConfig = meetingGridLayoutCore.getSpringConfig;
|
|
695
|
+
exports.springPresets = meetingGridLayoutCore.springPresets;
|
|
696
|
+
exports.FloatingGridItem = FloatingGridItem;
|
|
697
|
+
exports.GridContainer = GridContainer;
|
|
698
|
+
exports.GridContextKey = GridContextKey;
|
|
699
|
+
exports.GridItem = GridItem;
|
|
700
|
+
exports.GridOverlay = GridOverlay;
|
|
701
|
+
exports.useGridAnimation = useGridAnimation;
|
|
702
|
+
exports.useGridDimensions = useGridDimensions;
|
|
703
|
+
exports.useGridItemPosition = useGridItemPosition;
|
|
704
|
+
exports.useMeetGrid = useMeetGrid;
|