@codesinger0/shared-components 1.0.50 → 1.0.51

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,351 @@
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import useScrollLock from '../hooks/useScrollLock'
5
+
6
+ const MasonryImageList = ({ images = [], cols = 3, onImageClick = () => { } }) => {
7
+ // cols is the desktop column count; mobile will be forced to 2 via CSS
8
+ return (
9
+ <>
10
+ <div
11
+ className="masonry-columns h-full overflow-y-auto"
12
+ // set CSS variable so the component can control desktop column count
13
+ style={{ ['--masonry-cols']: cols }}
14
+ >
15
+ {images.map((img, i) => (
16
+ <button
17
+ key={i}
18
+ className="masonry-item w-full block mb-4 p-0 border-0 bg-transparent text-left"
19
+ onClick={() => onImageClick(i)}
20
+ aria-label={`open image ${i + 1}`}
21
+ >
22
+ <img
23
+ src={img.src || img}
24
+ alt={img.alt || `image-${i + 1}`}
25
+ loading="lazy"
26
+ className="masonry-img w-full h-auto rounded-lg block"
27
+ style={{ display: 'block' }}
28
+ />
29
+ </button>
30
+ ))}
31
+ </div>
32
+
33
+ <style jsx>{`
34
+ /* Container uses CSS columns to create the masonry stacking */
35
+ .masonry-columns {
36
+ column-gap: 0.75rem;
37
+ /* default: mobile 2 columns */
38
+ column-count: 2;
39
+ padding-right: 0.25rem; /* small padding so rtl scroll doesn't cut content */
40
+ }
41
+
42
+ /* Desktop: use the --masonry-cols variable */
43
+ @media (min-width: 1024px) {
44
+ .masonry-columns {
45
+ column-count: var(--masonry-cols);
46
+ }
47
+ }
48
+
49
+ /* Make each item avoid breaking across columns and have bottom margin */
50
+ .masonry-item {
51
+ /* avoid splitting an item between columns */
52
+ break-inside: avoid;
53
+ -webkit-column-break-inside: avoid;
54
+ page-break-inside: avoid;
55
+ margin-bottom: 0.75rem;
56
+ }
57
+
58
+ /* Image style */
59
+ .masonry-img {
60
+ width: 100%;
61
+ height: auto;
62
+ display: block;
63
+ object-fit: cover;
64
+ /* give images a subtle shadow & transition if desired */
65
+ transition: transform 0.2s ease, box-shadow 0.15s ease;
66
+ }
67
+ .masonry-item:hover .masonry-img {
68
+ transform: translateY(-2px) scale(1.01);
69
+ }
70
+
71
+ /* Optional: nicer scrollbar for the image column */
72
+ .masonry-columns::-webkit-scrollbar {
73
+ width: 10px;
74
+ }
75
+ .masonry-columns::-webkit-scrollbar-thumb {
76
+ border-radius: 9999px;
77
+ background: rgba(0, 0, 0, 0.12);
78
+ }
79
+ `}</style>
80
+ </>
81
+ );
82
+ };
83
+
84
+ // Image lightbox modal component
85
+ const ImageLightbox = ({
86
+ images,
87
+ currentIndex,
88
+ isOpen,
89
+ onClose,
90
+ onNavigate
91
+ }) => {
92
+ const currentImage = images[currentIndex];
93
+
94
+ useScrollLock(isOpen);
95
+
96
+ if (!currentImage) return null;
97
+
98
+ return (
99
+ <AnimatePresence>
100
+ {isOpen && (
101
+ <div className="fixed inset-0 z-50 flex items-center justify-center supports-[height:100dvh]:h-[100dvh]">
102
+ {/* Backdrop */}
103
+ <motion.div
104
+ key="backdrop"
105
+ initial={{ opacity: 0 }}
106
+ animate={{ opacity: 1 }}
107
+ exit={{ opacity: 0 }}
108
+ transition={{ duration: 0.2 }}
109
+ className="absolute inset-0 bg-black bg-opacity-90"
110
+ onClick={onClose}
111
+ />
112
+
113
+ {/* Image Container */}
114
+ <motion.div
115
+ key="image-container"
116
+ initial={{ opacity: 0, scale: 0.8 }}
117
+ animate={{ opacity: 1, scale: 1 }}
118
+ exit={{ opacity: 0, scale: 0.8 }}
119
+ transition={{ type: "spring", stiffness: 300, damping: 25 }}
120
+ className="relative max-w-[90vw] max-h-[90vh] z-10"
121
+ onClick={(e) => e.stopPropagation()}
122
+ >
123
+ {/* Main Image */}
124
+ <img
125
+ src={currentImage.src}
126
+ alt={currentImage.alt || currentImage.title || 'Gallery image'}
127
+ className="w-screen h-screen object-contain rounded-lg shadow-2xl"
128
+ />
129
+
130
+ {/* Close Button */}
131
+ <button
132
+ onClick={onClose}
133
+ className="absolute top-4 right-4 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-2 rounded-full transition-all duration-200"
134
+ aria-label="Close lightbox"
135
+ >
136
+ <X size={24} />
137
+ </button>
138
+
139
+ {/* Navigation Arrows */}
140
+ {images.length > 1 && (
141
+ <>
142
+ {currentIndex > 0 && (
143
+ <button
144
+ onClick={() => onNavigate(currentIndex - 1)}
145
+ className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
146
+ aria-label="Previous image"
147
+ >
148
+ <ChevronLeft size={24} />
149
+ </button>
150
+ )}
151
+
152
+ {currentIndex < images.length - 1 && (
153
+ <button
154
+ onClick={() => onNavigate(currentIndex + 1)}
155
+ className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-50 hover:bg-opacity-70 text-white p-3 rounded-full transition-all duration-200"
156
+ aria-label="Next image"
157
+ >
158
+ <ChevronRight size={24} />
159
+ </button>
160
+ )}
161
+ </>
162
+ )}
163
+
164
+ {/* Image Counter */}
165
+ {images.length > 1 && (
166
+ <div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
167
+ {currentIndex + 1} / {images.length}
168
+ </div>
169
+ )}
170
+
171
+ {/* Image Title */}
172
+ {currentImage.title && (
173
+ <div className="absolute bottom-4 right-4 bg-black bg-opacity-50 text-white px-3 py-2 rounded-lg max-w-xs">
174
+ <div className="text-sm font-medium" dir="rtl">
175
+ {currentImage.title}
176
+ </div>
177
+ </div>
178
+ )}
179
+ </motion.div>
180
+ </div>
181
+ )}
182
+ </AnimatePresence>
183
+ );
184
+ };
185
+
186
+ const MasonryItemCard = ({
187
+ images = [],
188
+ title,
189
+ subtitle,
190
+ description,
191
+ reverse = false,
192
+ textCols = 1,
193
+ imagesCols = 1,
194
+ mobileHeight = '60vh', // used for small screens
195
+ enableLightbox = true,
196
+ onImageClick,
197
+ className = '',
198
+ cols = 3, // default for desktop masonry (we want 3 per row on desktop)
199
+ ...props
200
+ }) => {
201
+ const [lightboxOpen, setLightboxOpen] = useState(false);
202
+ const [currentImageIndex, setCurrentImageIndex] = useState(0);
203
+
204
+ const totalCols = textCols + imagesCols;
205
+ // CSS variables to be applied to grid wrapper:
206
+ const textPct = `${(textCols / totalCols) * 100}%`;
207
+ const imagesPct = `${(imagesCols / totalCols) * 100}%`;
208
+
209
+ const handleImageClick = useCallback(
210
+ (index) => {
211
+ setCurrentImageIndex(index);
212
+
213
+ if (onImageClick) onImageClick(images[index], index);
214
+
215
+ if (enableLightbox) setLightboxOpen(true);
216
+ },
217
+ [images, onImageClick, enableLightbox]
218
+ );
219
+
220
+ const handleLightboxClose = useCallback(() => setLightboxOpen(false), []);
221
+ const handleLightboxNavigate = useCallback((index) => setCurrentImageIndex(index), []);
222
+
223
+ if (!images || images.length === 0) {
224
+ return (
225
+ <section className={`py-16 px-8 ${className}`} {...props}>
226
+ <div className="glass-card relative min-h-[48vh]">
227
+ <div className="max-w-7xl mx-auto px-6 pt-16">
228
+ <div className="text-center" dir="rtl">
229
+ <h2 className="title mb-4">{title}</h2>
230
+ <h3 className="subtitle mb-6">{subtitle}</h3>
231
+ <p className="content-text">אין תמונות להצגה</p>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ </section>
236
+ );
237
+ }
238
+
239
+ return (
240
+ <>
241
+ <section className={`relative w-full overflow-visible py-16 px-4 ${className}`} {...props}>
242
+ {/* NOTE: --text-frac / --image-frac are set inline so CSS can use them at lg and up */}
243
+ <div
244
+ className="glass-card relative min-h-[48vh] flex flex-col"
245
+ style={{ '--text-frac': textPct, '--image-frac': imagesPct }}
246
+ >
247
+ {/* top padding/content wrapper */}
248
+ <div className="max-w-7xl mx-auto px-6 pt-16 w-full flex-1">
249
+ {/* layout-grid will switch to two-column fractions on lg+ */}
250
+ <div className="layout-grid w-full h-full" dir="rtl">
251
+ {/* Text Content */}
252
+ <div
253
+ className={`text-column ${reverse ? 'lg:order-2' : 'lg:order-1'} px-2`}
254
+ >
255
+ <h2 className="title mb-6">{title}</h2>
256
+ <h3 className="subtitle mb-6">{subtitle}</h3>
257
+ <p className="content-text max-w-md mx-auto lg:mx-0">{description}</p>
258
+ </div>
259
+
260
+ {/* Desktop Masonry Images (visible on lg+) */}
261
+ <div
262
+ className={`image-column hidden lg:block ${reverse ? 'lg:order-1' : 'lg:order-2'} px-2 mb-8`}
263
+ >
264
+ {/* image-column fills available height and scrolls if needed */}
265
+ <MasonryImageList
266
+ images={images}
267
+ onImageClick={handleImageClick}
268
+ cols={cols /* desktop: 3 by default from caller prop */}
269
+ />
270
+ </div>
271
+ </div>
272
+ </div>
273
+
274
+ {/* Mobile Masonry Images (visible on mobile) */}
275
+ <div
276
+ className="block lg:hidden mt-8 w-full px-4"
277
+ style={{ height: mobileHeight }}
278
+ >
279
+ <MasonryImageList
280
+ images={images}
281
+ onImageClick={handleImageClick}
282
+ cols={2} // mobile: 2 per row
283
+ />
284
+ </div>
285
+ </div>
286
+ </section>
287
+
288
+ {/* Lightbox */}
289
+ {enableLightbox && (
290
+ <ImageLightbox
291
+ images={images}
292
+ currentIndex={currentImageIndex}
293
+ isOpen={lightboxOpen}
294
+ onClose={handleLightboxClose}
295
+ onNavigate={handleLightboxNavigate}
296
+ />
297
+ )}
298
+
299
+ {/* Component styles */}
300
+ <style jsx>{`
301
+ /* The overall grid - single column by default; switches to two fractional columns at lg+ */
302
+ .layout-grid {
303
+ display: grid;
304
+ grid-template-columns: 1fr;
305
+ gap: 3rem;
306
+ }
307
+
308
+ /* At large screens, use the fractional widths computed in inline styles */
309
+ @media (min-width: 1024px) {
310
+ .layout-grid {
311
+ /* use CSS variables set from JS: --text-frac and --image-frac */
312
+ grid-template-columns: var(--text-frac) var(--image-frac);
313
+ align-items: start;
314
+ gap: 3rem;
315
+ height: 100%;
316
+ }
317
+
318
+ .text-column {
319
+ /* ensure text column doesn't overflow the parent */
320
+ height: 100%;
321
+ }
322
+
323
+ .image-column {
324
+ /* make image column take full height of the parent wrapper and be scrollable */
325
+ height: 100%;
326
+ max-height: 100%;
327
+ overflow-y: auto;
328
+ }
329
+ }
330
+
331
+ /* Keep mobile images area scrollable if images exceed mobileHeight (we set that inline) */
332
+ @media (max-width: 1023px) {
333
+ .layout-grid {
334
+ gap: 1.5rem;
335
+ }
336
+ }
337
+
338
+ /* Optional: small visual tweak to hide scrollbar in WebKit (if you want) */
339
+ .image-column::-webkit-scrollbar {
340
+ width: 8px;
341
+ }
342
+ .image-column::-webkit-scrollbar-thumb {
343
+ border-radius: 9999px;
344
+ background: rgba(0, 0, 0, 0.2);
345
+ }
346
+ `}</style>
347
+ </>
348
+ );
349
+ };
350
+
351
+ export default MasonryItemCard;
@@ -0,0 +1,29 @@
1
+ /* Glass Card - Glassmorphism Effect */
2
+ .glass-card {
3
+ @apply rounded-xl backdrop-blur-md;
4
+ background-color: var(--card-bg);
5
+ background: color-mix(in srgb, var(--card-bg) 75%, transparent);
6
+ backdrop-filter: blur(10px);
7
+ box-shadow: 0 8px 32px 0 color-mix(in srgb, var(--primary) 20%, transparent);
8
+ border: 1px solid color-mix(in srgb, var(--card-bg) 30%, transparent);
9
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
10
+ }
11
+
12
+ .glass-button {
13
+ background: rgba(255, 107, 53, 0.2);
14
+ backdrop-filter: blur(10px);
15
+ border: 1px solid rgba(255, 107, 53, 0.3);
16
+ transition: all 0.3s ease;
17
+ }
18
+
19
+ .glass-button:hover {
20
+ background: rgba(255, 107, 53, 0.3);
21
+ transform: translateY(-2px);
22
+ box-shadow: 0 12px 24px rgba(255, 107, 53, 0.2);
23
+ }
24
+
25
+ .glass-card:hover {
26
+ background: color-mix(in srgb, var(--card-bg) 55%, transparent);
27
+ box-shadow: 0 12px 40px 0 color-mix(in srgb, var(--primary) 30%, transparent);
28
+ transform: translateY(-2px);
29
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.0.50",
3
+ "version": "1.0.51",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [
7
- "dist"
7
+ "dist",
8
+ "dist/styles"
8
9
  ],
9
10
  "scripts": {
10
11
  "build": "rm -fr dist; cp -r src dist"