social-masonry 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.
@@ -0,0 +1,730 @@
1
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
+ import { forwardRef, useRef, useState, useMemo, useCallback, useEffect, useImperativeHandle } from 'react';
3
+
4
+ /**
5
+ * Social Masonry - Utility Functions
6
+ */
7
+ /**
8
+ * Debounce function execution
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ function debounce(fn, delay) {
12
+ let timeoutId = null;
13
+ return function (...args) {
14
+ if (timeoutId) {
15
+ clearTimeout(timeoutId);
16
+ }
17
+ timeoutId = setTimeout(() => {
18
+ fn.apply(this, args);
19
+ timeoutId = null;
20
+ }, delay);
21
+ };
22
+ }
23
+ /**
24
+ * Throttle function execution
25
+ */
26
+ function throttle(fn, limit) {
27
+ let inThrottle = false;
28
+ let lastArgs = null;
29
+ return function (...args) {
30
+ if (!inThrottle) {
31
+ fn.apply(this, args);
32
+ inThrottle = true;
33
+ setTimeout(() => {
34
+ inThrottle = false;
35
+ if (lastArgs) {
36
+ fn.apply(this, lastArgs);
37
+ lastArgs = null;
38
+ }
39
+ }, limit);
40
+ }
41
+ else {
42
+ lastArgs = args;
43
+ }
44
+ };
45
+ }
46
+ /**
47
+ * Get number of columns based on viewport width
48
+ */
49
+ function getColumnCount(columns, containerWidth) {
50
+ if (typeof columns === 'number') {
51
+ return columns;
52
+ }
53
+ // Sort by minWidth descending
54
+ const sorted = [...columns].sort((a, b) => b.minWidth - a.minWidth);
55
+ for (const config of sorted) {
56
+ if (containerWidth >= config.minWidth) {
57
+ return config.columns;
58
+ }
59
+ }
60
+ // Return smallest breakpoint's columns or default to 1
61
+ return sorted[sorted.length - 1]?.columns ?? 1;
62
+ }
63
+ /**
64
+ * Default responsive column configuration
65
+ */
66
+ const defaultColumnConfig = [
67
+ { columns: 4, minWidth: 1200 },
68
+ { columns: 3, minWidth: 900 },
69
+ { columns: 2, minWidth: 600 },
70
+ { columns: 1, minWidth: 0 },
71
+ ];
72
+ /**
73
+ * Format number with abbreviations (1K, 1M, etc.)
74
+ */
75
+ function formatNumber(num) {
76
+ if (num >= 1000000) {
77
+ return `${(num / 1000000).toFixed(1).replace(/\.0$/, '')}M`;
78
+ }
79
+ if (num >= 1000) {
80
+ return `${(num / 1000).toFixed(1).replace(/\.0$/, '')}K`;
81
+ }
82
+ return num.toString();
83
+ }
84
+ /**
85
+ * Format relative time
86
+ */
87
+ function formatRelativeTime(date) {
88
+ const now = new Date();
89
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
90
+ if (diffInSeconds < 60) {
91
+ return 'now';
92
+ }
93
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
94
+ if (diffInMinutes < 60) {
95
+ return `${diffInMinutes}m`;
96
+ }
97
+ const diffInHours = Math.floor(diffInMinutes / 60);
98
+ if (diffInHours < 24) {
99
+ return `${diffInHours}h`;
100
+ }
101
+ const diffInDays = Math.floor(diffInHours / 24);
102
+ if (diffInDays < 7) {
103
+ return `${diffInDays}d`;
104
+ }
105
+ const diffInWeeks = Math.floor(diffInDays / 7);
106
+ if (diffInWeeks < 4) {
107
+ return `${diffInWeeks}w`;
108
+ }
109
+ // Format as date for older posts
110
+ return date.toLocaleDateString('en-US', {
111
+ month: 'short',
112
+ day: 'numeric',
113
+ });
114
+ }
115
+ /**
116
+ * Parse CSS value to pixels
117
+ */
118
+ function parseCSSValue(value, containerWidth) {
119
+ if (typeof value === 'number') {
120
+ return value;
121
+ }
122
+ const numMatch = value.match(/^([\d.]+)(px|rem|em|%|vw)?$/);
123
+ if (!numMatch) {
124
+ return 0;
125
+ }
126
+ const num = parseFloat(numMatch[1]);
127
+ const unit = numMatch[2] || 'px';
128
+ switch (unit) {
129
+ case 'px':
130
+ return num;
131
+ case 'rem':
132
+ return num * 16; // Assume 16px base
133
+ case 'em':
134
+ return num * 16;
135
+ case '%':
136
+ return 0;
137
+ case 'vw':
138
+ return (num / 100) * window.innerWidth;
139
+ default:
140
+ return num;
141
+ }
142
+ }
143
+ /**
144
+ * Check if element is in viewport
145
+ */
146
+ function isInViewport(element, scrollTop, viewportHeight, overscan = 0) {
147
+ const expandedTop = scrollTop - overscan;
148
+ const expandedBottom = scrollTop + viewportHeight + overscan;
149
+ return element.bottom >= expandedTop && element.top <= expandedBottom;
150
+ }
151
+ /**
152
+ * Get scroll position
153
+ */
154
+ function getScrollPosition(scrollContainer) {
155
+ if (!scrollContainer || scrollContainer === window) {
156
+ return {
157
+ scrollTop: window.scrollY || document.documentElement.scrollTop,
158
+ viewportHeight: window.innerHeight,
159
+ };
160
+ }
161
+ return {
162
+ scrollTop: scrollContainer.scrollTop,
163
+ viewportHeight: scrollContainer.clientHeight,
164
+ };
165
+ }
166
+ /**
167
+ * Type guard for Twitter posts
168
+ */
169
+ function isTwitterPost(post) {
170
+ return post.platform === 'twitter';
171
+ }
172
+ /**
173
+ * Type guard for Instagram posts
174
+ */
175
+ function isInstagramPost(post) {
176
+ return post.platform === 'instagram';
177
+ }
178
+ /**
179
+ * Request animation frame with fallback
180
+ */
181
+ const raf = typeof requestAnimationFrame !== 'undefined'
182
+ ? requestAnimationFrame
183
+ : (callback) => setTimeout(callback, 16);
184
+ /**
185
+ * Cancel animation frame with fallback
186
+ */
187
+ const cancelRaf = typeof cancelAnimationFrame !== 'undefined'
188
+ ? cancelAnimationFrame
189
+ : (id) => clearTimeout(id);
190
+
191
+ /**
192
+ * Social Masonry - Layout Engine
193
+ * Calculates positions for masonry grid items
194
+ */
195
+ class LayoutEngine {
196
+ constructor(options) {
197
+ this.options = {
198
+ gap: 16,
199
+ columns: defaultColumnConfig,
200
+ defaultColumns: 3,
201
+ padding: 0,
202
+ animationDuration: 300,
203
+ animate: true,
204
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
205
+ ...options,
206
+ };
207
+ this.state = {
208
+ positions: new Map(),
209
+ columnHeights: [],
210
+ containerHeight: 0,
211
+ columnWidth: 0,
212
+ };
213
+ }
214
+ /**
215
+ * Calculate layout for all posts
216
+ */
217
+ calculate(posts) {
218
+ const { containerWidth, itemHeights } = this.options;
219
+ const gap = parseCSSValue(this.options.gap);
220
+ const padding = parseCSSValue(this.options.padding);
221
+ // Calculate column count and width
222
+ const columnCount = getColumnCount(this.options.columns, containerWidth);
223
+ const availableWidth = containerWidth - padding * 2;
224
+ const totalGapWidth = gap * (columnCount - 1);
225
+ const columnWidth = (availableWidth - totalGapWidth) / columnCount;
226
+ // Initialize column heights
227
+ const columnHeights = new Array(columnCount).fill(0);
228
+ const positions = new Map();
229
+ // Place each item in the shortest column
230
+ for (const post of posts) {
231
+ const itemHeight = itemHeights.get(post.id) ?? this.estimateHeight(post, columnWidth);
232
+ // Find shortest column
233
+ const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
234
+ // Calculate position
235
+ const x = padding + shortestColumn * (columnWidth + gap);
236
+ const y = columnHeights[shortestColumn];
237
+ positions.set(post.id, {
238
+ id: post.id,
239
+ x,
240
+ y,
241
+ width: columnWidth,
242
+ height: itemHeight,
243
+ column: shortestColumn,
244
+ });
245
+ // Update column height
246
+ columnHeights[shortestColumn] = y + itemHeight + gap;
247
+ }
248
+ // Remove last gap from column heights
249
+ const containerHeight = Math.max(...columnHeights.map(h => h - gap), 0);
250
+ this.state = {
251
+ positions,
252
+ columnHeights,
253
+ containerHeight,
254
+ columnWidth,
255
+ };
256
+ return this.state;
257
+ }
258
+ /**
259
+ * Estimate item height based on content
260
+ */
261
+ estimateHeight(post, columnWidth) {
262
+ let height = 0;
263
+ // Header (avatar, name, etc.)
264
+ height += 56;
265
+ // Text content
266
+ if (post.platform === 'twitter') {
267
+ const textLength = post.content.text.length;
268
+ const avgCharsPerLine = Math.floor(columnWidth / 8); // ~8px per char
269
+ const lines = Math.ceil(textLength / avgCharsPerLine);
270
+ height += lines * 24; // ~24px per line
271
+ }
272
+ else if (post.content.caption) {
273
+ const captionLength = post.content.caption.length;
274
+ const avgCharsPerLine = Math.floor(columnWidth / 8);
275
+ const lines = Math.min(Math.ceil(captionLength / avgCharsPerLine), 4); // Max 4 lines
276
+ height += lines * 20;
277
+ }
278
+ // Media
279
+ if (post.platform === 'twitter' && post.media?.length) {
280
+ const media = post.media[0];
281
+ const aspectRatio = media.aspectRatio ?? 16 / 9;
282
+ height += columnWidth / aspectRatio;
283
+ }
284
+ else if (post.platform === 'instagram') {
285
+ const aspectRatio = post.media.aspectRatio ?? 1;
286
+ height += columnWidth / aspectRatio;
287
+ }
288
+ // Footer (metrics, timestamp)
289
+ height += 44;
290
+ // Padding
291
+ height += 24;
292
+ return Math.round(height);
293
+ }
294
+ /**
295
+ * Update single item height and recalculate affected items
296
+ */
297
+ updateItemHeight(id, height) {
298
+ this.options.itemHeights.set(id, height);
299
+ }
300
+ /**
301
+ * Get current layout state
302
+ */
303
+ getState() {
304
+ return this.state;
305
+ }
306
+ /**
307
+ * Get position for specific item
308
+ */
309
+ getPosition(id) {
310
+ return this.state.positions.get(id);
311
+ }
312
+ /**
313
+ * Update container width
314
+ */
315
+ setContainerWidth(width) {
316
+ this.options.containerWidth = width;
317
+ }
318
+ /**
319
+ * Get current column count
320
+ */
321
+ getColumnCount() {
322
+ return getColumnCount(this.options.columns, this.options.containerWidth);
323
+ }
324
+ /**
325
+ * Get CSS variables for animations
326
+ */
327
+ getCSSVariables() {
328
+ return {
329
+ '--sm-animation-duration': `${this.options.animationDuration}ms`,
330
+ '--sm-easing': this.options.easing,
331
+ '--sm-gap': typeof this.options.gap === 'number'
332
+ ? `${this.options.gap}px`
333
+ : this.options.gap,
334
+ '--sm-column-width': `${this.state.columnWidth}px`,
335
+ '--sm-container-height': `${this.state.containerHeight}px`,
336
+ };
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Social Masonry - Virtualization Engine
342
+ * Handles virtual scrolling for large lists
343
+ */
344
+ class VirtualizationEngine {
345
+ constructor(options) {
346
+ this.visibleItems = [];
347
+ this.rafId = null;
348
+ this.lastScrollTop = 0;
349
+ this.isScrolling = false;
350
+ this.scrollEndTimeout = null;
351
+ this.options = {
352
+ enabled: true,
353
+ overscan: 3,
354
+ estimatedItemHeight: 400,
355
+ scrollContainer: null,
356
+ ...options,
357
+ };
358
+ this.posts = options.posts;
359
+ this.positions = options.positions;
360
+ this.onVisibleItemsChange = options.onVisibleItemsChange;
361
+ this.scrollHandler = throttle(this.handleScroll.bind(this), 16);
362
+ }
363
+ /**
364
+ * Initialize scroll listener
365
+ */
366
+ init() {
367
+ if (!this.options.enabled)
368
+ return;
369
+ const container = this.options.scrollContainer ?? window;
370
+ container.addEventListener('scroll', this.scrollHandler, { passive: true });
371
+ // Initial calculation
372
+ this.calculateVisibleItems();
373
+ }
374
+ /**
375
+ * Destroy and cleanup
376
+ */
377
+ destroy() {
378
+ const container = this.options.scrollContainer ?? window;
379
+ container.removeEventListener('scroll', this.scrollHandler);
380
+ if (this.rafId !== null) {
381
+ cancelRaf(this.rafId);
382
+ }
383
+ if (this.scrollEndTimeout) {
384
+ clearTimeout(this.scrollEndTimeout);
385
+ }
386
+ }
387
+ /**
388
+ * Handle scroll event
389
+ */
390
+ handleScroll() {
391
+ if (this.rafId !== null)
392
+ return;
393
+ this.isScrolling = true;
394
+ // Clear existing scroll end timeout
395
+ if (this.scrollEndTimeout) {
396
+ clearTimeout(this.scrollEndTimeout);
397
+ }
398
+ // Set scroll end detection
399
+ this.scrollEndTimeout = setTimeout(() => {
400
+ this.isScrolling = false;
401
+ }, 150);
402
+ this.rafId = raf(() => {
403
+ this.rafId = null;
404
+ this.calculateVisibleItems();
405
+ });
406
+ }
407
+ /**
408
+ * Calculate which items are visible
409
+ */
410
+ calculateVisibleItems() {
411
+ const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
412
+ const overscanPx = this.options.overscan * this.options.estimatedItemHeight;
413
+ this.lastScrollTop = scrollTop;
414
+ const newVisibleItems = [];
415
+ for (let i = 0; i < this.posts.length; i++) {
416
+ const post = this.posts[i];
417
+ const position = this.positions.get(post.id);
418
+ if (!position)
419
+ continue;
420
+ const isVisible = isInViewport({
421
+ top: position.y,
422
+ bottom: position.y + position.height,
423
+ }, scrollTop, viewportHeight, overscanPx);
424
+ if (isVisible) {
425
+ newVisibleItems.push({
426
+ index: i,
427
+ post,
428
+ position,
429
+ isVisible: true,
430
+ });
431
+ }
432
+ }
433
+ // Check if visible items changed
434
+ const hasChanged = this.hasVisibleItemsChanged(newVisibleItems);
435
+ if (hasChanged) {
436
+ this.visibleItems = newVisibleItems;
437
+ this.onVisibleItemsChange?.(newVisibleItems);
438
+ }
439
+ return this.visibleItems;
440
+ }
441
+ /**
442
+ * Check if visible items have changed
443
+ */
444
+ hasVisibleItemsChanged(newItems) {
445
+ if (newItems.length !== this.visibleItems.length) {
446
+ return true;
447
+ }
448
+ for (let i = 0; i < newItems.length; i++) {
449
+ if (newItems[i].post.id !== this.visibleItems[i].post.id) {
450
+ return true;
451
+ }
452
+ }
453
+ return false;
454
+ }
455
+ /**
456
+ * Update posts and positions
457
+ */
458
+ update(posts, positions) {
459
+ this.posts = posts;
460
+ this.positions = positions;
461
+ this.calculateVisibleItems();
462
+ }
463
+ /**
464
+ * Get visible items
465
+ */
466
+ getVisibleItems() {
467
+ return this.visibleItems;
468
+ }
469
+ /**
470
+ * Get all items (for non-virtualized mode)
471
+ */
472
+ getAllItems() {
473
+ return this.posts.map((post, index) => ({
474
+ index,
475
+ post,
476
+ position: this.positions.get(post.id),
477
+ isVisible: true,
478
+ }));
479
+ }
480
+ /**
481
+ * Check if scrolling
482
+ */
483
+ getIsScrolling() {
484
+ return this.isScrolling;
485
+ }
486
+ /**
487
+ * Get scroll direction
488
+ */
489
+ getScrollDirection() {
490
+ const { scrollTop } = getScrollPosition(this.options.scrollContainer);
491
+ if (scrollTop > this.lastScrollTop) {
492
+ return 'down';
493
+ }
494
+ else if (scrollTop < this.lastScrollTop) {
495
+ return 'up';
496
+ }
497
+ return 'none';
498
+ }
499
+ /**
500
+ * Check if near bottom (for infinite scroll)
501
+ */
502
+ isNearBottom(threshold = 500) {
503
+ const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
504
+ // Get container height from positions
505
+ let maxBottom = 0;
506
+ for (const position of this.positions.values()) {
507
+ maxBottom = Math.max(maxBottom, position.y + position.height);
508
+ }
509
+ return scrollTop + viewportHeight >= maxBottom - threshold;
510
+ }
511
+ /**
512
+ * Scroll to item
513
+ */
514
+ scrollToItem(id, behavior = 'smooth') {
515
+ const position = this.positions.get(id);
516
+ if (!position)
517
+ return;
518
+ const container = this.options.scrollContainer ?? window;
519
+ if (container === window) {
520
+ window.scrollTo({
521
+ top: position.y,
522
+ behavior,
523
+ });
524
+ }
525
+ else {
526
+ container.scrollTo({
527
+ top: position.y,
528
+ behavior,
529
+ });
530
+ }
531
+ }
532
+ /**
533
+ * Get render range for optimization
534
+ */
535
+ getRenderRange() {
536
+ if (this.visibleItems.length === 0) {
537
+ return { start: 0, end: 0 };
538
+ }
539
+ const indices = this.visibleItems.map(item => item.index);
540
+ return {
541
+ start: Math.min(...indices),
542
+ end: Math.max(...indices) + 1,
543
+ };
544
+ }
545
+ }
546
+
547
+ // ============================================
548
+ // Icons
549
+ // ============================================
550
+ const TwitterIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) }));
551
+ const InstagramIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M12 2c2.717 0 3.056.01 4.122.06 1.065.05 1.79.217 2.428.465.66.254 1.216.598 1.772 1.153a4.908 4.908 0 0 1 1.153 1.772c.247.637.415 1.363.465 2.428.047 1.066.06 1.405.06 4.122 0 2.717-.01 3.056-.06 4.122-.05 1.065-.218 1.79-.465 2.428a4.883 4.883 0 0 1-1.153 1.772 4.915 4.915 0 0 1-1.772 1.153c-.637.247-1.363.415-2.428.465-1.066.047-1.405.06-4.122.06-2.717 0-3.056-.01-4.122-.06-1.065-.05-1.79-.218-2.428-.465a4.89 4.89 0 0 1-1.772-1.153 4.904 4.904 0 0 1-1.153-1.772c-.248-.637-.415-1.363-.465-2.428C2.013 15.056 2 14.717 2 12c0-2.717.01-3.056.06-4.122.05-1.066.217-1.79.465-2.428a4.88 4.88 0 0 1 1.153-1.772A4.897 4.897 0 0 1 5.45 2.525c.638-.248 1.362-.415 2.428-.465C8.944 2.013 9.283 2 12 2zm0 1.802c-2.67 0-2.986.01-4.04.058-.976.045-1.505.207-1.858.344-.466.182-.8.398-1.15.748-.35.35-.566.684-.748 1.15-.137.353-.3.882-.344 1.857-.048 1.055-.058 1.37-.058 4.041 0 2.67.01 2.986.058 4.04.045.976.207 1.505.344 1.858.182.466.399.8.748 1.15.35.35.684.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058 2.67 0 2.987-.01 4.04-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.684.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041 0-2.67-.01-2.986-.058-4.04-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 0 0-.748-1.15 3.098 3.098 0 0 0-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.055-.048-1.37-.058-4.041-.058zm0 3.063a5.135 5.135 0 1 1 0 10.27 5.135 5.135 0 0 1 0-10.27zm0 8.468a3.333 3.333 0 1 0 0-6.666 3.333 3.333 0 0 0 0 6.666zm6.538-8.671a1.2 1.2 0 1 1-2.4 0 1.2 1.2 0 0 1 2.4 0z" }) }));
552
+ const VerifiedIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.34 2.19c-1.39-.46-2.9-.2-3.91.81s-1.27 2.52-.81 3.91C2.63 9.33 1.75 10.57 1.75 12s.88 2.67 2.19 3.34c-.46 1.39-.2 2.9.81 3.91s2.52 1.27 3.91.81c.66 1.31 1.91 2.19 3.34 2.19s2.67-.88 3.34-2.19c1.39.46 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77z" }) }));
553
+ const PlayIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsx("path", { fill: "currentColor", d: "M8 5v14l11-7z" }) }));
554
+ const Card = ({ post, position, variant = 'default', theme = 'auto', borderRadius = 12, showPlatformIcon = true, showAuthor = true, showMetrics = true, showTimestamp = true, formatDate = formatRelativeTime, formatNumber: formatNum = formatNumber, className = '', hoverEffect = true, imageLoading = 'lazy', fallbackImage = '', onPostClick, onAuthorClick, onMediaClick, onHeightChange, }) => {
555
+ const cardRef = useRef(null);
556
+ useEffect(() => {
557
+ if (cardRef.current && onHeightChange) {
558
+ const height = cardRef.current.offsetHeight;
559
+ if (height !== position.height) {
560
+ onHeightChange(post.id, height);
561
+ }
562
+ }
563
+ }, [post.id, position.height, onHeightChange]);
564
+ const handlePostClick = useCallback((e) => {
565
+ const target = e.target;
566
+ if (target.tagName === 'A' || target.closest('a'))
567
+ return;
568
+ onPostClick?.(post, e);
569
+ }, [post, onPostClick]);
570
+ const handleAuthorClick = useCallback((e) => {
571
+ e.stopPropagation();
572
+ onAuthorClick?.(post, e);
573
+ }, [post, onAuthorClick]);
574
+ const handleMediaClick = useCallback((index) => (e) => {
575
+ e.stopPropagation();
576
+ onMediaClick?.(post, index, e);
577
+ }, [post, onMediaClick]);
578
+ const classes = [
579
+ 'sm-card',
580
+ `sm-card--${post.platform}`,
581
+ `sm-card--${variant}`,
582
+ `sm-card--${theme}`,
583
+ hoverEffect && 'sm-card--hover',
584
+ className,
585
+ ].filter(Boolean).join(' ');
586
+ const style = {
587
+ position: 'absolute',
588
+ left: position.x,
589
+ top: position.y,
590
+ width: position.width,
591
+ borderRadius: typeof borderRadius === 'number' ? borderRadius : borderRadius,
592
+ cursor: onPostClick ? 'pointer' : undefined,
593
+ };
594
+ const date = post.createdAt instanceof Date ? post.createdAt : new Date(post.createdAt);
595
+ return (jsxs("div", { ref: cardRef, className: classes, style: style, onClick: handlePostClick, "data-post-id": post.id, "data-platform": post.platform, children: [showPlatformIcon && (jsx("div", { className: "sm-card__platform-icon", children: post.platform === 'twitter' ? (jsx(TwitterIcon, { className: "sm-platform-icon sm-platform-icon--twitter" })) : (jsx(InstagramIcon, { className: "sm-platform-icon sm-platform-icon--instagram" })) })), showAuthor && (jsxs("div", { className: "sm-card__header", onClick: onAuthorClick ? handleAuthorClick : undefined, style: { cursor: onAuthorClick ? 'pointer' : undefined }, children: [jsx("img", { src: post.author.avatarUrl || fallbackImage, alt: post.author.displayName || post.author.username, className: "sm-card__avatar", loading: imageLoading, onError: (e) => {
596
+ if (fallbackImage) {
597
+ e.target.src = fallbackImage;
598
+ }
599
+ } }), jsxs("div", { className: "sm-card__author", children: [jsxs("span", { className: "sm-card__author-name", children: [post.author.displayName || post.author.username, post.author.verified && jsx(VerifiedIcon, { className: "sm-card__verified" })] }), jsxs("span", { className: "sm-card__author-handle", children: ["@", post.author.username] })] })] })), jsxs("div", { className: "sm-card__content", children: [isTwitterPost(post) && (jsx("p", { className: "sm-card__text", children: post.content.text })), isInstagramPost(post) && post.content.caption && (jsx("p", { className: "sm-card__caption", children: post.content.caption.length > 150
600
+ ? `${post.content.caption.substring(0, 150)}...`
601
+ : post.content.caption }))] }), isTwitterPost(post) && post.media && post.media.length > 0 && (jsx("div", { className: `sm-card__media ${post.media.length > 1 ? `sm-card__media-grid sm-card__media-grid--${Math.min(post.media.length, 4)}` : ''}`, children: post.media.slice(0, 4).map((media, index) => (jsxs("div", { className: `sm-card__media-item ${media.type !== 'image' ? 'sm-card__media-item--video' : ''}`, onClick: onMediaClick ? handleMediaClick(index) : undefined, style: { cursor: onMediaClick ? 'pointer' : undefined }, children: [jsx("img", { src: media.thumbnailUrl || media.url, alt: "Post media", loading: imageLoading, onError: (e) => {
602
+ if (fallbackImage) {
603
+ e.target.src = fallbackImage;
604
+ }
605
+ } }), (media.type === 'video' || media.type === 'gif') && (jsx("div", { className: "sm-card__play-button", children: jsx(PlayIcon, {}) }))] }, index))) })), isInstagramPost(post) && (jsxs("div", { className: "sm-card__media", style: { aspectRatio: post.media.aspectRatio || 1 }, children: [jsx("img", { src: post.media.thumbnailUrl || post.media.url, alt: "Post media", loading: imageLoading, onError: (e) => {
606
+ if (fallbackImage) {
607
+ e.target.src = fallbackImage;
608
+ }
609
+ } }), post.media.type === 'video' && (jsx("div", { className: "sm-card__play-button", children: jsx(PlayIcon, {}) })), post.media.type === 'carousel' && (jsx("div", { className: "sm-card__carousel-indicator", children: jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M6 13c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm12 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1zm-6 0c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z" }) }) }))] })), (showMetrics || showTimestamp) && (jsxs("div", { className: "sm-card__footer", children: [showMetrics && post.metrics && (jsxs("div", { className: "sm-card__metrics", children: [isTwitterPost(post) && (jsxs(Fragment, { children: [post.metrics.replies !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01zm8.005-6c-3.317 0-6.005 2.69-6.005 6 0 3.37 2.77 6.08 6.138 6.01l.351-.01h1.761v2.3l5.087-2.81c1.951-1.08 3.163-3.13 3.163-5.36 0-3.39-2.744-6.13-6.129-6.13H9.756z" }) }), formatNum(post.metrics.replies)] })), post.metrics.retweets !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z" }) }), formatNum(post.metrics.retweets)] })), post.metrics.likes !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91zm4.187 7.69c-1.351 2.48-4.001 5.12-8.379 7.67l-.503.3-.504-.3c-4.379-2.55-7.029-5.19-8.382-7.67-1.36-2.5-1.41-4.86-.514-6.67.887-1.79 2.647-2.91 4.601-3.01 1.651-.09 3.368.56 4.798 2.01 1.429-1.45 3.146-2.1 4.796-2.01 1.954.1 3.714 1.22 4.601 3.01.896 1.81.846 4.17-.514 6.67z" }) }), formatNum(post.metrics.likes)] }))] })), isInstagramPost(post) && (jsxs(Fragment, { children: [post.metrics.likes !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M16.792 3.904A4.989 4.989 0 0 1 21.5 9.122c0 3.072-2.652 4.959-5.197 7.222-2.512 2.243-3.865 3.469-4.303 3.752-.477-.309-2.143-1.823-4.303-3.752C5.141 14.072 2.5 12.167 2.5 9.122a4.989 4.989 0 0 1 4.708-5.218 4.21 4.21 0 0 1 3.675 1.941c.84 1.175.98 1.763 1.12 1.763s.278-.588 1.11-1.766a4.17 4.17 0 0 1 3.679-1.938m0-2a6.04 6.04 0 0 0-4.797 2.127 6.052 6.052 0 0 0-4.787-2.127A6.985 6.985 0 0 0 .5 9.122c0 3.61 2.55 5.827 5.015 7.97.283.246.569.494.853.747l1.027.918a44.998 44.998 0 0 0 3.518 3.018 2 2 0 0 0 2.174 0 45.263 45.263 0 0 0 3.626-3.115l.922-.824c.293-.26.59-.519.885-.774 2.334-2.025 4.98-4.32 4.98-7.94a6.985 6.985 0 0 0-6.708-7.218z" }) }), formatNum(post.metrics.likes)] })), post.metrics.comments !== undefined && (jsxs("span", { className: "sm-card__metric", children: [jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { fill: "currentColor", d: "M20.656 17.008a9.993 9.993 0 1 0-3.59 3.615L22 22l-1.344-4.992zM10 12a1 1 0 1 1 1 1 1 1 0 0 1-1-1zm-4 0a1 1 0 1 1 1 1 1 1 0 0 1-1-1zm8 0a1 1 0 1 1 1 1 1 1 0 0 1-1-1z" }) }), formatNum(post.metrics.comments)] }))] }))] })), showTimestamp && (jsx("time", { className: "sm-card__timestamp", dateTime: date.toISOString(), children: formatDate(date) }))] }))] }));
610
+ };
611
+ const SocialMasonry = forwardRef((props, ref) => {
612
+ const { posts: initialPosts = [], gap = 16, columns = defaultColumnConfig, padding = 0, animationDuration = 300, animate = true, easing = 'cubic-bezier(0.4, 0, 0.2, 1)', virtualization, loadMoreThreshold: _loadMoreThreshold = 500, onLoadMore: _onLoadMore, onLayoutComplete, className, style, ...cardProps } = props;
613
+ const containerRef = useRef(null);
614
+ const [posts, setPosts] = useState(initialPosts);
615
+ const [positions, setPositions] = useState(new Map());
616
+ const [containerHeight, setContainerHeight] = useState(0);
617
+ const [itemHeights] = useState(() => new Map());
618
+ const [visibleItems, setVisibleItems] = useState([]);
619
+ const layoutEngine = useMemo(() => {
620
+ return new LayoutEngine({
621
+ gap,
622
+ columns,
623
+ padding,
624
+ animationDuration,
625
+ animate,
626
+ easing,
627
+ containerWidth: containerRef.current?.clientWidth || 800,
628
+ itemHeights,
629
+ });
630
+ }, [gap, columns, padding, animationDuration, animate, easing]);
631
+ const virtualizationEngine = useMemo(() => {
632
+ if (!virtualization?.enabled)
633
+ return null;
634
+ return new VirtualizationEngine({
635
+ ...virtualization,
636
+ posts,
637
+ positions,
638
+ onVisibleItemsChange: setVisibleItems,
639
+ });
640
+ }, [virtualization?.enabled]);
641
+ // Calculate layout
642
+ const calculateLayout = useCallback(() => {
643
+ if (!containerRef.current)
644
+ return;
645
+ layoutEngine.setContainerWidth(containerRef.current.clientWidth);
646
+ const state = layoutEngine.calculate(posts);
647
+ setPositions(state.positions);
648
+ setContainerHeight(state.containerHeight);
649
+ if (virtualizationEngine) {
650
+ virtualizationEngine.update(posts, state.positions);
651
+ setVisibleItems(virtualizationEngine.calculateVisibleItems());
652
+ }
653
+ onLayoutComplete?.(Array.from(state.positions.values()));
654
+ }, [posts, layoutEngine, virtualizationEngine, onLayoutComplete]);
655
+ // Handle resize
656
+ useEffect(() => {
657
+ const handleResize = debounce(() => {
658
+ calculateLayout();
659
+ }, 150);
660
+ const resizeObserver = new ResizeObserver(handleResize);
661
+ if (containerRef.current) {
662
+ resizeObserver.observe(containerRef.current);
663
+ }
664
+ return () => {
665
+ resizeObserver.disconnect();
666
+ };
667
+ }, [calculateLayout]);
668
+ // Initial layout
669
+ useEffect(() => {
670
+ calculateLayout();
671
+ }, [posts, calculateLayout]);
672
+ // Initialize virtualization
673
+ useEffect(() => {
674
+ if (virtualizationEngine) {
675
+ virtualizationEngine.init();
676
+ return () => virtualizationEngine.destroy();
677
+ }
678
+ return undefined;
679
+ }, [virtualizationEngine]);
680
+ // Handle height change
681
+ const handleHeightChange = useCallback((id, height) => {
682
+ itemHeights.set(id, height);
683
+ calculateLayout();
684
+ }, [itemHeights, calculateLayout]);
685
+ // Expose methods via ref
686
+ useImperativeHandle(ref, () => ({
687
+ addPosts: (newPosts) => {
688
+ setPosts(prev => [...prev, ...newPosts]);
689
+ },
690
+ setPosts: (newPosts) => {
691
+ itemHeights.clear();
692
+ setPosts(newPosts);
693
+ },
694
+ removePost: (id) => {
695
+ itemHeights.delete(id);
696
+ setPosts(prev => prev.filter(p => p.id !== id));
697
+ },
698
+ refresh: () => {
699
+ itemHeights.clear();
700
+ calculateLayout();
701
+ },
702
+ scrollToPost: (id, behavior = 'smooth') => {
703
+ const position = positions.get(id);
704
+ if (position) {
705
+ window.scrollTo({ top: position.y, behavior });
706
+ }
707
+ },
708
+ }), [positions, calculateLayout, itemHeights]);
709
+ // Determine which posts to render
710
+ const postsToRender = virtualizationEngine
711
+ ? visibleItems.map(item => item.post)
712
+ : posts;
713
+ return (jsxs("div", { ref: containerRef, className: `sm-container ${className || ''}`, style: {
714
+ position: 'relative',
715
+ width: '100%',
716
+ height: containerHeight,
717
+ ...style,
718
+ }, children: [postsToRender.map(post => {
719
+ const position = positions.get(post.id);
720
+ if (!position)
721
+ return null;
722
+ // Extract event handlers and adapt types
723
+ const { onPostClick, onAuthorClick, onMediaClick, ...restCardProps } = cardProps;
724
+ return (jsx(Card, { post: post, position: position, onHeightChange: handleHeightChange, onPostClick: onPostClick ? (p, e) => onPostClick(p, e.nativeEvent) : undefined, onAuthorClick: onAuthorClick ? (p, e) => onAuthorClick(p, e.nativeEvent) : undefined, onMediaClick: onMediaClick ? (p, i, e) => onMediaClick(p, i, e.nativeEvent) : undefined, ...restCardProps }, post.id));
725
+ }), posts.length === 0 && (jsx("div", { className: "sm-empty", children: "No posts to display" }))] }));
726
+ });
727
+ SocialMasonry.displayName = 'SocialMasonry';
728
+
729
+ export { SocialMasonry, SocialMasonry as default };
730
+ //# sourceMappingURL=index.esm.js.map