@thangdevalone/meet-layout-grid-core 1.0.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/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @meet-layout-grid/core
2
+
3
+ Core grid calculation logic for meet-layout-grid library. Zero dependencies, framework-agnostic.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @meet-layout-grid/core
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createMeetGrid, createGrid } from '@meet-layout-grid/core'
15
+
16
+ // Basic grid
17
+ const grid = createGrid({
18
+ dimensions: { width: 800, height: 600 },
19
+ count: 6,
20
+ aspectRatio: '16:9',
21
+ gap: 8,
22
+ })
23
+
24
+ console.log(`Item size: ${grid.width}x${grid.height}`)
25
+ console.log(`Grid: ${grid.cols} cols, ${grid.rows} rows`)
26
+
27
+ // Position each item
28
+ for (let i = 0; i < 6; i++) {
29
+ const { top, left } = grid.getPosition(i)
30
+ console.log(`Item ${i}: top=${top}, left=${left}`)
31
+ }
32
+
33
+ // Meet-style grid with layout modes
34
+ const meetGrid = createMeetGrid({
35
+ dimensions: { width: 800, height: 600 },
36
+ count: 6,
37
+ aspectRatio: '16:9',
38
+ gap: 8,
39
+ layoutMode: 'speaker', // 'gallery' | 'speaker' | 'spotlight' | 'sidebar'
40
+ speakerIndex: 0,
41
+ })
42
+
43
+ // Items may have different sizes in speaker mode
44
+ for (let i = 0; i < 6; i++) {
45
+ const { width, height } = meetGrid.getItemDimensions(i)
46
+ const isMain = meetGrid.isMainItem(i)
47
+ console.log(`Item ${i}: ${width}x${height}, main: ${isMain}`)
48
+ }
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `createGrid(options)`
54
+
55
+ Creates a basic responsive grid.
56
+
57
+ **Options:**
58
+ - `dimensions: { width, height }` - Container dimensions
59
+ - `count: number` - Number of items
60
+ - `aspectRatio: string` - Aspect ratio (e.g., "16:9")
61
+ - `gap: number` - Gap between items in pixels
62
+
63
+ **Returns:**
64
+ - `width: number` - Item width
65
+ - `height: number` - Item height
66
+ - `rows: number` - Number of rows
67
+ - `cols: number` - Number of columns
68
+ - `getPosition(index): { top, left }` - Position getter
69
+
70
+ ### `createMeetGrid(options)`
71
+
72
+ Creates a meet-style grid with layout modes.
73
+
74
+ **Additional Options:**
75
+ - `layoutMode: 'gallery' | 'speaker' | 'spotlight' | 'sidebar'`
76
+ - `pinnedIndex?: number` - Index of pinned item
77
+ - `speakerIndex?: number` - Index of active speaker
78
+ - `sidebarPosition?: 'left' | 'right' | 'bottom'`
79
+ - `sidebarRatio?: number` - Sidebar width ratio (0-1)
80
+
81
+ **Additional Returns:**
82
+ - `layoutMode: LayoutMode` - Current layout mode
83
+ - `getItemDimensions(index): { width, height }` - Per-item dimensions
84
+ - `isMainItem(index): boolean` - Check if item is featured
85
+
86
+ ## Layout Modes
87
+
88
+ - **Gallery** - Equal-sized tiles in a responsive grid
89
+ - **Speaker** - Active speaker takes larger space (65% height)
90
+ - **Spotlight** - Single participant in focus, others hidden
91
+ - **Sidebar** - Main view with thumbnail strip
92
+
93
+ ## License
94
+
95
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,349 @@
1
+ 'use strict';
2
+
3
+ function getAspectRatio(ratio) {
4
+ const [width, height] = ratio.split(":");
5
+ if (!width || !height) {
6
+ throw new Error(
7
+ 'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
8
+ );
9
+ }
10
+ return Number.parseInt(height) / Number.parseInt(width);
11
+ }
12
+ function parseAspectRatio(ratio) {
13
+ const [width, height] = ratio.split(":").map(Number);
14
+ if (!width || !height || isNaN(width) || isNaN(height)) {
15
+ throw new Error(
16
+ 'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
17
+ );
18
+ }
19
+ return { widthRatio: width, heightRatio: height };
20
+ }
21
+ function getGridItemDimensions({
22
+ count,
23
+ dimensions,
24
+ aspectRatio,
25
+ gap
26
+ }) {
27
+ let { width: W, height: H } = dimensions;
28
+ if (W === 0 || H === 0 || count === 0) {
29
+ return { width: 0, height: 0, rows: 1, cols: 1 };
30
+ }
31
+ W -= gap * 2;
32
+ H -= gap * 2;
33
+ const s = gap;
34
+ const N = count;
35
+ const r = getAspectRatio(aspectRatio);
36
+ let w = 0;
37
+ let h = 0;
38
+ let a = 1;
39
+ let b = 1;
40
+ const widths = [];
41
+ for (let n = 1; n <= N; n++) {
42
+ widths.push((W - s * (n - 1)) / n, (H - s * (n - 1)) / (n * r));
43
+ }
44
+ widths.sort((a2, b2) => b2 - a2);
45
+ for (const width of widths) {
46
+ w = width;
47
+ h = w * r;
48
+ a = Math.floor((W + s) / (w + s));
49
+ b = Math.floor((H + s) / (h + s));
50
+ if (a * b >= N) {
51
+ a = Math.ceil(N / b);
52
+ b = Math.ceil(N / a);
53
+ break;
54
+ }
55
+ }
56
+ return { width: w, height: h, rows: b, cols: a };
57
+ }
58
+ function createGridItemPositioner({
59
+ parentDimensions,
60
+ dimensions,
61
+ rows,
62
+ cols,
63
+ count,
64
+ gap
65
+ }) {
66
+ const { width: W, height: H } = parentDimensions;
67
+ const { width: w, height: h } = dimensions;
68
+ const firstTop = (H - (h * rows + (rows - 1) * gap)) / 2;
69
+ let firstLeft = (W - (w * cols + (cols - 1) * gap)) / 2;
70
+ const topAdd = h + gap;
71
+ const leftAdd = w + gap;
72
+ let col = 0;
73
+ let row = 0;
74
+ const incompleteRowCols = count % cols;
75
+ function getPosition(index) {
76
+ const remaining = count - index;
77
+ if (remaining === incompleteRowCols && incompleteRowCols > 0) {
78
+ firstLeft = (W - (w * remaining + (remaining - 1) * gap)) / 2;
79
+ }
80
+ const top = firstTop + row * topAdd;
81
+ const left = firstLeft + col * leftAdd;
82
+ col++;
83
+ if ((index + 1) % cols === 0) {
84
+ row++;
85
+ col = 0;
86
+ }
87
+ return { top, left };
88
+ }
89
+ return getPosition;
90
+ }
91
+ function createGrid({ aspectRatio, count, dimensions, gap }) {
92
+ const { width, height, rows, cols } = getGridItemDimensions({
93
+ aspectRatio,
94
+ count,
95
+ dimensions,
96
+ gap
97
+ });
98
+ const getPosition = createGridItemPositioner({
99
+ parentDimensions: dimensions,
100
+ dimensions: { width, height },
101
+ rows,
102
+ cols,
103
+ count,
104
+ gap
105
+ });
106
+ return {
107
+ width,
108
+ height,
109
+ rows,
110
+ cols,
111
+ getPosition
112
+ };
113
+ }
114
+ function createSidebarGrid(options) {
115
+ const { dimensions, gap, aspectRatio, count, sidebarPosition = "right", sidebarRatio = 0.25, pinnedIndex = 0 } = options;
116
+ if (count === 0) {
117
+ return createEmptyMeetGridResult("sidebar");
118
+ }
119
+ const { width: W, height: H } = dimensions;
120
+ const ratio = getAspectRatio(aspectRatio);
121
+ const isVertical = sidebarPosition === "bottom";
122
+ let mainWidth;
123
+ let mainHeight;
124
+ let sidebarWidth;
125
+ let sidebarHeight;
126
+ if (isVertical) {
127
+ mainHeight = H * (1 - sidebarRatio) - gap;
128
+ mainWidth = W - gap * 2;
129
+ sidebarHeight = H * sidebarRatio - gap;
130
+ sidebarWidth = W - gap * 2;
131
+ } else {
132
+ mainWidth = W * (1 - sidebarRatio) - gap * 2;
133
+ mainHeight = H - gap * 2;
134
+ sidebarWidth = W * sidebarRatio - gap;
135
+ sidebarHeight = H - gap * 2;
136
+ }
137
+ let mainItemWidth = mainWidth;
138
+ let mainItemHeight = mainItemWidth * ratio;
139
+ if (mainItemHeight > mainHeight) {
140
+ mainItemHeight = mainHeight;
141
+ mainItemWidth = mainItemHeight / ratio;
142
+ }
143
+ const sidebarCount = count - 1;
144
+ let thumbWidth;
145
+ let thumbHeight;
146
+ if (sidebarCount > 0) {
147
+ if (isVertical) {
148
+ thumbWidth = Math.min((sidebarWidth - (sidebarCount - 1) * gap) / sidebarCount, sidebarHeight / ratio);
149
+ thumbHeight = thumbWidth * ratio;
150
+ } else {
151
+ thumbHeight = Math.min((sidebarHeight - (sidebarCount - 1) * gap) / sidebarCount, sidebarWidth / ratio);
152
+ thumbWidth = thumbHeight / ratio;
153
+ }
154
+ } else {
155
+ thumbWidth = 0;
156
+ thumbHeight = 0;
157
+ }
158
+ const positions = [];
159
+ const mainLeft = isVertical ? gap + (mainWidth - mainItemWidth) / 2 : sidebarPosition === "left" ? sidebarWidth + gap * 2 + (mainWidth - mainItemWidth) / 2 : gap + (mainWidth - mainItemWidth) / 2;
160
+ const mainTop = isVertical ? gap + (mainHeight - mainItemHeight) / 2 : gap + (mainHeight - mainItemHeight) / 2;
161
+ positions[pinnedIndex] = {
162
+ position: { top: mainTop, left: mainLeft },
163
+ dimensions: { width: mainItemWidth, height: mainItemHeight }
164
+ };
165
+ let sidebarIndex = 0;
166
+ for (let i = 0; i < count; i++) {
167
+ if (i === pinnedIndex)
168
+ continue;
169
+ let left;
170
+ let top;
171
+ if (isVertical) {
172
+ const totalThumbWidth = sidebarCount * thumbWidth + (sidebarCount - 1) * gap;
173
+ const startLeft = gap + (sidebarWidth - totalThumbWidth) / 2;
174
+ left = startLeft + sidebarIndex * (thumbWidth + gap);
175
+ top = mainHeight + gap * 2;
176
+ } else {
177
+ left = sidebarPosition === "left" ? gap : mainWidth + gap * 2;
178
+ const totalThumbHeight = sidebarCount * thumbHeight + (sidebarCount - 1) * gap;
179
+ const startTop = gap + (sidebarHeight - totalThumbHeight) / 2;
180
+ top = startTop + sidebarIndex * (thumbHeight + gap);
181
+ }
182
+ positions[i] = {
183
+ position: { top, left },
184
+ dimensions: { width: thumbWidth, height: thumbHeight }
185
+ };
186
+ sidebarIndex++;
187
+ }
188
+ return {
189
+ width: mainItemWidth,
190
+ height: mainItemHeight,
191
+ rows: isVertical ? 2 : 1,
192
+ cols: isVertical ? 1 : 2,
193
+ layoutMode: "sidebar",
194
+ getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
195
+ getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
196
+ isMainItem: (index) => index === pinnedIndex
197
+ };
198
+ }
199
+ function createSpeakerGrid(options) {
200
+ const { dimensions, gap, aspectRatio, count, speakerIndex = 0 } = options;
201
+ if (count === 0) {
202
+ return createEmptyMeetGridResult("speaker");
203
+ }
204
+ if (count === 1) {
205
+ const grid = createGrid({ ...options, count: 1 });
206
+ return {
207
+ ...grid,
208
+ layoutMode: "speaker",
209
+ getItemDimensions: () => ({ width: grid.width, height: grid.height }),
210
+ isMainItem: () => true
211
+ };
212
+ }
213
+ const { width: W, height: H } = dimensions;
214
+ const ratio = getAspectRatio(aspectRatio);
215
+ const speakerHeight = (H - gap * 3) * 0.65;
216
+ const othersHeight = (H - gap * 3) * 0.35;
217
+ let speakerW = W - gap * 2;
218
+ let speakerH = speakerW * ratio;
219
+ if (speakerH > speakerHeight) {
220
+ speakerH = speakerHeight;
221
+ speakerW = speakerH / ratio;
222
+ }
223
+ const othersCount = count - 1;
224
+ let otherW = Math.min((W - gap * 2 - (othersCount - 1) * gap) / othersCount, othersHeight / ratio);
225
+ let otherH = otherW * ratio;
226
+ if (otherH > othersHeight) {
227
+ otherH = othersHeight;
228
+ otherW = otherH / ratio;
229
+ }
230
+ const positions = [];
231
+ positions[speakerIndex] = {
232
+ position: {
233
+ top: gap + (speakerHeight - speakerH) / 2,
234
+ left: gap + (W - gap * 2 - speakerW) / 2
235
+ },
236
+ dimensions: { width: speakerW, height: speakerH }
237
+ };
238
+ const totalOthersWidth = othersCount * otherW + (othersCount - 1) * gap;
239
+ const startLeft = gap + (W - gap * 2 - totalOthersWidth) / 2;
240
+ let otherIndex = 0;
241
+ for (let i = 0; i < count; i++) {
242
+ if (i === speakerIndex)
243
+ continue;
244
+ positions[i] = {
245
+ position: {
246
+ top: speakerHeight + gap * 2 + (othersHeight - otherH) / 2,
247
+ left: startLeft + otherIndex * (otherW + gap)
248
+ },
249
+ dimensions: { width: otherW, height: otherH }
250
+ };
251
+ otherIndex++;
252
+ }
253
+ return {
254
+ width: speakerW,
255
+ height: speakerH,
256
+ rows: 2,
257
+ cols: othersCount,
258
+ layoutMode: "speaker",
259
+ getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
260
+ getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
261
+ isMainItem: (index) => index === speakerIndex
262
+ };
263
+ }
264
+ function createSpotlightGrid(options) {
265
+ const { dimensions, gap, aspectRatio, pinnedIndex = 0 } = options;
266
+ const { width: W, height: H } = dimensions;
267
+ const ratio = getAspectRatio(aspectRatio);
268
+ let spotWidth = W - gap * 2;
269
+ let spotHeight = spotWidth * ratio;
270
+ if (spotHeight > H - gap * 2) {
271
+ spotHeight = H - gap * 2;
272
+ spotWidth = spotHeight / ratio;
273
+ }
274
+ const position = {
275
+ top: gap + (H - gap * 2 - spotHeight) / 2,
276
+ left: gap + (W - gap * 2 - spotWidth) / 2
277
+ };
278
+ return {
279
+ width: spotWidth,
280
+ height: spotHeight,
281
+ rows: 1,
282
+ cols: 1,
283
+ layoutMode: "spotlight",
284
+ getPosition: (index) => index === pinnedIndex ? position : { top: -9999, left: -9999 },
285
+ getItemDimensions: (index) => index === pinnedIndex ? { width: spotWidth, height: spotHeight } : { width: 0, height: 0 },
286
+ isMainItem: (index) => index === pinnedIndex
287
+ };
288
+ }
289
+ function createEmptyMeetGridResult(layoutMode) {
290
+ return {
291
+ width: 0,
292
+ height: 0,
293
+ rows: 0,
294
+ cols: 0,
295
+ layoutMode,
296
+ getPosition: () => ({ top: 0, left: 0 }),
297
+ getItemDimensions: () => ({ width: 0, height: 0 }),
298
+ isMainItem: () => false
299
+ };
300
+ }
301
+ function createMeetGrid(options) {
302
+ const { layoutMode = "gallery", count } = options;
303
+ if (count === 0) {
304
+ return createEmptyMeetGridResult(layoutMode);
305
+ }
306
+ switch (layoutMode) {
307
+ case "spotlight":
308
+ return createSpotlightGrid(options);
309
+ case "speaker":
310
+ return createSpeakerGrid(options);
311
+ case "sidebar":
312
+ return createSidebarGrid(options);
313
+ case "gallery":
314
+ default: {
315
+ const grid = createGrid(options);
316
+ return {
317
+ ...grid,
318
+ layoutMode: "gallery",
319
+ getItemDimensions: () => ({ width: grid.width, height: grid.height }),
320
+ isMainItem: () => false
321
+ };
322
+ }
323
+ }
324
+ }
325
+ const springPresets = {
326
+ /** Snappy animations for UI interactions */
327
+ snappy: { stiffness: 400, damping: 30 },
328
+ /** Smooth animations for layout changes */
329
+ smooth: { stiffness: 300, damping: 30 },
330
+ /** Gentle animations for subtle effects */
331
+ gentle: { stiffness: 200, damping: 25 },
332
+ /** Bouncy animations for playful effects */
333
+ bouncy: { stiffness: 400, damping: 15 }
334
+ };
335
+ function getSpringConfig(preset = "smooth") {
336
+ return {
337
+ type: "spring",
338
+ ...springPresets[preset]
339
+ };
340
+ }
341
+
342
+ exports.createGrid = createGrid;
343
+ exports.createGridItemPositioner = createGridItemPositioner;
344
+ exports.createMeetGrid = createMeetGrid;
345
+ exports.getAspectRatio = getAspectRatio;
346
+ exports.getGridItemDimensions = getGridItemDimensions;
347
+ exports.getSpringConfig = getSpringConfig;
348
+ exports.parseAspectRatio = parseAspectRatio;
349
+ exports.springPresets = springPresets;
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Dimensions of an element (width and height in pixels)
3
+ */
4
+ interface GridDimensions {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * Position of a grid item
10
+ */
11
+ interface Position {
12
+ top: number;
13
+ left: number;
14
+ }
15
+ /**
16
+ * Layout modes for the grid
17
+ * - gallery: Equal-sized tiles in a responsive grid
18
+ * - speaker: Active speaker takes larger space (2x size)
19
+ * - spotlight: Single participant in focus, others hidden
20
+ * - sidebar: Main view with thumbnail strip on the side
21
+ */
22
+ type LayoutMode = 'gallery' | 'speaker' | 'spotlight' | 'sidebar';
23
+ /**
24
+ * Options for creating a basic grid
25
+ */
26
+ interface GridOptions {
27
+ /** Aspect ratio in format "width:height" (e.g., "16:9", "4:3") */
28
+ aspectRatio: string;
29
+ /** Number of items in the grid */
30
+ count: number;
31
+ /** Container dimensions */
32
+ dimensions: GridDimensions;
33
+ /** Gap between items in pixels */
34
+ gap: number;
35
+ }
36
+ /**
37
+ * Extended options for meet-style grid with layout modes
38
+ */
39
+ interface MeetGridOptions extends GridOptions {
40
+ /** Layout mode for the grid */
41
+ layoutMode?: LayoutMode;
42
+ /** Index of pinned item (for speaker/spotlight modes) */
43
+ pinnedIndex?: number;
44
+ /** Index of active speaker */
45
+ speakerIndex?: number;
46
+ /** Sidebar position (for sidebar mode) */
47
+ sidebarPosition?: 'left' | 'right' | 'bottom';
48
+ /** Sidebar width ratio (0-1) */
49
+ sidebarRatio?: number;
50
+ }
51
+ /**
52
+ * Result from grid calculations
53
+ */
54
+ interface GridResult {
55
+ /** Width of each grid item */
56
+ width: number;
57
+ /** Height of each grid item */
58
+ height: number;
59
+ /** Number of rows */
60
+ rows: number;
61
+ /** Number of columns */
62
+ cols: number;
63
+ /** Function to get position of item at index */
64
+ getPosition: (index: number) => Position;
65
+ }
66
+ /**
67
+ * Extended result for meet-style grid
68
+ */
69
+ interface MeetGridResult extends GridResult {
70
+ /** Layout mode used */
71
+ layoutMode: LayoutMode;
72
+ /** Get item dimensions (may vary by index in some modes) */
73
+ getItemDimensions: (index: number) => GridDimensions;
74
+ /** Check if item is the main/featured item */
75
+ isMainItem: (index: number) => boolean;
76
+ }
77
+ /**
78
+ * Parses the Aspect Ratio string to actual ratio (height/width)
79
+ * @param ratio The aspect ratio in the format of "width:height" (e.g., "16:9")
80
+ * @returns The parsed value of aspect ratio (height/width)
81
+ */
82
+ declare function getAspectRatio(ratio: string): number;
83
+ /**
84
+ * Parse aspect ratio to get width/height multiplier
85
+ */
86
+ declare function parseAspectRatio(ratio: string): {
87
+ widthRatio: number;
88
+ heightRatio: number;
89
+ };
90
+ /**
91
+ * Calculates grid item dimensions for items that can fit in a container.
92
+ * Adapted from: https://stackoverflow.com/a/28268965
93
+ */
94
+ declare function getGridItemDimensions({ count, dimensions, aspectRatio, gap, }: GridOptions): {
95
+ width: number;
96
+ height: number;
97
+ rows: number;
98
+ cols: number;
99
+ };
100
+ /**
101
+ * Creates a utility function which helps you position grid items in a container.
102
+ */
103
+ declare function createGridItemPositioner({ parentDimensions, dimensions, rows, cols, count, gap, }: {
104
+ parentDimensions: GridDimensions;
105
+ dimensions: GridDimensions;
106
+ rows: number;
107
+ cols: number;
108
+ count: number;
109
+ gap: number;
110
+ }): (index: number) => Position;
111
+ /**
112
+ * Calculates data required for making a responsive grid.
113
+ */
114
+ declare function createGrid({ aspectRatio, count, dimensions, gap }: GridOptions): GridResult;
115
+ /**
116
+ * Create a meet-style grid with support for different layout modes.
117
+ * This is the main function for creating video conferencing-style layouts.
118
+ */
119
+ declare function createMeetGrid(options: MeetGridOptions): MeetGridResult;
120
+ /**
121
+ * Spring animation configuration presets
122
+ */
123
+ declare const springPresets: {
124
+ /** Snappy animations for UI interactions */
125
+ readonly snappy: {
126
+ readonly stiffness: 400;
127
+ readonly damping: 30;
128
+ };
129
+ /** Smooth animations for layout changes */
130
+ readonly smooth: {
131
+ readonly stiffness: 300;
132
+ readonly damping: 30;
133
+ };
134
+ /** Gentle animations for subtle effects */
135
+ readonly gentle: {
136
+ readonly stiffness: 200;
137
+ readonly damping: 25;
138
+ };
139
+ /** Bouncy animations for playful effects */
140
+ readonly bouncy: {
141
+ readonly stiffness: 400;
142
+ readonly damping: 15;
143
+ };
144
+ };
145
+ type SpringPreset = keyof typeof springPresets;
146
+ /**
147
+ * Get spring configuration for Motion animations
148
+ */
149
+ declare function getSpringConfig(preset?: SpringPreset): {
150
+ stiffness: 400;
151
+ damping: 30;
152
+ type: "spring";
153
+ } | {
154
+ stiffness: 300;
155
+ damping: 30;
156
+ type: "spring";
157
+ } | {
158
+ stiffness: 200;
159
+ damping: 25;
160
+ type: "spring";
161
+ } | {
162
+ stiffness: 400;
163
+ damping: 15;
164
+ type: "spring";
165
+ };
166
+
167
+ export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
168
+ export type { GridDimensions, GridOptions, GridResult, LayoutMode, MeetGridOptions, MeetGridResult, Position, SpringPreset };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Dimensions of an element (width and height in pixels)
3
+ */
4
+ interface GridDimensions {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * Position of a grid item
10
+ */
11
+ interface Position {
12
+ top: number;
13
+ left: number;
14
+ }
15
+ /**
16
+ * Layout modes for the grid
17
+ * - gallery: Equal-sized tiles in a responsive grid
18
+ * - speaker: Active speaker takes larger space (2x size)
19
+ * - spotlight: Single participant in focus, others hidden
20
+ * - sidebar: Main view with thumbnail strip on the side
21
+ */
22
+ type LayoutMode = 'gallery' | 'speaker' | 'spotlight' | 'sidebar';
23
+ /**
24
+ * Options for creating a basic grid
25
+ */
26
+ interface GridOptions {
27
+ /** Aspect ratio in format "width:height" (e.g., "16:9", "4:3") */
28
+ aspectRatio: string;
29
+ /** Number of items in the grid */
30
+ count: number;
31
+ /** Container dimensions */
32
+ dimensions: GridDimensions;
33
+ /** Gap between items in pixels */
34
+ gap: number;
35
+ }
36
+ /**
37
+ * Extended options for meet-style grid with layout modes
38
+ */
39
+ interface MeetGridOptions extends GridOptions {
40
+ /** Layout mode for the grid */
41
+ layoutMode?: LayoutMode;
42
+ /** Index of pinned item (for speaker/spotlight modes) */
43
+ pinnedIndex?: number;
44
+ /** Index of active speaker */
45
+ speakerIndex?: number;
46
+ /** Sidebar position (for sidebar mode) */
47
+ sidebarPosition?: 'left' | 'right' | 'bottom';
48
+ /** Sidebar width ratio (0-1) */
49
+ sidebarRatio?: number;
50
+ }
51
+ /**
52
+ * Result from grid calculations
53
+ */
54
+ interface GridResult {
55
+ /** Width of each grid item */
56
+ width: number;
57
+ /** Height of each grid item */
58
+ height: number;
59
+ /** Number of rows */
60
+ rows: number;
61
+ /** Number of columns */
62
+ cols: number;
63
+ /** Function to get position of item at index */
64
+ getPosition: (index: number) => Position;
65
+ }
66
+ /**
67
+ * Extended result for meet-style grid
68
+ */
69
+ interface MeetGridResult extends GridResult {
70
+ /** Layout mode used */
71
+ layoutMode: LayoutMode;
72
+ /** Get item dimensions (may vary by index in some modes) */
73
+ getItemDimensions: (index: number) => GridDimensions;
74
+ /** Check if item is the main/featured item */
75
+ isMainItem: (index: number) => boolean;
76
+ }
77
+ /**
78
+ * Parses the Aspect Ratio string to actual ratio (height/width)
79
+ * @param ratio The aspect ratio in the format of "width:height" (e.g., "16:9")
80
+ * @returns The parsed value of aspect ratio (height/width)
81
+ */
82
+ declare function getAspectRatio(ratio: string): number;
83
+ /**
84
+ * Parse aspect ratio to get width/height multiplier
85
+ */
86
+ declare function parseAspectRatio(ratio: string): {
87
+ widthRatio: number;
88
+ heightRatio: number;
89
+ };
90
+ /**
91
+ * Calculates grid item dimensions for items that can fit in a container.
92
+ * Adapted from: https://stackoverflow.com/a/28268965
93
+ */
94
+ declare function getGridItemDimensions({ count, dimensions, aspectRatio, gap, }: GridOptions): {
95
+ width: number;
96
+ height: number;
97
+ rows: number;
98
+ cols: number;
99
+ };
100
+ /**
101
+ * Creates a utility function which helps you position grid items in a container.
102
+ */
103
+ declare function createGridItemPositioner({ parentDimensions, dimensions, rows, cols, count, gap, }: {
104
+ parentDimensions: GridDimensions;
105
+ dimensions: GridDimensions;
106
+ rows: number;
107
+ cols: number;
108
+ count: number;
109
+ gap: number;
110
+ }): (index: number) => Position;
111
+ /**
112
+ * Calculates data required for making a responsive grid.
113
+ */
114
+ declare function createGrid({ aspectRatio, count, dimensions, gap }: GridOptions): GridResult;
115
+ /**
116
+ * Create a meet-style grid with support for different layout modes.
117
+ * This is the main function for creating video conferencing-style layouts.
118
+ */
119
+ declare function createMeetGrid(options: MeetGridOptions): MeetGridResult;
120
+ /**
121
+ * Spring animation configuration presets
122
+ */
123
+ declare const springPresets: {
124
+ /** Snappy animations for UI interactions */
125
+ readonly snappy: {
126
+ readonly stiffness: 400;
127
+ readonly damping: 30;
128
+ };
129
+ /** Smooth animations for layout changes */
130
+ readonly smooth: {
131
+ readonly stiffness: 300;
132
+ readonly damping: 30;
133
+ };
134
+ /** Gentle animations for subtle effects */
135
+ readonly gentle: {
136
+ readonly stiffness: 200;
137
+ readonly damping: 25;
138
+ };
139
+ /** Bouncy animations for playful effects */
140
+ readonly bouncy: {
141
+ readonly stiffness: 400;
142
+ readonly damping: 15;
143
+ };
144
+ };
145
+ type SpringPreset = keyof typeof springPresets;
146
+ /**
147
+ * Get spring configuration for Motion animations
148
+ */
149
+ declare function getSpringConfig(preset?: SpringPreset): {
150
+ stiffness: 400;
151
+ damping: 30;
152
+ type: "spring";
153
+ } | {
154
+ stiffness: 300;
155
+ damping: 30;
156
+ type: "spring";
157
+ } | {
158
+ stiffness: 200;
159
+ damping: 25;
160
+ type: "spring";
161
+ } | {
162
+ stiffness: 400;
163
+ damping: 15;
164
+ type: "spring";
165
+ };
166
+
167
+ export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
168
+ export type { GridDimensions, GridOptions, GridResult, LayoutMode, MeetGridOptions, MeetGridResult, Position, SpringPreset };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Dimensions of an element (width and height in pixels)
3
+ */
4
+ interface GridDimensions {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * Position of a grid item
10
+ */
11
+ interface Position {
12
+ top: number;
13
+ left: number;
14
+ }
15
+ /**
16
+ * Layout modes for the grid
17
+ * - gallery: Equal-sized tiles in a responsive grid
18
+ * - speaker: Active speaker takes larger space (2x size)
19
+ * - spotlight: Single participant in focus, others hidden
20
+ * - sidebar: Main view with thumbnail strip on the side
21
+ */
22
+ type LayoutMode = 'gallery' | 'speaker' | 'spotlight' | 'sidebar';
23
+ /**
24
+ * Options for creating a basic grid
25
+ */
26
+ interface GridOptions {
27
+ /** Aspect ratio in format "width:height" (e.g., "16:9", "4:3") */
28
+ aspectRatio: string;
29
+ /** Number of items in the grid */
30
+ count: number;
31
+ /** Container dimensions */
32
+ dimensions: GridDimensions;
33
+ /** Gap between items in pixels */
34
+ gap: number;
35
+ }
36
+ /**
37
+ * Extended options for meet-style grid with layout modes
38
+ */
39
+ interface MeetGridOptions extends GridOptions {
40
+ /** Layout mode for the grid */
41
+ layoutMode?: LayoutMode;
42
+ /** Index of pinned item (for speaker/spotlight modes) */
43
+ pinnedIndex?: number;
44
+ /** Index of active speaker */
45
+ speakerIndex?: number;
46
+ /** Sidebar position (for sidebar mode) */
47
+ sidebarPosition?: 'left' | 'right' | 'bottom';
48
+ /** Sidebar width ratio (0-1) */
49
+ sidebarRatio?: number;
50
+ }
51
+ /**
52
+ * Result from grid calculations
53
+ */
54
+ interface GridResult {
55
+ /** Width of each grid item */
56
+ width: number;
57
+ /** Height of each grid item */
58
+ height: number;
59
+ /** Number of rows */
60
+ rows: number;
61
+ /** Number of columns */
62
+ cols: number;
63
+ /** Function to get position of item at index */
64
+ getPosition: (index: number) => Position;
65
+ }
66
+ /**
67
+ * Extended result for meet-style grid
68
+ */
69
+ interface MeetGridResult extends GridResult {
70
+ /** Layout mode used */
71
+ layoutMode: LayoutMode;
72
+ /** Get item dimensions (may vary by index in some modes) */
73
+ getItemDimensions: (index: number) => GridDimensions;
74
+ /** Check if item is the main/featured item */
75
+ isMainItem: (index: number) => boolean;
76
+ }
77
+ /**
78
+ * Parses the Aspect Ratio string to actual ratio (height/width)
79
+ * @param ratio The aspect ratio in the format of "width:height" (e.g., "16:9")
80
+ * @returns The parsed value of aspect ratio (height/width)
81
+ */
82
+ declare function getAspectRatio(ratio: string): number;
83
+ /**
84
+ * Parse aspect ratio to get width/height multiplier
85
+ */
86
+ declare function parseAspectRatio(ratio: string): {
87
+ widthRatio: number;
88
+ heightRatio: number;
89
+ };
90
+ /**
91
+ * Calculates grid item dimensions for items that can fit in a container.
92
+ * Adapted from: https://stackoverflow.com/a/28268965
93
+ */
94
+ declare function getGridItemDimensions({ count, dimensions, aspectRatio, gap, }: GridOptions): {
95
+ width: number;
96
+ height: number;
97
+ rows: number;
98
+ cols: number;
99
+ };
100
+ /**
101
+ * Creates a utility function which helps you position grid items in a container.
102
+ */
103
+ declare function createGridItemPositioner({ parentDimensions, dimensions, rows, cols, count, gap, }: {
104
+ parentDimensions: GridDimensions;
105
+ dimensions: GridDimensions;
106
+ rows: number;
107
+ cols: number;
108
+ count: number;
109
+ gap: number;
110
+ }): (index: number) => Position;
111
+ /**
112
+ * Calculates data required for making a responsive grid.
113
+ */
114
+ declare function createGrid({ aspectRatio, count, dimensions, gap }: GridOptions): GridResult;
115
+ /**
116
+ * Create a meet-style grid with support for different layout modes.
117
+ * This is the main function for creating video conferencing-style layouts.
118
+ */
119
+ declare function createMeetGrid(options: MeetGridOptions): MeetGridResult;
120
+ /**
121
+ * Spring animation configuration presets
122
+ */
123
+ declare const springPresets: {
124
+ /** Snappy animations for UI interactions */
125
+ readonly snappy: {
126
+ readonly stiffness: 400;
127
+ readonly damping: 30;
128
+ };
129
+ /** Smooth animations for layout changes */
130
+ readonly smooth: {
131
+ readonly stiffness: 300;
132
+ readonly damping: 30;
133
+ };
134
+ /** Gentle animations for subtle effects */
135
+ readonly gentle: {
136
+ readonly stiffness: 200;
137
+ readonly damping: 25;
138
+ };
139
+ /** Bouncy animations for playful effects */
140
+ readonly bouncy: {
141
+ readonly stiffness: 400;
142
+ readonly damping: 15;
143
+ };
144
+ };
145
+ type SpringPreset = keyof typeof springPresets;
146
+ /**
147
+ * Get spring configuration for Motion animations
148
+ */
149
+ declare function getSpringConfig(preset?: SpringPreset): {
150
+ stiffness: 400;
151
+ damping: 30;
152
+ type: "spring";
153
+ } | {
154
+ stiffness: 300;
155
+ damping: 30;
156
+ type: "spring";
157
+ } | {
158
+ stiffness: 200;
159
+ damping: 25;
160
+ type: "spring";
161
+ } | {
162
+ stiffness: 400;
163
+ damping: 15;
164
+ type: "spring";
165
+ };
166
+
167
+ export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
168
+ export type { GridDimensions, GridOptions, GridResult, LayoutMode, MeetGridOptions, MeetGridResult, Position, SpringPreset };
package/dist/index.mjs ADDED
@@ -0,0 +1,340 @@
1
+ function getAspectRatio(ratio) {
2
+ const [width, height] = ratio.split(":");
3
+ if (!width || !height) {
4
+ throw new Error(
5
+ 'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
6
+ );
7
+ }
8
+ return Number.parseInt(height) / Number.parseInt(width);
9
+ }
10
+ function parseAspectRatio(ratio) {
11
+ const [width, height] = ratio.split(":").map(Number);
12
+ if (!width || !height || isNaN(width) || isNaN(height)) {
13
+ throw new Error(
14
+ 'meet-layout-grid: Invalid aspect ratio provided, expected format is "width:height".'
15
+ );
16
+ }
17
+ return { widthRatio: width, heightRatio: height };
18
+ }
19
+ function getGridItemDimensions({
20
+ count,
21
+ dimensions,
22
+ aspectRatio,
23
+ gap
24
+ }) {
25
+ let { width: W, height: H } = dimensions;
26
+ if (W === 0 || H === 0 || count === 0) {
27
+ return { width: 0, height: 0, rows: 1, cols: 1 };
28
+ }
29
+ W -= gap * 2;
30
+ H -= gap * 2;
31
+ const s = gap;
32
+ const N = count;
33
+ const r = getAspectRatio(aspectRatio);
34
+ let w = 0;
35
+ let h = 0;
36
+ let a = 1;
37
+ let b = 1;
38
+ const widths = [];
39
+ for (let n = 1; n <= N; n++) {
40
+ widths.push((W - s * (n - 1)) / n, (H - s * (n - 1)) / (n * r));
41
+ }
42
+ widths.sort((a2, b2) => b2 - a2);
43
+ for (const width of widths) {
44
+ w = width;
45
+ h = w * r;
46
+ a = Math.floor((W + s) / (w + s));
47
+ b = Math.floor((H + s) / (h + s));
48
+ if (a * b >= N) {
49
+ a = Math.ceil(N / b);
50
+ b = Math.ceil(N / a);
51
+ break;
52
+ }
53
+ }
54
+ return { width: w, height: h, rows: b, cols: a };
55
+ }
56
+ function createGridItemPositioner({
57
+ parentDimensions,
58
+ dimensions,
59
+ rows,
60
+ cols,
61
+ count,
62
+ gap
63
+ }) {
64
+ const { width: W, height: H } = parentDimensions;
65
+ const { width: w, height: h } = dimensions;
66
+ const firstTop = (H - (h * rows + (rows - 1) * gap)) / 2;
67
+ let firstLeft = (W - (w * cols + (cols - 1) * gap)) / 2;
68
+ const topAdd = h + gap;
69
+ const leftAdd = w + gap;
70
+ let col = 0;
71
+ let row = 0;
72
+ const incompleteRowCols = count % cols;
73
+ function getPosition(index) {
74
+ const remaining = count - index;
75
+ if (remaining === incompleteRowCols && incompleteRowCols > 0) {
76
+ firstLeft = (W - (w * remaining + (remaining - 1) * gap)) / 2;
77
+ }
78
+ const top = firstTop + row * topAdd;
79
+ const left = firstLeft + col * leftAdd;
80
+ col++;
81
+ if ((index + 1) % cols === 0) {
82
+ row++;
83
+ col = 0;
84
+ }
85
+ return { top, left };
86
+ }
87
+ return getPosition;
88
+ }
89
+ function createGrid({ aspectRatio, count, dimensions, gap }) {
90
+ const { width, height, rows, cols } = getGridItemDimensions({
91
+ aspectRatio,
92
+ count,
93
+ dimensions,
94
+ gap
95
+ });
96
+ const getPosition = createGridItemPositioner({
97
+ parentDimensions: dimensions,
98
+ dimensions: { width, height },
99
+ rows,
100
+ cols,
101
+ count,
102
+ gap
103
+ });
104
+ return {
105
+ width,
106
+ height,
107
+ rows,
108
+ cols,
109
+ getPosition
110
+ };
111
+ }
112
+ function createSidebarGrid(options) {
113
+ const { dimensions, gap, aspectRatio, count, sidebarPosition = "right", sidebarRatio = 0.25, pinnedIndex = 0 } = options;
114
+ if (count === 0) {
115
+ return createEmptyMeetGridResult("sidebar");
116
+ }
117
+ const { width: W, height: H } = dimensions;
118
+ const ratio = getAspectRatio(aspectRatio);
119
+ const isVertical = sidebarPosition === "bottom";
120
+ let mainWidth;
121
+ let mainHeight;
122
+ let sidebarWidth;
123
+ let sidebarHeight;
124
+ if (isVertical) {
125
+ mainHeight = H * (1 - sidebarRatio) - gap;
126
+ mainWidth = W - gap * 2;
127
+ sidebarHeight = H * sidebarRatio - gap;
128
+ sidebarWidth = W - gap * 2;
129
+ } else {
130
+ mainWidth = W * (1 - sidebarRatio) - gap * 2;
131
+ mainHeight = H - gap * 2;
132
+ sidebarWidth = W * sidebarRatio - gap;
133
+ sidebarHeight = H - gap * 2;
134
+ }
135
+ let mainItemWidth = mainWidth;
136
+ let mainItemHeight = mainItemWidth * ratio;
137
+ if (mainItemHeight > mainHeight) {
138
+ mainItemHeight = mainHeight;
139
+ mainItemWidth = mainItemHeight / ratio;
140
+ }
141
+ const sidebarCount = count - 1;
142
+ let thumbWidth;
143
+ let thumbHeight;
144
+ if (sidebarCount > 0) {
145
+ if (isVertical) {
146
+ thumbWidth = Math.min((sidebarWidth - (sidebarCount - 1) * gap) / sidebarCount, sidebarHeight / ratio);
147
+ thumbHeight = thumbWidth * ratio;
148
+ } else {
149
+ thumbHeight = Math.min((sidebarHeight - (sidebarCount - 1) * gap) / sidebarCount, sidebarWidth / ratio);
150
+ thumbWidth = thumbHeight / ratio;
151
+ }
152
+ } else {
153
+ thumbWidth = 0;
154
+ thumbHeight = 0;
155
+ }
156
+ const positions = [];
157
+ const mainLeft = isVertical ? gap + (mainWidth - mainItemWidth) / 2 : sidebarPosition === "left" ? sidebarWidth + gap * 2 + (mainWidth - mainItemWidth) / 2 : gap + (mainWidth - mainItemWidth) / 2;
158
+ const mainTop = isVertical ? gap + (mainHeight - mainItemHeight) / 2 : gap + (mainHeight - mainItemHeight) / 2;
159
+ positions[pinnedIndex] = {
160
+ position: { top: mainTop, left: mainLeft },
161
+ dimensions: { width: mainItemWidth, height: mainItemHeight }
162
+ };
163
+ let sidebarIndex = 0;
164
+ for (let i = 0; i < count; i++) {
165
+ if (i === pinnedIndex)
166
+ continue;
167
+ let left;
168
+ let top;
169
+ if (isVertical) {
170
+ const totalThumbWidth = sidebarCount * thumbWidth + (sidebarCount - 1) * gap;
171
+ const startLeft = gap + (sidebarWidth - totalThumbWidth) / 2;
172
+ left = startLeft + sidebarIndex * (thumbWidth + gap);
173
+ top = mainHeight + gap * 2;
174
+ } else {
175
+ left = sidebarPosition === "left" ? gap : mainWidth + gap * 2;
176
+ const totalThumbHeight = sidebarCount * thumbHeight + (sidebarCount - 1) * gap;
177
+ const startTop = gap + (sidebarHeight - totalThumbHeight) / 2;
178
+ top = startTop + sidebarIndex * (thumbHeight + gap);
179
+ }
180
+ positions[i] = {
181
+ position: { top, left },
182
+ dimensions: { width: thumbWidth, height: thumbHeight }
183
+ };
184
+ sidebarIndex++;
185
+ }
186
+ return {
187
+ width: mainItemWidth,
188
+ height: mainItemHeight,
189
+ rows: isVertical ? 2 : 1,
190
+ cols: isVertical ? 1 : 2,
191
+ layoutMode: "sidebar",
192
+ getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
193
+ getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
194
+ isMainItem: (index) => index === pinnedIndex
195
+ };
196
+ }
197
+ function createSpeakerGrid(options) {
198
+ const { dimensions, gap, aspectRatio, count, speakerIndex = 0 } = options;
199
+ if (count === 0) {
200
+ return createEmptyMeetGridResult("speaker");
201
+ }
202
+ if (count === 1) {
203
+ const grid = createGrid({ ...options, count: 1 });
204
+ return {
205
+ ...grid,
206
+ layoutMode: "speaker",
207
+ getItemDimensions: () => ({ width: grid.width, height: grid.height }),
208
+ isMainItem: () => true
209
+ };
210
+ }
211
+ const { width: W, height: H } = dimensions;
212
+ const ratio = getAspectRatio(aspectRatio);
213
+ const speakerHeight = (H - gap * 3) * 0.65;
214
+ const othersHeight = (H - gap * 3) * 0.35;
215
+ let speakerW = W - gap * 2;
216
+ let speakerH = speakerW * ratio;
217
+ if (speakerH > speakerHeight) {
218
+ speakerH = speakerHeight;
219
+ speakerW = speakerH / ratio;
220
+ }
221
+ const othersCount = count - 1;
222
+ let otherW = Math.min((W - gap * 2 - (othersCount - 1) * gap) / othersCount, othersHeight / ratio);
223
+ let otherH = otherW * ratio;
224
+ if (otherH > othersHeight) {
225
+ otherH = othersHeight;
226
+ otherW = otherH / ratio;
227
+ }
228
+ const positions = [];
229
+ positions[speakerIndex] = {
230
+ position: {
231
+ top: gap + (speakerHeight - speakerH) / 2,
232
+ left: gap + (W - gap * 2 - speakerW) / 2
233
+ },
234
+ dimensions: { width: speakerW, height: speakerH }
235
+ };
236
+ const totalOthersWidth = othersCount * otherW + (othersCount - 1) * gap;
237
+ const startLeft = gap + (W - gap * 2 - totalOthersWidth) / 2;
238
+ let otherIndex = 0;
239
+ for (let i = 0; i < count; i++) {
240
+ if (i === speakerIndex)
241
+ continue;
242
+ positions[i] = {
243
+ position: {
244
+ top: speakerHeight + gap * 2 + (othersHeight - otherH) / 2,
245
+ left: startLeft + otherIndex * (otherW + gap)
246
+ },
247
+ dimensions: { width: otherW, height: otherH }
248
+ };
249
+ otherIndex++;
250
+ }
251
+ return {
252
+ width: speakerW,
253
+ height: speakerH,
254
+ rows: 2,
255
+ cols: othersCount,
256
+ layoutMode: "speaker",
257
+ getPosition: (index) => positions[index]?.position ?? { top: 0, left: 0 },
258
+ getItemDimensions: (index) => positions[index]?.dimensions ?? { width: 0, height: 0 },
259
+ isMainItem: (index) => index === speakerIndex
260
+ };
261
+ }
262
+ function createSpotlightGrid(options) {
263
+ const { dimensions, gap, aspectRatio, pinnedIndex = 0 } = options;
264
+ const { width: W, height: H } = dimensions;
265
+ const ratio = getAspectRatio(aspectRatio);
266
+ let spotWidth = W - gap * 2;
267
+ let spotHeight = spotWidth * ratio;
268
+ if (spotHeight > H - gap * 2) {
269
+ spotHeight = H - gap * 2;
270
+ spotWidth = spotHeight / ratio;
271
+ }
272
+ const position = {
273
+ top: gap + (H - gap * 2 - spotHeight) / 2,
274
+ left: gap + (W - gap * 2 - spotWidth) / 2
275
+ };
276
+ return {
277
+ width: spotWidth,
278
+ height: spotHeight,
279
+ rows: 1,
280
+ cols: 1,
281
+ layoutMode: "spotlight",
282
+ getPosition: (index) => index === pinnedIndex ? position : { top: -9999, left: -9999 },
283
+ getItemDimensions: (index) => index === pinnedIndex ? { width: spotWidth, height: spotHeight } : { width: 0, height: 0 },
284
+ isMainItem: (index) => index === pinnedIndex
285
+ };
286
+ }
287
+ function createEmptyMeetGridResult(layoutMode) {
288
+ return {
289
+ width: 0,
290
+ height: 0,
291
+ rows: 0,
292
+ cols: 0,
293
+ layoutMode,
294
+ getPosition: () => ({ top: 0, left: 0 }),
295
+ getItemDimensions: () => ({ width: 0, height: 0 }),
296
+ isMainItem: () => false
297
+ };
298
+ }
299
+ function createMeetGrid(options) {
300
+ const { layoutMode = "gallery", count } = options;
301
+ if (count === 0) {
302
+ return createEmptyMeetGridResult(layoutMode);
303
+ }
304
+ switch (layoutMode) {
305
+ case "spotlight":
306
+ return createSpotlightGrid(options);
307
+ case "speaker":
308
+ return createSpeakerGrid(options);
309
+ case "sidebar":
310
+ return createSidebarGrid(options);
311
+ case "gallery":
312
+ default: {
313
+ const grid = createGrid(options);
314
+ return {
315
+ ...grid,
316
+ layoutMode: "gallery",
317
+ getItemDimensions: () => ({ width: grid.width, height: grid.height }),
318
+ isMainItem: () => false
319
+ };
320
+ }
321
+ }
322
+ }
323
+ const springPresets = {
324
+ /** Snappy animations for UI interactions */
325
+ snappy: { stiffness: 400, damping: 30 },
326
+ /** Smooth animations for layout changes */
327
+ smooth: { stiffness: 300, damping: 30 },
328
+ /** Gentle animations for subtle effects */
329
+ gentle: { stiffness: 200, damping: 25 },
330
+ /** Bouncy animations for playful effects */
331
+ bouncy: { stiffness: 400, damping: 15 }
332
+ };
333
+ function getSpringConfig(preset = "smooth") {
334
+ return {
335
+ type: "spring",
336
+ ...springPresets[preset]
337
+ };
338
+ }
339
+
340
+ export { createGrid, createGridItemPositioner, createMeetGrid, getAspectRatio, getGridItemDimensions, getSpringConfig, parseAspectRatio, springPresets };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@thangdevalone/meet-layout-grid-core",
3
+ "version": "1.0.0",
4
+ "description": "Core grid calculation logic for meet-layout-grid",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "unbuild",
26
+ "dev": "unbuild --watch",
27
+ "clean": "rimraf dist",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest"
30
+ },
31
+ "keywords": [
32
+ "grid",
33
+ "responsive",
34
+ "meeting",
35
+ "video",
36
+ "layout"
37
+ ],
38
+ "author": "ThangDevAlone",
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/thangdevalone/meet-layout-grid"
43
+ },
44
+ "devDependencies": {
45
+ "unbuild": "^2.0.0",
46
+ "vitest": "^1.0.0",
47
+ "rimraf": "^5.0.0"
48
+ }
49
+ }