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,735 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var react = require('react');
7
+
8
+ /**
9
+ * Social Masonry - Utility Functions
10
+ */
11
+ /**
12
+ * Debounce function execution
13
+ */
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ function debounce(fn, delay) {
16
+ let timeoutId = null;
17
+ return function (...args) {
18
+ if (timeoutId) {
19
+ clearTimeout(timeoutId);
20
+ }
21
+ timeoutId = setTimeout(() => {
22
+ fn.apply(this, args);
23
+ timeoutId = null;
24
+ }, delay);
25
+ };
26
+ }
27
+ /**
28
+ * Throttle function execution
29
+ */
30
+ function throttle(fn, limit) {
31
+ let inThrottle = false;
32
+ let lastArgs = null;
33
+ return function (...args) {
34
+ if (!inThrottle) {
35
+ fn.apply(this, args);
36
+ inThrottle = true;
37
+ setTimeout(() => {
38
+ inThrottle = false;
39
+ if (lastArgs) {
40
+ fn.apply(this, lastArgs);
41
+ lastArgs = null;
42
+ }
43
+ }, limit);
44
+ }
45
+ else {
46
+ lastArgs = args;
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Get number of columns based on viewport width
52
+ */
53
+ function getColumnCount(columns, containerWidth) {
54
+ if (typeof columns === 'number') {
55
+ return columns;
56
+ }
57
+ // Sort by minWidth descending
58
+ const sorted = [...columns].sort((a, b) => b.minWidth - a.minWidth);
59
+ for (const config of sorted) {
60
+ if (containerWidth >= config.minWidth) {
61
+ return config.columns;
62
+ }
63
+ }
64
+ // Return smallest breakpoint's columns or default to 1
65
+ return sorted[sorted.length - 1]?.columns ?? 1;
66
+ }
67
+ /**
68
+ * Default responsive column configuration
69
+ */
70
+ const defaultColumnConfig = [
71
+ { columns: 4, minWidth: 1200 },
72
+ { columns: 3, minWidth: 900 },
73
+ { columns: 2, minWidth: 600 },
74
+ { columns: 1, minWidth: 0 },
75
+ ];
76
+ /**
77
+ * Format number with abbreviations (1K, 1M, etc.)
78
+ */
79
+ function formatNumber(num) {
80
+ if (num >= 1000000) {
81
+ return `${(num / 1000000).toFixed(1).replace(/\.0$/, '')}M`;
82
+ }
83
+ if (num >= 1000) {
84
+ return `${(num / 1000).toFixed(1).replace(/\.0$/, '')}K`;
85
+ }
86
+ return num.toString();
87
+ }
88
+ /**
89
+ * Format relative time
90
+ */
91
+ function formatRelativeTime(date) {
92
+ const now = new Date();
93
+ const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
94
+ if (diffInSeconds < 60) {
95
+ return 'now';
96
+ }
97
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
98
+ if (diffInMinutes < 60) {
99
+ return `${diffInMinutes}m`;
100
+ }
101
+ const diffInHours = Math.floor(diffInMinutes / 60);
102
+ if (diffInHours < 24) {
103
+ return `${diffInHours}h`;
104
+ }
105
+ const diffInDays = Math.floor(diffInHours / 24);
106
+ if (diffInDays < 7) {
107
+ return `${diffInDays}d`;
108
+ }
109
+ const diffInWeeks = Math.floor(diffInDays / 7);
110
+ if (diffInWeeks < 4) {
111
+ return `${diffInWeeks}w`;
112
+ }
113
+ // Format as date for older posts
114
+ return date.toLocaleDateString('en-US', {
115
+ month: 'short',
116
+ day: 'numeric',
117
+ });
118
+ }
119
+ /**
120
+ * Parse CSS value to pixels
121
+ */
122
+ function parseCSSValue(value, containerWidth) {
123
+ if (typeof value === 'number') {
124
+ return value;
125
+ }
126
+ const numMatch = value.match(/^([\d.]+)(px|rem|em|%|vw)?$/);
127
+ if (!numMatch) {
128
+ return 0;
129
+ }
130
+ const num = parseFloat(numMatch[1]);
131
+ const unit = numMatch[2] || 'px';
132
+ switch (unit) {
133
+ case 'px':
134
+ return num;
135
+ case 'rem':
136
+ return num * 16; // Assume 16px base
137
+ case 'em':
138
+ return num * 16;
139
+ case '%':
140
+ return 0;
141
+ case 'vw':
142
+ return (num / 100) * window.innerWidth;
143
+ default:
144
+ return num;
145
+ }
146
+ }
147
+ /**
148
+ * Check if element is in viewport
149
+ */
150
+ function isInViewport(element, scrollTop, viewportHeight, overscan = 0) {
151
+ const expandedTop = scrollTop - overscan;
152
+ const expandedBottom = scrollTop + viewportHeight + overscan;
153
+ return element.bottom >= expandedTop && element.top <= expandedBottom;
154
+ }
155
+ /**
156
+ * Get scroll position
157
+ */
158
+ function getScrollPosition(scrollContainer) {
159
+ if (!scrollContainer || scrollContainer === window) {
160
+ return {
161
+ scrollTop: window.scrollY || document.documentElement.scrollTop,
162
+ viewportHeight: window.innerHeight,
163
+ };
164
+ }
165
+ return {
166
+ scrollTop: scrollContainer.scrollTop,
167
+ viewportHeight: scrollContainer.clientHeight,
168
+ };
169
+ }
170
+ /**
171
+ * Type guard for Twitter posts
172
+ */
173
+ function isTwitterPost(post) {
174
+ return post.platform === 'twitter';
175
+ }
176
+ /**
177
+ * Type guard for Instagram posts
178
+ */
179
+ function isInstagramPost(post) {
180
+ return post.platform === 'instagram';
181
+ }
182
+ /**
183
+ * Request animation frame with fallback
184
+ */
185
+ const raf = typeof requestAnimationFrame !== 'undefined'
186
+ ? requestAnimationFrame
187
+ : (callback) => setTimeout(callback, 16);
188
+ /**
189
+ * Cancel animation frame with fallback
190
+ */
191
+ const cancelRaf = typeof cancelAnimationFrame !== 'undefined'
192
+ ? cancelAnimationFrame
193
+ : (id) => clearTimeout(id);
194
+
195
+ /**
196
+ * Social Masonry - Layout Engine
197
+ * Calculates positions for masonry grid items
198
+ */
199
+ class LayoutEngine {
200
+ constructor(options) {
201
+ this.options = {
202
+ gap: 16,
203
+ columns: defaultColumnConfig,
204
+ defaultColumns: 3,
205
+ padding: 0,
206
+ animationDuration: 300,
207
+ animate: true,
208
+ easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
209
+ ...options,
210
+ };
211
+ this.state = {
212
+ positions: new Map(),
213
+ columnHeights: [],
214
+ containerHeight: 0,
215
+ columnWidth: 0,
216
+ };
217
+ }
218
+ /**
219
+ * Calculate layout for all posts
220
+ */
221
+ calculate(posts) {
222
+ const { containerWidth, itemHeights } = this.options;
223
+ const gap = parseCSSValue(this.options.gap);
224
+ const padding = parseCSSValue(this.options.padding);
225
+ // Calculate column count and width
226
+ const columnCount = getColumnCount(this.options.columns, containerWidth);
227
+ const availableWidth = containerWidth - padding * 2;
228
+ const totalGapWidth = gap * (columnCount - 1);
229
+ const columnWidth = (availableWidth - totalGapWidth) / columnCount;
230
+ // Initialize column heights
231
+ const columnHeights = new Array(columnCount).fill(0);
232
+ const positions = new Map();
233
+ // Place each item in the shortest column
234
+ for (const post of posts) {
235
+ const itemHeight = itemHeights.get(post.id) ?? this.estimateHeight(post, columnWidth);
236
+ // Find shortest column
237
+ const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights));
238
+ // Calculate position
239
+ const x = padding + shortestColumn * (columnWidth + gap);
240
+ const y = columnHeights[shortestColumn];
241
+ positions.set(post.id, {
242
+ id: post.id,
243
+ x,
244
+ y,
245
+ width: columnWidth,
246
+ height: itemHeight,
247
+ column: shortestColumn,
248
+ });
249
+ // Update column height
250
+ columnHeights[shortestColumn] = y + itemHeight + gap;
251
+ }
252
+ // Remove last gap from column heights
253
+ const containerHeight = Math.max(...columnHeights.map(h => h - gap), 0);
254
+ this.state = {
255
+ positions,
256
+ columnHeights,
257
+ containerHeight,
258
+ columnWidth,
259
+ };
260
+ return this.state;
261
+ }
262
+ /**
263
+ * Estimate item height based on content
264
+ */
265
+ estimateHeight(post, columnWidth) {
266
+ let height = 0;
267
+ // Header (avatar, name, etc.)
268
+ height += 56;
269
+ // Text content
270
+ if (post.platform === 'twitter') {
271
+ const textLength = post.content.text.length;
272
+ const avgCharsPerLine = Math.floor(columnWidth / 8); // ~8px per char
273
+ const lines = Math.ceil(textLength / avgCharsPerLine);
274
+ height += lines * 24; // ~24px per line
275
+ }
276
+ else if (post.content.caption) {
277
+ const captionLength = post.content.caption.length;
278
+ const avgCharsPerLine = Math.floor(columnWidth / 8);
279
+ const lines = Math.min(Math.ceil(captionLength / avgCharsPerLine), 4); // Max 4 lines
280
+ height += lines * 20;
281
+ }
282
+ // Media
283
+ if (post.platform === 'twitter' && post.media?.length) {
284
+ const media = post.media[0];
285
+ const aspectRatio = media.aspectRatio ?? 16 / 9;
286
+ height += columnWidth / aspectRatio;
287
+ }
288
+ else if (post.platform === 'instagram') {
289
+ const aspectRatio = post.media.aspectRatio ?? 1;
290
+ height += columnWidth / aspectRatio;
291
+ }
292
+ // Footer (metrics, timestamp)
293
+ height += 44;
294
+ // Padding
295
+ height += 24;
296
+ return Math.round(height);
297
+ }
298
+ /**
299
+ * Update single item height and recalculate affected items
300
+ */
301
+ updateItemHeight(id, height) {
302
+ this.options.itemHeights.set(id, height);
303
+ }
304
+ /**
305
+ * Get current layout state
306
+ */
307
+ getState() {
308
+ return this.state;
309
+ }
310
+ /**
311
+ * Get position for specific item
312
+ */
313
+ getPosition(id) {
314
+ return this.state.positions.get(id);
315
+ }
316
+ /**
317
+ * Update container width
318
+ */
319
+ setContainerWidth(width) {
320
+ this.options.containerWidth = width;
321
+ }
322
+ /**
323
+ * Get current column count
324
+ */
325
+ getColumnCount() {
326
+ return getColumnCount(this.options.columns, this.options.containerWidth);
327
+ }
328
+ /**
329
+ * Get CSS variables for animations
330
+ */
331
+ getCSSVariables() {
332
+ return {
333
+ '--sm-animation-duration': `${this.options.animationDuration}ms`,
334
+ '--sm-easing': this.options.easing,
335
+ '--sm-gap': typeof this.options.gap === 'number'
336
+ ? `${this.options.gap}px`
337
+ : this.options.gap,
338
+ '--sm-column-width': `${this.state.columnWidth}px`,
339
+ '--sm-container-height': `${this.state.containerHeight}px`,
340
+ };
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Social Masonry - Virtualization Engine
346
+ * Handles virtual scrolling for large lists
347
+ */
348
+ class VirtualizationEngine {
349
+ constructor(options) {
350
+ this.visibleItems = [];
351
+ this.rafId = null;
352
+ this.lastScrollTop = 0;
353
+ this.isScrolling = false;
354
+ this.scrollEndTimeout = null;
355
+ this.options = {
356
+ enabled: true,
357
+ overscan: 3,
358
+ estimatedItemHeight: 400,
359
+ scrollContainer: null,
360
+ ...options,
361
+ };
362
+ this.posts = options.posts;
363
+ this.positions = options.positions;
364
+ this.onVisibleItemsChange = options.onVisibleItemsChange;
365
+ this.scrollHandler = throttle(this.handleScroll.bind(this), 16);
366
+ }
367
+ /**
368
+ * Initialize scroll listener
369
+ */
370
+ init() {
371
+ if (!this.options.enabled)
372
+ return;
373
+ const container = this.options.scrollContainer ?? window;
374
+ container.addEventListener('scroll', this.scrollHandler, { passive: true });
375
+ // Initial calculation
376
+ this.calculateVisibleItems();
377
+ }
378
+ /**
379
+ * Destroy and cleanup
380
+ */
381
+ destroy() {
382
+ const container = this.options.scrollContainer ?? window;
383
+ container.removeEventListener('scroll', this.scrollHandler);
384
+ if (this.rafId !== null) {
385
+ cancelRaf(this.rafId);
386
+ }
387
+ if (this.scrollEndTimeout) {
388
+ clearTimeout(this.scrollEndTimeout);
389
+ }
390
+ }
391
+ /**
392
+ * Handle scroll event
393
+ */
394
+ handleScroll() {
395
+ if (this.rafId !== null)
396
+ return;
397
+ this.isScrolling = true;
398
+ // Clear existing scroll end timeout
399
+ if (this.scrollEndTimeout) {
400
+ clearTimeout(this.scrollEndTimeout);
401
+ }
402
+ // Set scroll end detection
403
+ this.scrollEndTimeout = setTimeout(() => {
404
+ this.isScrolling = false;
405
+ }, 150);
406
+ this.rafId = raf(() => {
407
+ this.rafId = null;
408
+ this.calculateVisibleItems();
409
+ });
410
+ }
411
+ /**
412
+ * Calculate which items are visible
413
+ */
414
+ calculateVisibleItems() {
415
+ const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
416
+ const overscanPx = this.options.overscan * this.options.estimatedItemHeight;
417
+ this.lastScrollTop = scrollTop;
418
+ const newVisibleItems = [];
419
+ for (let i = 0; i < this.posts.length; i++) {
420
+ const post = this.posts[i];
421
+ const position = this.positions.get(post.id);
422
+ if (!position)
423
+ continue;
424
+ const isVisible = isInViewport({
425
+ top: position.y,
426
+ bottom: position.y + position.height,
427
+ }, scrollTop, viewportHeight, overscanPx);
428
+ if (isVisible) {
429
+ newVisibleItems.push({
430
+ index: i,
431
+ post,
432
+ position,
433
+ isVisible: true,
434
+ });
435
+ }
436
+ }
437
+ // Check if visible items changed
438
+ const hasChanged = this.hasVisibleItemsChanged(newVisibleItems);
439
+ if (hasChanged) {
440
+ this.visibleItems = newVisibleItems;
441
+ this.onVisibleItemsChange?.(newVisibleItems);
442
+ }
443
+ return this.visibleItems;
444
+ }
445
+ /**
446
+ * Check if visible items have changed
447
+ */
448
+ hasVisibleItemsChanged(newItems) {
449
+ if (newItems.length !== this.visibleItems.length) {
450
+ return true;
451
+ }
452
+ for (let i = 0; i < newItems.length; i++) {
453
+ if (newItems[i].post.id !== this.visibleItems[i].post.id) {
454
+ return true;
455
+ }
456
+ }
457
+ return false;
458
+ }
459
+ /**
460
+ * Update posts and positions
461
+ */
462
+ update(posts, positions) {
463
+ this.posts = posts;
464
+ this.positions = positions;
465
+ this.calculateVisibleItems();
466
+ }
467
+ /**
468
+ * Get visible items
469
+ */
470
+ getVisibleItems() {
471
+ return this.visibleItems;
472
+ }
473
+ /**
474
+ * Get all items (for non-virtualized mode)
475
+ */
476
+ getAllItems() {
477
+ return this.posts.map((post, index) => ({
478
+ index,
479
+ post,
480
+ position: this.positions.get(post.id),
481
+ isVisible: true,
482
+ }));
483
+ }
484
+ /**
485
+ * Check if scrolling
486
+ */
487
+ getIsScrolling() {
488
+ return this.isScrolling;
489
+ }
490
+ /**
491
+ * Get scroll direction
492
+ */
493
+ getScrollDirection() {
494
+ const { scrollTop } = getScrollPosition(this.options.scrollContainer);
495
+ if (scrollTop > this.lastScrollTop) {
496
+ return 'down';
497
+ }
498
+ else if (scrollTop < this.lastScrollTop) {
499
+ return 'up';
500
+ }
501
+ return 'none';
502
+ }
503
+ /**
504
+ * Check if near bottom (for infinite scroll)
505
+ */
506
+ isNearBottom(threshold = 500) {
507
+ const { scrollTop, viewportHeight } = getScrollPosition(this.options.scrollContainer);
508
+ // Get container height from positions
509
+ let maxBottom = 0;
510
+ for (const position of this.positions.values()) {
511
+ maxBottom = Math.max(maxBottom, position.y + position.height);
512
+ }
513
+ return scrollTop + viewportHeight >= maxBottom - threshold;
514
+ }
515
+ /**
516
+ * Scroll to item
517
+ */
518
+ scrollToItem(id, behavior = 'smooth') {
519
+ const position = this.positions.get(id);
520
+ if (!position)
521
+ return;
522
+ const container = this.options.scrollContainer ?? window;
523
+ if (container === window) {
524
+ window.scrollTo({
525
+ top: position.y,
526
+ behavior,
527
+ });
528
+ }
529
+ else {
530
+ container.scrollTo({
531
+ top: position.y,
532
+ behavior,
533
+ });
534
+ }
535
+ }
536
+ /**
537
+ * Get render range for optimization
538
+ */
539
+ getRenderRange() {
540
+ if (this.visibleItems.length === 0) {
541
+ return { start: 0, end: 0 };
542
+ }
543
+ const indices = this.visibleItems.map(item => item.index);
544
+ return {
545
+ start: Math.min(...indices),
546
+ end: Math.max(...indices) + 1,
547
+ };
548
+ }
549
+ }
550
+
551
+ // ============================================
552
+ // Icons
553
+ // ============================================
554
+ const TwitterIcon = ({ className }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsxRuntime.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" }) }));
555
+ const InstagramIcon = ({ className }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsxRuntime.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" }) }));
556
+ const VerifiedIcon = ({ className }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsxRuntime.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" }) }));
557
+ const PlayIcon = ({ className }) => (jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: className, children: jsxRuntime.jsx("path", { fill: "currentColor", d: "M8 5v14l11-7z" }) }));
558
+ 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, }) => {
559
+ const cardRef = react.useRef(null);
560
+ react.useEffect(() => {
561
+ if (cardRef.current && onHeightChange) {
562
+ const height = cardRef.current.offsetHeight;
563
+ if (height !== position.height) {
564
+ onHeightChange(post.id, height);
565
+ }
566
+ }
567
+ }, [post.id, position.height, onHeightChange]);
568
+ const handlePostClick = react.useCallback((e) => {
569
+ const target = e.target;
570
+ if (target.tagName === 'A' || target.closest('a'))
571
+ return;
572
+ onPostClick?.(post, e);
573
+ }, [post, onPostClick]);
574
+ const handleAuthorClick = react.useCallback((e) => {
575
+ e.stopPropagation();
576
+ onAuthorClick?.(post, e);
577
+ }, [post, onAuthorClick]);
578
+ const handleMediaClick = react.useCallback((index) => (e) => {
579
+ e.stopPropagation();
580
+ onMediaClick?.(post, index, e);
581
+ }, [post, onMediaClick]);
582
+ const classes = [
583
+ 'sm-card',
584
+ `sm-card--${post.platform}`,
585
+ `sm-card--${variant}`,
586
+ `sm-card--${theme}`,
587
+ hoverEffect && 'sm-card--hover',
588
+ className,
589
+ ].filter(Boolean).join(' ');
590
+ const style = {
591
+ position: 'absolute',
592
+ left: position.x,
593
+ top: position.y,
594
+ width: position.width,
595
+ borderRadius: typeof borderRadius === 'number' ? borderRadius : borderRadius,
596
+ cursor: onPostClick ? 'pointer' : undefined,
597
+ };
598
+ const date = post.createdAt instanceof Date ? post.createdAt : new Date(post.createdAt);
599
+ return (jsxRuntime.jsxs("div", { ref: cardRef, className: classes, style: style, onClick: handlePostClick, "data-post-id": post.id, "data-platform": post.platform, children: [showPlatformIcon && (jsxRuntime.jsx("div", { className: "sm-card__platform-icon", children: post.platform === 'twitter' ? (jsxRuntime.jsx(TwitterIcon, { className: "sm-platform-icon sm-platform-icon--twitter" })) : (jsxRuntime.jsx(InstagramIcon, { className: "sm-platform-icon sm-platform-icon--instagram" })) })), showAuthor && (jsxRuntime.jsxs("div", { className: "sm-card__header", onClick: onAuthorClick ? handleAuthorClick : undefined, style: { cursor: onAuthorClick ? 'pointer' : undefined }, children: [jsxRuntime.jsx("img", { src: post.author.avatarUrl || fallbackImage, alt: post.author.displayName || post.author.username, className: "sm-card__avatar", loading: imageLoading, onError: (e) => {
600
+ if (fallbackImage) {
601
+ e.target.src = fallbackImage;
602
+ }
603
+ } }), jsxRuntime.jsxs("div", { className: "sm-card__author", children: [jsxRuntime.jsxs("span", { className: "sm-card__author-name", children: [post.author.displayName || post.author.username, post.author.verified && jsxRuntime.jsx(VerifiedIcon, { className: "sm-card__verified" })] }), jsxRuntime.jsxs("span", { className: "sm-card__author-handle", children: ["@", post.author.username] })] })] })), jsxRuntime.jsxs("div", { className: "sm-card__content", children: [isTwitterPost(post) && (jsxRuntime.jsx("p", { className: "sm-card__text", children: post.content.text })), isInstagramPost(post) && post.content.caption && (jsxRuntime.jsx("p", { className: "sm-card__caption", children: post.content.caption.length > 150
604
+ ? `${post.content.caption.substring(0, 150)}...`
605
+ : post.content.caption }))] }), isTwitterPost(post) && post.media && post.media.length > 0 && (jsxRuntime.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) => (jsxRuntime.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: [jsxRuntime.jsx("img", { src: media.thumbnailUrl || media.url, alt: "Post media", loading: imageLoading, onError: (e) => {
606
+ if (fallbackImage) {
607
+ e.target.src = fallbackImage;
608
+ }
609
+ } }), (media.type === 'video' || media.type === 'gif') && (jsxRuntime.jsx("div", { className: "sm-card__play-button", children: jsxRuntime.jsx(PlayIcon, {}) }))] }, index))) })), isInstagramPost(post) && (jsxRuntime.jsxs("div", { className: "sm-card__media", style: { aspectRatio: post.media.aspectRatio || 1 }, children: [jsxRuntime.jsx("img", { src: post.media.thumbnailUrl || post.media.url, alt: "Post media", loading: imageLoading, onError: (e) => {
610
+ if (fallbackImage) {
611
+ e.target.src = fallbackImage;
612
+ }
613
+ } }), post.media.type === 'video' && (jsxRuntime.jsx("div", { className: "sm-card__play-button", children: jsxRuntime.jsx(PlayIcon, {}) })), post.media.type === 'carousel' && (jsxRuntime.jsx("div", { className: "sm-card__carousel-indicator", children: jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.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) && (jsxRuntime.jsxs("div", { className: "sm-card__footer", children: [showMetrics && post.metrics && (jsxRuntime.jsxs("div", { className: "sm-card__metrics", children: [isTwitterPost(post) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [post.metrics.replies !== undefined && (jsxRuntime.jsxs("span", { className: "sm-card__metric", children: [jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.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 && (jsxRuntime.jsxs("span", { className: "sm-card__metric", children: [jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.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 && (jsxRuntime.jsxs("span", { className: "sm-card__metric", children: [jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.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) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [post.metrics.likes !== undefined && (jsxRuntime.jsxs("span", { className: "sm-card__metric", children: [jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.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 && (jsxRuntime.jsxs("span", { className: "sm-card__metric", children: [jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.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 && (jsxRuntime.jsx("time", { className: "sm-card__timestamp", dateTime: date.toISOString(), children: formatDate(date) }))] }))] }));
614
+ };
615
+ const SocialMasonry = react.forwardRef((props, ref) => {
616
+ 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;
617
+ const containerRef = react.useRef(null);
618
+ const [posts, setPosts] = react.useState(initialPosts);
619
+ const [positions, setPositions] = react.useState(new Map());
620
+ const [containerHeight, setContainerHeight] = react.useState(0);
621
+ const [itemHeights] = react.useState(() => new Map());
622
+ const [visibleItems, setVisibleItems] = react.useState([]);
623
+ const layoutEngine = react.useMemo(() => {
624
+ return new LayoutEngine({
625
+ gap,
626
+ columns,
627
+ padding,
628
+ animationDuration,
629
+ animate,
630
+ easing,
631
+ containerWidth: containerRef.current?.clientWidth || 800,
632
+ itemHeights,
633
+ });
634
+ }, [gap, columns, padding, animationDuration, animate, easing]);
635
+ const virtualizationEngine = react.useMemo(() => {
636
+ if (!virtualization?.enabled)
637
+ return null;
638
+ return new VirtualizationEngine({
639
+ ...virtualization,
640
+ posts,
641
+ positions,
642
+ onVisibleItemsChange: setVisibleItems,
643
+ });
644
+ }, [virtualization?.enabled]);
645
+ // Calculate layout
646
+ const calculateLayout = react.useCallback(() => {
647
+ if (!containerRef.current)
648
+ return;
649
+ layoutEngine.setContainerWidth(containerRef.current.clientWidth);
650
+ const state = layoutEngine.calculate(posts);
651
+ setPositions(state.positions);
652
+ setContainerHeight(state.containerHeight);
653
+ if (virtualizationEngine) {
654
+ virtualizationEngine.update(posts, state.positions);
655
+ setVisibleItems(virtualizationEngine.calculateVisibleItems());
656
+ }
657
+ onLayoutComplete?.(Array.from(state.positions.values()));
658
+ }, [posts, layoutEngine, virtualizationEngine, onLayoutComplete]);
659
+ // Handle resize
660
+ react.useEffect(() => {
661
+ const handleResize = debounce(() => {
662
+ calculateLayout();
663
+ }, 150);
664
+ const resizeObserver = new ResizeObserver(handleResize);
665
+ if (containerRef.current) {
666
+ resizeObserver.observe(containerRef.current);
667
+ }
668
+ return () => {
669
+ resizeObserver.disconnect();
670
+ };
671
+ }, [calculateLayout]);
672
+ // Initial layout
673
+ react.useEffect(() => {
674
+ calculateLayout();
675
+ }, [posts, calculateLayout]);
676
+ // Initialize virtualization
677
+ react.useEffect(() => {
678
+ if (virtualizationEngine) {
679
+ virtualizationEngine.init();
680
+ return () => virtualizationEngine.destroy();
681
+ }
682
+ return undefined;
683
+ }, [virtualizationEngine]);
684
+ // Handle height change
685
+ const handleHeightChange = react.useCallback((id, height) => {
686
+ itemHeights.set(id, height);
687
+ calculateLayout();
688
+ }, [itemHeights, calculateLayout]);
689
+ // Expose methods via ref
690
+ react.useImperativeHandle(ref, () => ({
691
+ addPosts: (newPosts) => {
692
+ setPosts(prev => [...prev, ...newPosts]);
693
+ },
694
+ setPosts: (newPosts) => {
695
+ itemHeights.clear();
696
+ setPosts(newPosts);
697
+ },
698
+ removePost: (id) => {
699
+ itemHeights.delete(id);
700
+ setPosts(prev => prev.filter(p => p.id !== id));
701
+ },
702
+ refresh: () => {
703
+ itemHeights.clear();
704
+ calculateLayout();
705
+ },
706
+ scrollToPost: (id, behavior = 'smooth') => {
707
+ const position = positions.get(id);
708
+ if (position) {
709
+ window.scrollTo({ top: position.y, behavior });
710
+ }
711
+ },
712
+ }), [positions, calculateLayout, itemHeights]);
713
+ // Determine which posts to render
714
+ const postsToRender = virtualizationEngine
715
+ ? visibleItems.map(item => item.post)
716
+ : posts;
717
+ return (jsxRuntime.jsxs("div", { ref: containerRef, className: `sm-container ${className || ''}`, style: {
718
+ position: 'relative',
719
+ width: '100%',
720
+ height: containerHeight,
721
+ ...style,
722
+ }, children: [postsToRender.map(post => {
723
+ const position = positions.get(post.id);
724
+ if (!position)
725
+ return null;
726
+ // Extract event handlers and adapt types
727
+ const { onPostClick, onAuthorClick, onMediaClick, ...restCardProps } = cardProps;
728
+ return (jsxRuntime.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));
729
+ }), posts.length === 0 && (jsxRuntime.jsx("div", { className: "sm-empty", children: "No posts to display" }))] }));
730
+ });
731
+ SocialMasonry.displayName = 'SocialMasonry';
732
+
733
+ exports.SocialMasonry = SocialMasonry;
734
+ exports.default = SocialMasonry;
735
+ //# sourceMappingURL=index.js.map