@propbinder/mobile-design 0.0.1 → 0.0.2

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.
Files changed (123) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +12 -39
  3. package/src/animations/page-transitions.ts +86 -0
  4. package/src/assets/fonts/Brockmann-Bold.otf +0 -0
  5. package/src/assets/fonts/Brockmann-BoldItalic.otf +0 -0
  6. package/src/assets/fonts/Brockmann-Medium.otf +0 -0
  7. package/src/assets/fonts/Brockmann-MediumItalic.otf +0 -0
  8. package/src/assets/fonts/Brockmann-Regular.otf +0 -0
  9. package/src/assets/fonts/Brockmann-RegularItalic.otf +0 -0
  10. package/src/assets/fonts/Brockmann-SemiBold.otf +0 -0
  11. package/src/assets/fonts/Brockmann-SemiBoldItalic.otf +0 -0
  12. package/src/assets/fonts/Brockmann_desktop_license.pdf +0 -0
  13. package/src/assets/fonts/brockmann-medium-webfont.woff2 +0 -0
  14. package/src/assets/fonts/brockmann-regular-webfont.woff2 +0 -0
  15. package/src/assets/fonts/brockmann-semibold-webfont.woff2 +0 -0
  16. package/src/components/action-list-item/ds-mobile-action-list-item.ts +83 -0
  17. package/src/components/action-list-item/index.ts +2 -0
  18. package/src/components/app-layout/ds-mobile-app-layout.css +343 -0
  19. package/src/components/app-layout/ds-mobile-app-layout.ts +271 -0
  20. package/src/components/app-layout/index.ts +2 -0
  21. package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +130 -0
  22. package/src/components/avatar-with-badge/index.ts +2 -0
  23. package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +273 -0
  24. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +110 -0
  25. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +167 -0
  26. package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +656 -0
  27. package/src/components/bottom-sheet/index.ts +3 -0
  28. package/src/components/comment/ds-mobile-comment.ts +516 -0
  29. package/src/components/comment/index.ts +2 -0
  30. package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
  31. package/src/components/contact-list-item/index.ts +2 -0
  32. package/src/components/content/ds-mobile-content.ts +158 -0
  33. package/src/components/content/index.ts +2 -0
  34. package/src/components/ds-mobile-tabs.css +372 -0
  35. package/src/components/ds-mobile-tabs.ts +217 -0
  36. package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
  37. package/src/components/file-attachment/index.ts +2 -0
  38. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +98 -0
  39. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +514 -0
  40. package/src/components/handbook-detail-modal/index.ts +3 -0
  41. package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +130 -0
  42. package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +444 -0
  43. package/src/components/handbook-folder/index.ts +4 -0
  44. package/src/components/header-content/ds-mobile-header-content.ts +211 -0
  45. package/src/components/header-content/index.ts +2 -0
  46. package/src/components/index.ts +45 -0
  47. package/src/components/inline-photo/ds-mobile-inline-photo.ts +269 -0
  48. package/src/components/inline-photo/index.ts +1 -0
  49. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.css +60 -0
  50. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +280 -0
  51. package/src/components/interactive-list-item-inquiry/index.ts +2 -0
  52. package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +197 -0
  53. package/src/components/interactive-list-item-message/index.ts +2 -0
  54. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.css +70 -0
  55. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +594 -0
  56. package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
  57. package/src/components/interactive-list-item-post/index.ts +13 -0
  58. package/src/components/lightbox/ds-mobile-lightbox-footer.ts +331 -0
  59. package/src/components/lightbox/ds-mobile-lightbox-header.ts +173 -0
  60. package/src/components/lightbox/ds-mobile-lightbox-image.ts +464 -0
  61. package/src/components/lightbox/ds-mobile-lightbox-pdf.css +375 -0
  62. package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
  63. package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
  64. package/src/components/lightbox/ds-mobile-lightbox.service.ts +293 -0
  65. package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
  66. package/src/components/lightbox/index.ts +22 -0
  67. package/src/components/list-item/ds-mobile-list-item.ts +499 -0
  68. package/src/components/list-item/index.ts +2 -0
  69. package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
  70. package/src/components/list-item-static/index.ts +2 -0
  71. package/src/components/logo/ds-logo.ts +85 -0
  72. package/src/components/logo/index.ts +2 -0
  73. package/src/components/modal/ds-mobile-modal.css +163 -0
  74. package/src/components/modal/ds-mobile-modal.service.ts +329 -0
  75. package/src/components/modal/index.ts +8 -0
  76. package/src/components/page-details/ds-mobile-page-details.css +285 -0
  77. package/src/components/page-details/ds-mobile-page-details.ts +128 -0
  78. package/src/components/page-details/index.ts +2 -0
  79. package/src/components/page-main/ds-mobile-page-main.css +346 -0
  80. package/src/components/page-main/ds-mobile-page-main.ts +331 -0
  81. package/src/components/page-main/index.ts +2 -0
  82. package/src/components/post-card/ds-mobile-post-card.ts +685 -0
  83. package/src/components/post-card/ds-mobile-post-pdf-attachment.ts +124 -0
  84. package/src/components/post-card/index.ts +11 -0
  85. package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
  86. package/src/components/post-composer/index.ts +2 -0
  87. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +104 -0
  88. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +1273 -0
  89. package/src/components/post-detail-modal/index.ts +9 -0
  90. package/src/components/shared/directives/index.ts +2 -0
  91. package/src/components/shared/directives/long-press.directive.ts +208 -0
  92. package/src/components/shared/index.ts +3 -0
  93. package/src/components/shared/mobile-common.css +94 -0
  94. package/src/components/shared/mobile-page-base.css +315 -0
  95. package/src/components/shared/mobile-page-base.ts +70 -0
  96. package/src/components/swiper/ds-mobile-swiper.ts +123 -0
  97. package/src/components/swiper/index.ts +2 -0
  98. package/src/components/tab-bar/ds-mobile-tab-bar.ts +132 -0
  99. package/src/components/tab-bar/index.ts +2 -0
  100. package/src/components/tabs/ds-mobile-tabs.css +405 -0
  101. package/src/components/tabs/ds-mobile-tabs.ts +204 -0
  102. package/src/components/tabs/index.ts +2 -0
  103. package/src/pages/community.page.ts +768 -0
  104. package/src/pages/handbook.page.ts +298 -0
  105. package/src/pages/home.page.ts +192 -0
  106. package/src/pages/index.ts +9 -0
  107. package/src/pages/inquiries.example.ts +212 -0
  108. package/src/pages/inquiry-detail.example.css +434 -0
  109. package/src/pages/inquiry-detail.example.ts +416 -0
  110. package/src/pages/mobile-tabs-example.component.ts +146 -0
  111. package/src/pages/post-create.page.ts +311 -0
  112. package/src/pages/post-detail.page.ts +295 -0
  113. package/src/pages/whitelabel-demo.page.ts +548 -0
  114. package/src/public-api.ts +5 -0
  115. package/src/services/user.service.ts +35 -0
  116. package/src/services/whitelabel.service.ts +171 -0
  117. package/src/styles/ionic.css +673 -0
  118. package/tsconfig.lib.json +17 -0
  119. package/tsconfig.lib.prod.json +9 -0
  120. package/tsconfig.spec.json +13 -0
  121. package/fesm2022/propbinder-mobile-design.mjs +0 -8294
  122. package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
  123. package/index.d.ts +0 -2860
@@ -0,0 +1,464 @@
1
+ import {
2
+ Component,
3
+ signal,
4
+ computed,
5
+ ViewChild,
6
+ ElementRef,
7
+ AfterViewInit,
8
+ OnDestroy,
9
+ OnInit,
10
+ CUSTOM_ELEMENTS_SCHEMA,
11
+ HostListener
12
+ } from '@angular/core';
13
+ import { CommonModule } from '@angular/common';
14
+ import {
15
+ IonSpinner,
16
+ GestureController,
17
+ Gesture
18
+ } from '@ionic/angular/standalone';
19
+ import { Share } from '@capacitor/share';
20
+ import { Haptics, ImpactStyle } from '@capacitor/haptics';
21
+ import { DsIconButtonComponent } from '@propbinder/design-system';
22
+ import { DsAvatarComponent } from '@propbinder/design-system';
23
+ import { DsMobileLightboxHeaderComponent } from './ds-mobile-lightbox-header';
24
+ import { DsMobileLightboxFooterComponent } from './ds-mobile-lightbox-footer';
25
+ import type { LightboxImage, LightboxAuthor } from './ds-mobile-lightbox.service';
26
+ import Swiper from 'swiper';
27
+ import type { SwiperOptions } from 'swiper/types';
28
+
29
+ /**
30
+ * DsMobileLightboxImageComponent
31
+ *
32
+ * Full-screen image lightbox component with Swiper.js navigation and pinch-zoom.
33
+ *
34
+ * This component is typically not used directly - use DsMobileLightboxService instead.
35
+ *
36
+ * Features:
37
+ * - Swiper.js for smooth image navigation
38
+ * - Pinch to zoom in/out
39
+ * - Double-tap to toggle zoom
40
+ * - Swipe down to close (when not zoomed)
41
+ * - Image counter and navigation controls
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // Don't instantiate directly - use the service:
46
+ * constructor(private lightbox: DsMobileLightboxService) {}
47
+ *
48
+ * openImage() {
49
+ * this.lightbox.openImages({
50
+ * images: [{ type: 'image', src: 'image.jpg', title: 'My Image' }]
51
+ * });
52
+ * }
53
+ * ```
54
+ */
55
+ @Component({
56
+ selector: 'ds-mobile-lightbox-image',
57
+ standalone: true,
58
+ imports: [
59
+ CommonModule,
60
+ IonSpinner,
61
+ DsIconButtonComponent,
62
+ DsAvatarComponent,
63
+ DsMobileLightboxHeaderComponent,
64
+ DsMobileLightboxFooterComponent
65
+ ],
66
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
67
+ template: `
68
+ <div class="lightbox-overlay"
69
+ [class.zoomed]="isZoomed()">
70
+
71
+ <div class="lightbox-wrapper">
72
+ <!-- Header with author info and close button -->
73
+ <ds-mobile-lightbox-header
74
+ [author]="author"
75
+ (closeClick)="close()"
76
+ />
77
+
78
+ <!-- Swiper container -->
79
+ <div class="swiper-container" #swiperContainer>
80
+ <div class="swiper-wrapper">
81
+ @for (image of images; track image.src; let i = $index) {
82
+ <div class="swiper-slide">
83
+ <div class="image-zoom-container" [attr.data-index]="i">
84
+ <img
85
+ [src]="image.src"
86
+ [alt]="image.alt || 'Lightbox image'"
87
+ class="lightbox-image"
88
+ (load)="onImageLoad(i)"
89
+ (error)="onImageError(i)">
90
+ </div>
91
+ </div>
92
+ }
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Loading indicator -->
97
+ @if (isLoading()) {
98
+ <div class="loading-spinner">
99
+ <ion-spinner name="crescent"></ion-spinner>
100
+ </div>
101
+ }
102
+
103
+ <!-- Footer with navigation and actions -->
104
+ <ds-mobile-lightbox-footer
105
+ [showNavigation]="showControls && images.length > 1"
106
+ [currentIndex]="currentIndex()"
107
+ [totalImages]="images.length"
108
+ [isLiked]="isLiked()"
109
+ [likeCount]="likeCount()"
110
+ [commentCount]="commentCount()"
111
+ (prevClick)="previousImage()"
112
+ (nextClick)="nextImage()"
113
+ (likeClick)="onLikeToggle()"
114
+ (commentClick)="onReply()"
115
+ (shareClick)="onShare()"
116
+ />
117
+ </div>
118
+ </div>
119
+ `,
120
+ styleUrl: './ds-mobile-lightbox.css'
121
+ })
122
+ export class DsMobileLightboxImageComponent implements OnInit, AfterViewInit, OnDestroy {
123
+ // Inputs (passed from service as regular properties, not signals)
124
+ images!: LightboxImage[];
125
+ author?: LightboxAuthor;
126
+ initialIndex: number = 0;
127
+ enableZoom: boolean = true;
128
+ showControls: boolean = true;
129
+ enableSwipe: boolean = true;
130
+ showInfo: boolean = true;
131
+ animation: 'fade' | 'zoom' | 'slide' = 'fade';
132
+ onCloseRequested?: () => void;
133
+
134
+ // View children
135
+ @ViewChild('swiperContainer', { read: ElementRef }) swiperContainer!: ElementRef<HTMLDivElement>;
136
+
137
+ // State
138
+ currentIndex = signal(0);
139
+ scale = signal(1);
140
+ isZoomed = signal(false);
141
+ isLoading = signal(true);
142
+ hasError = signal(false);
143
+
144
+ // Action states
145
+ isLiked = signal(false);
146
+ likeCount = signal(0);
147
+ commentCount = signal(0);
148
+
149
+ // Computed
150
+ currentImage = computed(() => this.images[this.currentIndex()]);
151
+
152
+ // Swiper instance
153
+ private swiper?: Swiper;
154
+ private zoomData: Map<number, { scale: number; x: number; y: number }> = new Map();
155
+
156
+ constructor(
157
+ private gestureCtrl: GestureController
158
+ ) {}
159
+
160
+ ngOnInit(): void {
161
+ // Set initial index from the passed property
162
+ if (this.initialIndex !== undefined) {
163
+ this.currentIndex.set(this.initialIndex);
164
+ }
165
+
166
+ // Initialize action states from current image
167
+ const currentImg = this.images[this.currentIndex()];
168
+ if (currentImg) {
169
+ this.isLiked.set(currentImg.isLiked ?? false);
170
+ this.likeCount.set(currentImg.likeCount ?? 0);
171
+ this.commentCount.set(currentImg.commentCount ?? 0);
172
+ }
173
+ }
174
+
175
+ ngAfterViewInit(): void {
176
+ setTimeout(() => {
177
+ this.initializeSwiper();
178
+ this.initializeZoomGestures();
179
+ }, 100);
180
+ }
181
+
182
+ ngOnDestroy(): void {
183
+ // Clean up Swiper
184
+ if (this.swiper) {
185
+ this.swiper.destroy();
186
+ this.swiper = undefined;
187
+ }
188
+ }
189
+ /**
190
+ * Initialize Swiper for image navigation
191
+ */
192
+ private initializeSwiper(): void {
193
+ if (!this.swiperContainer) {
194
+ console.error('[Lightbox] Swiper container not found');
195
+ return;
196
+ }
197
+
198
+ const swiperOptions: SwiperOptions = {
199
+ initialSlide: this.initialIndex,
200
+ speed: 300,
201
+ resistance: true,
202
+ resistanceRatio: 0.85,
203
+ slidesPerView: 1,
204
+ spaceBetween: 0,
205
+ touchRatio: 1,
206
+ longSwipesRatio: 0.5,
207
+ threshold: 10,
208
+ on: {
209
+ slideChange: (swiper) => {
210
+ this.currentIndex.set(swiper.activeIndex);
211
+ this.updateActionStates();
212
+
213
+ // Check if the image is already loaded
214
+ const currentSlide = swiper.slides[swiper.activeIndex];
215
+ const img = currentSlide?.querySelector('img');
216
+ if (img && img.complete && img.naturalHeight !== 0) {
217
+ // Image is already loaded
218
+ this.isLoading.set(false);
219
+ }
220
+ },
221
+ slideChangeTransitionStart: () => {
222
+ // Don't show loading spinner if image is already loaded
223
+ const currentSlide = this.swiper?.slides[this.swiper.activeIndex];
224
+ const img = currentSlide?.querySelector('img');
225
+ if (!img || !img.complete || img.naturalHeight === 0) {
226
+ this.isLoading.set(true);
227
+ }
228
+ }
229
+ }
230
+ };
231
+
232
+ this.swiper = new Swiper(this.swiperContainer.nativeElement, swiperOptions);
233
+
234
+ // Check if the initial image is already loaded
235
+ setTimeout(() => {
236
+ const currentSlide = this.swiper?.slides[this.currentIndex()];
237
+ const img = currentSlide?.querySelector('img');
238
+ if (img && img.complete && img.naturalHeight !== 0) {
239
+ this.isLoading.set(false);
240
+ }
241
+ }, 0);
242
+ }
243
+
244
+ /**
245
+ * Initialize pinch-zoom gestures for all slides
246
+ */
247
+ private initializeZoomGestures(): void {
248
+ if (!this.enableZoom) return;
249
+
250
+ const slides = this.swiperContainer.nativeElement.querySelectorAll('.image-zoom-container');
251
+
252
+ slides.forEach((slide, index) => {
253
+ this.initializeZoomForSlide(slide as HTMLElement, index);
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Initialize zoom gestures for a specific slide
259
+ */
260
+ private initializeZoomForSlide(container: HTMLElement, index: number): void {
261
+ let initialDistance = 0;
262
+ let initialScale = 1;
263
+ let currentScale = 1;
264
+ let lastTap = 0;
265
+
266
+ // Double-tap to zoom
267
+ container.addEventListener('click', (event: MouseEvent) => {
268
+ const now = Date.now();
269
+ const timeSinceLastTap = now - lastTap;
270
+
271
+ if (timeSinceLastTap < 300 && timeSinceLastTap > 0) {
272
+ event.preventDefault();
273
+ this.toggleZoom(container, index);
274
+ }
275
+
276
+ lastTap = now;
277
+ });
278
+
279
+ // Pinch to zoom
280
+ const getTouchDistance = (touches: TouchList) => {
281
+ const dx = touches[0].clientX - touches[1].clientX;
282
+ const dy = touches[0].clientY - touches[1].clientY;
283
+ return Math.sqrt(dx * dx + dy * dy);
284
+ };
285
+
286
+ container.addEventListener('touchstart', (event: TouchEvent) => {
287
+ if (event.touches.length === 2) {
288
+ event.preventDefault();
289
+ initialDistance = getTouchDistance(event.touches);
290
+ const zoomData = this.zoomData.get(index) || { scale: 1, x: 0, y: 0 };
291
+ initialScale = zoomData.scale;
292
+
293
+ // Disable Swiper when zooming
294
+ if (this.swiper) {
295
+ this.swiper.allowTouchMove = false;
296
+ }
297
+ }
298
+ }, { passive: false });
299
+
300
+ container.addEventListener('touchmove', (event: TouchEvent) => {
301
+ if (event.touches.length === 2) {
302
+ event.preventDefault();
303
+
304
+ const currentDistance = getTouchDistance(event.touches);
305
+ const pinchScale = currentDistance / initialDistance;
306
+
307
+ currentScale = Math.max(1, Math.min(initialScale * pinchScale, 4));
308
+
309
+ const img = container.querySelector('img');
310
+ if (img) {
311
+ img.style.transform = `scale(${currentScale})`;
312
+ }
313
+
314
+ if (currentScale > 1) {
315
+ this.isZoomed.set(true);
316
+ } else {
317
+ this.isZoomed.set(false);
318
+ }
319
+ }
320
+ }, { passive: false });
321
+
322
+ container.addEventListener('touchend', (event: TouchEvent) => {
323
+ if (event.touches.length < 2) {
324
+ // Save zoom state
325
+ this.zoomData.set(index, { scale: currentScale, x: 0, y: 0 });
326
+
327
+ // Re-enable Swiper if not zoomed
328
+ if (this.swiper && currentScale <= 1) {
329
+ this.swiper.allowTouchMove = true;
330
+ }
331
+ }
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Toggle zoom on double-tap
337
+ */
338
+ private toggleZoom(container: HTMLElement, index: number): void {
339
+ const zoomData = this.zoomData.get(index) || { scale: 1, x: 0, y: 0 };
340
+ const img = container.querySelector('img');
341
+
342
+ if (!img) return;
343
+
344
+ if (zoomData.scale > 1) {
345
+ // Zoom out
346
+ img.style.transform = 'scale(1)';
347
+ this.zoomData.set(index, { scale: 1, x: 0, y: 0 });
348
+ this.isZoomed.set(false);
349
+ if (this.swiper) {
350
+ this.swiper.allowTouchMove = true;
351
+ }
352
+ } else {
353
+ // Zoom in
354
+ img.style.transform = 'scale(2)';
355
+ this.zoomData.set(index, { scale: 2, x: 0, y: 0 });
356
+ this.isZoomed.set(true);
357
+ if (this.swiper) {
358
+ this.swiper.allowTouchMove = false;
359
+ }
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Update action states (like, comments) when slide changes
365
+ */
366
+ private updateActionStates(): void {
367
+ const currentImg = this.images[this.currentIndex()];
368
+ if (currentImg) {
369
+ this.isLiked.set(currentImg.isLiked ?? false);
370
+ this.likeCount.set(currentImg.likeCount ?? 0);
371
+ this.commentCount.set(currentImg.commentCount ?? 0);
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Close the lightbox
377
+ */
378
+ close(): void {
379
+ if (this.onCloseRequested) {
380
+ this.onCloseRequested();
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Handle share button click
386
+ */
387
+ async onShare(): Promise<void> {
388
+ console.log('[Lightbox] Share button clicked');
389
+ const currentImg = this.currentImage();
390
+
391
+ try {
392
+ await Share.share({
393
+ title: currentImg.title || 'Shared Image',
394
+ url: currentImg.src,
395
+ dialogTitle: 'Share Image',
396
+ });
397
+ } catch (error) {
398
+ console.error('[Lightbox] Share failed:', error);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Handle like button toggle
404
+ */
405
+ onLikeToggle(): void {
406
+ console.log('[Lightbox] Like button toggled');
407
+ this.isLiked.update(liked => !liked);
408
+
409
+ if (this.isLiked()) {
410
+ this.likeCount.update(count => count + 1);
411
+ } else {
412
+ this.likeCount.update(count => Math.max(0, count - 1));
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Handle reply/comment button click
418
+ */
419
+ onReply(): void {
420
+ console.log('[Lightbox] Reply button clicked');
421
+ if (this.onCloseRequested) {
422
+ this.onCloseRequested();
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Navigate to the next image
428
+ */
429
+ nextImage(): void {
430
+ if (this.swiper && this.currentIndex() < this.images.length - 1) {
431
+ this.swiper.slideNext();
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Navigate to the previous image
437
+ */
438
+ previousImage(): void {
439
+ if (this.swiper && this.currentIndex() > 0) {
440
+ this.swiper.slidePrev();
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Handle image load success
446
+ */
447
+ onImageLoad(index: number): void {
448
+ if (index === this.currentIndex()) {
449
+ this.isLoading.set(false);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Handle image load error
455
+ */
456
+ onImageError(index: number): void {
457
+ if (index === this.currentIndex()) {
458
+ console.error(`[Lightbox] Image ${index} failed to load`);
459
+ this.isLoading.set(false);
460
+ this.hasError.set(true);
461
+ }
462
+ }
463
+ }
464
+