@propbinder/mobile-design 0.2.47 → 0.2.50

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 (221) hide show
  1. package/ng-package.json +24 -0
  2. package/package.json +3 -39
  3. package/src/animations/page-transitions.ts +165 -0
  4. package/src/assets/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
  5. package/src/assets/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
  6. package/src/assets/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
  7. package/src/components/action-list-item/ds-mobile-action-list-item.ts +102 -0
  8. package/src/components/action-list-item/index.ts +2 -0
  9. package/src/components/app-icon/ds-app-icon.ts +133 -0
  10. package/src/components/app-icon/index.ts +2 -0
  11. package/src/components/attachment-preview/ds-mobile-attachment-preview.css +139 -0
  12. package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +164 -0
  13. package/src/components/attachment-preview/index.ts +1 -0
  14. package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +142 -0
  15. package/src/components/avatar-with-badge/index.ts +2 -0
  16. package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +71 -0
  17. package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +121 -0
  18. package/src/components/booking-modal/ds-mobile-booking-modal.ts +598 -0
  19. package/src/components/booking-modal/ds-mobile-booking-summary.ts +161 -0
  20. package/src/components/booking-modal/index.ts +4 -0
  21. package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +266 -0
  22. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +146 -0
  23. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +156 -0
  24. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +101 -0
  25. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +169 -0
  26. package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +211 -0
  27. package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +578 -0
  28. package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +614 -0
  29. package/src/components/bottom-sheet/index.ts +8 -0
  30. package/src/components/bottom-sheet/modal-shadow-fix.ts +42 -0
  31. package/src/components/card-inline/ds-mobile-card-inline.ts +301 -0
  32. package/src/components/card-inline/index.ts +2 -0
  33. package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +118 -0
  34. package/src/components/card-inline-banner/index.ts +1 -0
  35. package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +120 -0
  36. package/src/components/card-inline-contact/index.ts +1 -0
  37. package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +141 -0
  38. package/src/components/card-inline-file/index.ts +1 -0
  39. package/src/components/chat-modal/ds-mobile-chat-modal.css +159 -0
  40. package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +105 -0
  41. package/src/components/chat-modal/ds-mobile-chat-modal.ts +918 -0
  42. package/src/components/chat-modal/index.ts +8 -0
  43. package/src/components/comment/ds-mobile-comment.ts +568 -0
  44. package/src/components/comment/index.ts +2 -0
  45. package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
  46. package/src/components/contact-list-item/index.ts +2 -0
  47. package/src/components/content/ds-mobile-content.ts +139 -0
  48. package/src/components/content/index.ts +2 -0
  49. package/src/components/dropdown/ds-mobile-dropdown.css +199 -0
  50. package/src/components/dropdown/ds-mobile-dropdown.ts +340 -0
  51. package/src/components/dropdown/index.ts +2 -0
  52. package/src/components/ds-mobile-tabs.css +407 -0
  53. package/src/components/ds-mobile-tabs.ts +216 -0
  54. package/src/components/empty-state/ds-mobile-empty-state.ts +120 -0
  55. package/src/components/empty-state/index.ts +2 -0
  56. package/src/components/fab/ds-mobile-fab.ts +315 -0
  57. package/src/components/fab/index.ts +1 -0
  58. package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +121 -0
  59. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +189 -0
  60. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +135 -0
  61. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +656 -0
  62. package/src/components/facility-creation-modal/index.ts +9 -0
  63. package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +105 -0
  64. package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +188 -0
  65. package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +460 -0
  66. package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +134 -0
  67. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +69 -0
  68. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +379 -0
  69. package/src/components/facility-detail-modal/index.ts +2 -0
  70. package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
  71. package/src/components/file-attachment/index.ts +2 -0
  72. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +214 -0
  73. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +84 -0
  74. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +424 -0
  75. package/src/components/handbook-detail-modal/index.ts +3 -0
  76. package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +175 -0
  77. package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +533 -0
  78. package/src/components/handbook-folder/index.ts +4 -0
  79. package/src/components/header-content/ds-mobile-header-content.ts +222 -0
  80. package/src/components/header-content/index.ts +2 -0
  81. package/src/components/illustration/ds-mobile-illustration.ts +124 -0
  82. package/src/components/illustration/index.ts +2 -0
  83. package/src/components/index.ts +124 -0
  84. package/src/components/inline-photo/ds-mobile-inline-photo.ts +361 -0
  85. package/src/components/inline-photo/index.ts +1 -0
  86. package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +132 -0
  87. package/src/components/inline-tabs/index.ts +2 -0
  88. package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +350 -0
  89. package/src/components/interactive-list-item-booking/index.ts +1 -0
  90. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +321 -0
  91. package/src/components/interactive-list-item-inquiry/index.ts +2 -0
  92. package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +237 -0
  93. package/src/components/interactive-list-item-message/index.ts +2 -0
  94. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +549 -0
  95. package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
  96. package/src/components/interactive-list-item-post/index.ts +13 -0
  97. package/src/components/lightbox/ds-mobile-lightbox-footer.ts +315 -0
  98. package/src/components/lightbox/ds-mobile-lightbox-header.ts +202 -0
  99. package/src/components/lightbox/ds-mobile-lightbox-image.ts +484 -0
  100. package/src/components/lightbox/ds-mobile-lightbox-pdf.css +377 -0
  101. package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
  102. package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
  103. package/src/components/lightbox/ds-mobile-lightbox.service.ts +296 -0
  104. package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
  105. package/src/components/lightbox/index.ts +22 -0
  106. package/src/components/list-item/ds-mobile-list-item.ts +603 -0
  107. package/src/components/list-item/index.ts +2 -0
  108. package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
  109. package/src/components/list-item-static/index.ts +2 -0
  110. package/src/components/loader-overlay/ds-mobile-loader-overlay.css +49 -0
  111. package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +77 -0
  112. package/src/components/loader-overlay/index.ts +1 -0
  113. package/src/components/logo/ds-logo.ts +95 -0
  114. package/src/components/logo/index.ts +2 -0
  115. package/src/components/message-bubble/ds-mobile-message-bubble.ts +633 -0
  116. package/src/components/message-bubble/index.ts +7 -0
  117. package/src/components/message-composer/ds-mobile-message-composer.ts +1146 -0
  118. package/src/components/message-composer/index.ts +7 -0
  119. package/src/components/modal/ds-mobile-modal.css +163 -0
  120. package/src/components/modal/ds-mobile-modal.service.ts +329 -0
  121. package/src/components/modal/index.ts +8 -0
  122. package/src/components/modal-base/ds-mobile-modal-base.css +378 -0
  123. package/src/components/modal-base/ds-mobile-modal-base.ts +261 -0
  124. package/src/components/modal-base/index.ts +2 -0
  125. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +112 -0
  126. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +93 -0
  127. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +442 -0
  128. package/src/components/new-inquiry-modal/index.ts +4 -0
  129. package/src/components/offline-banner/ds-mobile-offline-banner.ts +135 -0
  130. package/src/components/offline-banner/index.ts +1 -0
  131. package/src/components/page-details/ds-mobile-page-details.css +83 -0
  132. package/src/components/page-details/ds-mobile-page-details.ts +282 -0
  133. package/src/components/page-details/index.ts +2 -0
  134. package/src/components/page-main/ds-mobile-page-main.css +68 -0
  135. package/src/components/page-main/ds-mobile-page-main.ts +421 -0
  136. package/src/components/page-main/index.ts +2 -0
  137. package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
  138. package/src/components/post-composer/index.ts +2 -0
  139. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +390 -0
  140. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +108 -0
  141. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +722 -0
  142. package/src/components/post-detail-modal/index.ts +9 -0
  143. package/src/components/property-banner/ds-mobile-property-banner.ts +95 -0
  144. package/src/components/property-banner/index.ts +2 -0
  145. package/src/components/section/ds-mobile-section.ts +263 -0
  146. package/src/components/section/index.ts +2 -0
  147. package/src/components/shared/directives/index.ts +2 -0
  148. package/src/components/shared/directives/long-press.directive.ts +212 -0
  149. package/src/components/shared/index.ts +3 -0
  150. package/src/components/shared/mobile-modal-base.ts +457 -0
  151. package/src/components/shared/mobile-page-base.ts +204 -0
  152. package/src/components/swiper/ds-mobile-swiper-with-nav.ts +160 -0
  153. package/src/components/swiper/ds-mobile-swiper.ts +327 -0
  154. package/src/components/swiper/index.ts +3 -0
  155. package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +129 -0
  156. package/src/components/system-message-banner/index.ts +2 -0
  157. package/src/components/tab-bar/ds-mobile-tab-bar.css +533 -0
  158. package/src/components/tab-bar/ds-mobile-tab-bar.ts +735 -0
  159. package/src/components/tab-bar/index.ts +2 -0
  160. package/src/components/tabs/ds-mobile-tabs.css +25 -0
  161. package/src/components/tabs/ds-mobile-tabs.ts +89 -0
  162. package/src/components/tabs/index.ts +2 -0
  163. package/src/components/text-input/ds-text-input.ts +287 -0
  164. package/src/components/text-input/index.ts +2 -0
  165. package/src/examples/booking.page.ts +434 -0
  166. package/src/examples/community.page.ts +776 -0
  167. package/src/examples/handbook.page.ts +324 -0
  168. package/src/examples/home.page.ts +347 -0
  169. package/src/examples/index.ts +12 -0
  170. package/src/examples/inquiries.example.ts +273 -0
  171. package/src/examples/inquiry-detail.example.css +189 -0
  172. package/src/examples/inquiry-detail.example.ts +415 -0
  173. package/src/examples/mobile-tabs-example.component.ts +208 -0
  174. package/src/examples/post-create.page.ts +311 -0
  175. package/src/examples/post-detail.page.ts +296 -0
  176. package/src/examples/sign-in.page.ts +291 -0
  177. package/src/examples/whitelabel-demo-modal.component.ts +1094 -0
  178. package/src/examples/whitelabel-demo-modal.service.ts +77 -0
  179. package/src/models/index.ts +7 -0
  180. package/src/models/post.model.ts +41 -0
  181. package/src/pages/community.page.ts +769 -0
  182. package/src/pages/handbook.page.ts +388 -0
  183. package/src/pages/home.page.ts +303 -0
  184. package/src/pages/index.ts +11 -0
  185. package/src/pages/inquiries.example.ts +273 -0
  186. package/src/pages/inquiry-detail.example.css +189 -0
  187. package/src/pages/inquiry-detail.example.ts +415 -0
  188. package/src/pages/mobile-tabs-example.component.ts +179 -0
  189. package/src/pages/post-create.page.ts +311 -0
  190. package/src/pages/post-detail.page.ts +296 -0
  191. package/src/pages/sign-in.page.ts +291 -0
  192. package/src/pages/whitelabel-demo-modal.component.ts +1094 -0
  193. package/src/pages/whitelabel-demo-modal.service.ts +77 -0
  194. package/src/public-api.ts +6 -0
  195. package/src/services/base-modal.service.ts +101 -0
  196. package/src/services/index.ts +11 -0
  197. package/src/services/posts.service.ts +542 -0
  198. package/src/services/tracking-permission.service.ts +88 -0
  199. package/src/services/user.service.ts +60 -0
  200. package/src/services/whitelabel.service.ts +675 -0
  201. package/{styles → src/styles}/ionic.css +25 -0
  202. package/tsconfig.lib.json +17 -0
  203. package/tsconfig.lib.prod.json +9 -0
  204. package/tsconfig.spec.json +13 -0
  205. package/fesm2022/propbinder-mobile-design.mjs +0 -26136
  206. package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
  207. package/index.d.ts +0 -8154
  208. /package/{assets → src/assets}/fonts/Brockmann-Bold.otf +0 -0
  209. /package/{assets → src/assets}/fonts/Brockmann-BoldItalic.otf +0 -0
  210. /package/{assets → src/assets}/fonts/Brockmann-Medium.otf +0 -0
  211. /package/{assets → src/assets}/fonts/Brockmann-MediumItalic.otf +0 -0
  212. /package/{assets → src/assets}/fonts/Brockmann-Regular.otf +0 -0
  213. /package/{assets → src/assets}/fonts/Brockmann-RegularItalic.otf +0 -0
  214. /package/{assets → src/assets}/fonts/Brockmann-SemiBold.otf +0 -0
  215. /package/{assets → src/assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
  216. /package/{assets → src/assets}/fonts/Brockmann_desktop_license.pdf +0 -0
  217. /package/{assets → src/assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
  218. /package/{assets → src/assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
  219. /package/{assets → src/assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
  220. /package/{styles → src/components/shared}/mobile-common.css +0 -0
  221. /package/{styles → src/components/shared}/mobile-page-base.css +0 -0
@@ -0,0 +1,735 @@
1
+ import { Component, Input, Output, EventEmitter, signal, OnInit, AfterViewInit, OnDestroy, ElementRef, computed, inject, effect } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { Router, NavigationEnd } from '@angular/router';
4
+ import { filter } from 'rxjs/operators';
5
+ import { IonTabBar, IonTabButton, IonLabel, ModalController } from '@ionic/angular/standalone';
6
+ import { DsIconComponent } from '@propbinder/design-system';
7
+ import { DsAvatarComponent } from '@propbinder/design-system';
8
+ import { DsLogoComponent } from '../logo/ds-logo';
9
+ import { DsMobileProfileActionsSheetComponent, ActionResult, ActionGroup, Language } from '../bottom-sheet';
10
+ import { disableModalShadowPointerEvents } from '../bottom-sheet/modal-shadow-fix';
11
+ import { UserService } from '../../services/user.service';
12
+
13
+ export interface TabConfig {
14
+ id: string;
15
+ label: string;
16
+ route: string;
17
+ icon: string;
18
+ iconActive: string;
19
+ }
20
+
21
+ /**
22
+ * DsMobileTabBarComponent
23
+ *
24
+ * Responsive navigation tab bar that adapts from mobile to desktop:
25
+ * - Mobile (< 768px): Bottom tab bar with icons + labels
26
+ * - Desktop (≥ 768px): Top navigation bar with logo, tabs, and avatar
27
+ *
28
+ * Use this component INSIDE your own `ion-tabs` when Angular routing
29
+ * requires `ion-tabs` to be a direct child in your component.
30
+ *
31
+ * @example
32
+ * ```html
33
+ * <!-- In your component with child routes -->
34
+ * <!-- IMPORTANT: Add class="ds-tabs-wrapper" to ion-tabs for proper styling -->
35
+ * <ion-tabs class="ds-tabs-wrapper">
36
+ * <ds-mobile-tab-bar
37
+ * [tabs]="tabs"
38
+ * [avatarInitials]="'JD'"
39
+ * [profileMenuItems]="profileMenuItems"
40
+ * (profileActionSelected)="handleProfileAction($event)"
41
+ * />
42
+ * </ion-tabs>
43
+ * ```
44
+ *
45
+ * @example With profile menu configuration
46
+ * ```typescript
47
+ * profileMenuItems: ActionGroup[] = [
48
+ * {
49
+ * actions: [
50
+ * { action: 'profile', title: 'My Profile', icon: 'remixUser3Line' },
51
+ * { action: 'settings', title: 'Settings', icon: 'remixSettings3Line' }
52
+ * ]
53
+ * },
54
+ * {
55
+ * actions: [
56
+ * { action: 'logout', title: 'Log Out', icon: 'remixLogoutBoxLine', destructive: true }
57
+ * ]
58
+ * }
59
+ * ];
60
+ *
61
+ * handleProfileAction(result: ActionResult): void {
62
+ * switch (result.action) {
63
+ * case 'profile': // Navigate to profile
64
+ * case 'settings': // Navigate to settings
65
+ * case 'logout': // Handle logout
66
+ * }
67
+ * }
68
+ * ```
69
+ *
70
+ * @note When using this component, you must add the class "ds-tabs-wrapper"
71
+ * to your `ion-tabs` element, or manually apply these styles:
72
+ * ```css
73
+ * ion-tabs {
74
+ * height: 100%;
75
+ * background: var(--color-header-surface);
76
+ * }
77
+ * ```
78
+ */
79
+ @Component({
80
+ selector: 'ds-mobile-tab-bar',
81
+ standalone: true,
82
+ imports: [CommonModule, IonTabBar, IonTabButton, IonLabel, DsIconComponent, DsAvatarComponent, DsLogoComponent],
83
+ styleUrls: ['./ds-mobile-tab-bar.css'],
84
+ template: `
85
+ <ion-tab-bar [attr.slot]="isDesktop() ? 'top' : 'bottom'" class="ds-tab-bar" [class.ds-tab-bar--desktop]="isDesktop()">
86
+ <!-- Logo (desktop only, full logo in header) -->
87
+ <div class="ds-tab-bar__logo">
88
+ <ds-logo variant="full" size="lg" />
89
+ </div>
90
+
91
+ <!-- Tab buttons container -->
92
+ <div class="ds-tab-bar__tabs" *ngIf="tabs">
93
+ <ion-tab-button
94
+ *ngFor="let tab of tabs; trackBy: trackByTabId"
95
+ [tab]="tab.route"
96
+ [attr.data-icon]="tab.icon"
97
+ [attr.data-icon-active]="tab.iconActive"
98
+ [attr.aria-label]="tab.label"
99
+ class="ds-tab-button ion-activatable"
100
+ [class.tab-selected]="isTabActive(tab.route)"
101
+ >
102
+ <div class="tab-icon-ripple"></div>
103
+ <div class="tab-icon-wrapper">
104
+ <ds-icon [name]="tab.icon" [size]="isDesktop() ? '20px' : '24px'" class="tab-icon-inactive" />
105
+ <ds-icon [name]="tab.iconActive" [size]="isDesktop() ? '20px' : '24px'" class="tab-icon-active" />
106
+ </div>
107
+ <ion-label [attr.aria-hidden]="true">{{ tab.label }}</ion-label>
108
+ </ion-tab-button>
109
+ </div>
110
+
111
+ <!-- Avatar (desktop only, positioned via CSS) -->
112
+ <div class="ds-tab-bar__actions">
113
+ <ds-avatar [size]="'md'" [type]="avatarType" [initials]="avatarInitials" [src]="avatarSrc" [iconName]="avatarIconName" (click)="handleAvatarClick()" />
114
+ </div>
115
+ </ion-tab-bar>
116
+ `,
117
+ })
118
+ export class DsMobileTabBarComponent implements OnInit, AfterViewInit, OnDestroy {
119
+ // Inputs
120
+ @Input() tabs: TabConfig[] = [];
121
+
122
+ // Avatar inputs
123
+ @Input() avatarType: 'initials' | 'photo' | 'icon' = 'initials';
124
+ @Input() avatarInitials: string = 'U';
125
+ @Input() avatarSrc: string = '';
126
+ @Input() avatarIconName: string = 'remixUser3Line';
127
+
128
+ /**
129
+ * Profile menu action groups to display when avatar is clicked.
130
+ * If not provided, only the avatarClick event will be emitted.
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * profileMenuItems: ActionGroup[] = [
135
+ * {
136
+ * actions: [
137
+ * { action: 'profile', title: 'My Profile', icon: 'remixUser3Line' },
138
+ * { action: 'settings', title: 'Settings', icon: 'remixSettings3Line' }
139
+ * ]
140
+ * }
141
+ * ];
142
+ * ```
143
+ */
144
+ @Input() profileMenuItems?: ActionGroup[];
145
+
146
+ // Outputs
147
+ @Output() avatarClick = new EventEmitter<void>();
148
+
149
+ /**
150
+ * Emitted when a profile menu action is selected.
151
+ * Parent component should handle the action logic (navigation, logout, etc.).
152
+ */
153
+ @Output() profileActionSelected = new EventEmitter<ActionResult>();
154
+
155
+ // Internal state - exposed for template binding
156
+ activeTab = signal<string>('');
157
+ isDesktop = signal<boolean>(false);
158
+
159
+ private mutationObserver?: MutationObserver;
160
+ private slotEnforcementObserver?: MutationObserver;
161
+ private resizeObserver?: ResizeObserver;
162
+ private mediaQuery?: MediaQueryList;
163
+ private routerSubscription?: any;
164
+
165
+ private router?: Router | null;
166
+ private modalController = inject(ModalController);
167
+ private userService = inject(UserService);
168
+
169
+ constructor(private elementRef: ElementRef) {
170
+ // Inject Router optionally
171
+ this.router = inject(Router, { optional: true }) || undefined;
172
+
173
+ // Initialize breakpoint detection EARLY (before effect)
174
+ // This ensures isDesktop() is set before the effect runs
175
+ this.setupBreakpointDetection();
176
+
177
+ // Debug: Log initial state
178
+ setTimeout(() => {
179
+ // console.log('[ds-mobile-tab-bar] Initial state:', {
180
+ // isDesktop: this.isDesktop(),
181
+ // windowWidth: window.innerWidth,
182
+ // mediaQuery: this.mediaQuery?.matches,
183
+ // userAgent: navigator.userAgent
184
+ // });
185
+ }, 100);
186
+
187
+ // Watch for isDesktop changes and update slot reactively
188
+ // effect() must be called in constructor (injection context)
189
+ effect(() => {
190
+ // This effect runs whenever isDesktop() changes
191
+ const _ = this.isDesktop(); // Read the signal to create dependency
192
+ // console.log('[ds-mobile-tab-bar] effect() triggered, isDesktop:', this.isDesktop());
193
+ if (this.elementRef.nativeElement) {
194
+ // Use setTimeout to ensure DOM is ready
195
+ setTimeout(() => this.updateSlot(), 0);
196
+ }
197
+ });
198
+ }
199
+
200
+ ngOnInit(): void {
201
+ // Listen to router events to detect active tab from URL
202
+ if (this.router) {
203
+ this.routerSubscription = this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe((event: any) => {
204
+ const url = event.urlAfterRedirects || event.url;
205
+ // Extract the route segment (e.g., /tab-bar-test/home -> home)
206
+ const segments = url.split('/').filter((s: string) => s);
207
+ const lastSegment = segments[segments.length - 1];
208
+
209
+ // Find matching tab by route
210
+ if (this.tabs && lastSegment) {
211
+ const matchingTab = this.tabs.find((tab) => tab.route === lastSegment);
212
+ if (matchingTab) {
213
+ this.activeTab.set(matchingTab.route);
214
+ }
215
+ }
216
+ });
217
+ }
218
+ }
219
+
220
+ ngAfterViewInit(): void {
221
+ // Initial removal
222
+ this.removeTitleAttributes();
223
+
224
+ // Set up mutation observer to continuously remove title attributes
225
+ this.setupTitleRemovalObserver();
226
+
227
+ // Set up active tab detection
228
+ this.setupActiveTabDetection();
229
+
230
+ // Ensure slot is set correctly on initial render (with retries)
231
+ this.updateSlot();
232
+
233
+ // Set up slot enforcement to prevent Ionic from overriding
234
+ setTimeout(() => {
235
+ this.setupSlotEnforcement();
236
+ // Also retry updateSlot a few times to ensure it sticks
237
+ setTimeout(() => this.updateSlot(), 100);
238
+ setTimeout(() => this.updateSlot(), 300);
239
+ }, 0);
240
+ }
241
+
242
+ private updateSlot(): void {
243
+ // CRITICAL: Set slot on the HOST element (ds-mobile-tab-bar) first
244
+ // Ionic positions children based on the wrapper's slot, not the inner element's slot
245
+ const hostElement = this.elementRef.nativeElement;
246
+ const hostSlotValue = this.isDesktop() ? 'top' : 'bottom';
247
+ const currentHostSlot = hostElement.getAttribute('slot');
248
+
249
+ if (currentHostSlot !== hostSlotValue) {
250
+ // console.log('[ds-mobile-tab-bar] updateSlot: Setting HOST slot from', currentHostSlot, 'to', hostSlotValue);
251
+ hostElement.setAttribute('slot', hostSlotValue);
252
+ (hostElement as any).slot = hostSlotValue;
253
+ }
254
+
255
+ // Get the ion-tab-bar element
256
+ const tabBar = this.elementRef.nativeElement.querySelector('ion-tab-bar');
257
+ if (!tabBar) {
258
+ // console.log('[ds-mobile-tab-bar] updateSlot: tabBar not found, retrying...');
259
+ // Retry if element not found yet
260
+ setTimeout(() => this.updateSlot(), 50);
261
+ return;
262
+ }
263
+
264
+ const slotValue = this.isDesktop() ? 'top' : 'bottom';
265
+ const currentSlot = tabBar.getAttribute('slot');
266
+ const currentSlotProperty = (tabBar as any).slot;
267
+
268
+ // Debug logging
269
+ // console.log('[ds-mobile-tab-bar] updateSlot:', {
270
+ // isDesktop: this.isDesktop(),
271
+ // windowWidth: window.innerWidth,
272
+ // slotValue,
273
+ // currentSlotAttribute: currentSlot,
274
+ // currentSlotProperty: currentSlotProperty,
275
+ // tabBarElement: tabBar,
276
+ // tabBarParent: tabBar.parentElement?.tagName,
277
+ // tabBarInIonTabs: tabBar.closest('ion-tabs') !== null
278
+ // });
279
+
280
+ // Only update if different to avoid unnecessary DOM manipulation
281
+ if (currentSlot !== slotValue || currentSlotProperty !== slotValue) {
282
+ // console.log('[ds-mobile-tab-bar] updateSlot: Setting slot from', currentSlot, 'to', slotValue);
283
+
284
+ // Set both attribute and property to ensure it works
285
+ tabBar.setAttribute('slot', slotValue);
286
+ (tabBar as any).slot = slotValue;
287
+
288
+ // Also try setting it on the parent ion-tabs
289
+ const parentIonTabs = tabBar.closest('ion-tabs');
290
+ if (parentIonTabs) {
291
+ // console.log('[ds-mobile-tab-bar] updateSlot: Found parent ion-tabs');
292
+ // Force Ionic to recognize the slot change
293
+ (parentIonTabs as any).forceUpdate?.();
294
+ }
295
+
296
+ // Force a reflow to ensure Ionic processes the change
297
+ void tabBar.offsetHeight;
298
+
299
+ // Verify it was set
300
+ const verifySlot = tabBar.getAttribute('slot');
301
+ const verifySlotProperty = (tabBar as any).slot;
302
+ // console.log('[ds-mobile-tab-bar] updateSlot: After update, slot attribute:', verifySlot, 'slot property:', verifySlotProperty);
303
+
304
+ // Check computed styles
305
+ const computedStyle = window.getComputedStyle(tabBar);
306
+ const parentComputedStyle = tabBar.parentElement ? window.getComputedStyle(tabBar.parentElement) : null;
307
+ const ionTabsForStyles = tabBar.closest('ion-tabs');
308
+ const ionTabsComputedStyle = ionTabsForStyles ? window.getComputedStyle(ionTabsForStyles) : null;
309
+
310
+ // console.log('[ds-mobile-tab-bar] updateSlot: Computed styles:', {
311
+ // tabBar: {
312
+ // position: computedStyle.position,
313
+ // top: computedStyle.top,
314
+ // bottom: computedStyle.bottom,
315
+ // order: computedStyle.order,
316
+ // display: computedStyle.display,
317
+ // zIndex: computedStyle.zIndex,
318
+ // transform: computedStyle.transform
319
+ // },
320
+ // parent: parentComputedStyle ? {
321
+ // display: parentComputedStyle.display,
322
+ // flexDirection: parentComputedStyle.flexDirection,
323
+ // gridTemplateRows: parentComputedStyle.gridTemplateRows
324
+ // } : null,
325
+ // ionTabs: ionTabsComputedStyle ? {
326
+ // display: ionTabsComputedStyle.display,
327
+ // flexDirection: ionTabsComputedStyle.flexDirection,
328
+ // gridTemplateRows: ionTabsComputedStyle.gridTemplateRows,
329
+ // position: ionTabsComputedStyle.position
330
+ // } : null,
331
+ // tabBarRect: tabBar.getBoundingClientRect(),
332
+ // windowHeight: window.innerHeight
333
+ // });
334
+ } else {
335
+ // console.log('[ds-mobile-tab-bar] updateSlot: Slot already correct, no update needed');
336
+
337
+ // Even if slot is correct, check computed styles to see why it's not at top
338
+ const computedStyle = window.getComputedStyle(tabBar);
339
+ const ionTabsForStyles = tabBar.closest('ion-tabs');
340
+ const ionTabsComputedStyle = ionTabsForStyles ? window.getComputedStyle(ionTabsForStyles) : null;
341
+ const tabBarRect = tabBar.getBoundingClientRect();
342
+
343
+ // Log key values directly so they're always visible
344
+ // console.log('[ds-mobile-tab-bar] KEY VALUES:');
345
+ // console.log(' tabBar.position:', computedStyle.position);
346
+ // console.log(' tabBar.top:', computedStyle.top);
347
+ // console.log(' tabBar.bottom:', computedStyle.bottom);
348
+ // console.log(' tabBar.order:', computedStyle.order);
349
+ // console.log(' tabBar.display:', computedStyle.display);
350
+ // console.log(' tabBarRect.top:', tabBarRect.top, 'px from top');
351
+ // console.log(' tabBarRect.bottom:', tabBarRect.bottom, 'px from top');
352
+ // console.log(' window.innerHeight:', window.innerHeight);
353
+ if (ionTabsComputedStyle) {
354
+ // console.log(' ionTabs.display:', ionTabsComputedStyle.display);
355
+ // console.log(' ionTabs.flexDirection:', ionTabsComputedStyle.flexDirection);
356
+ // console.log(' ionTabs.gridTemplateRows:', ionTabsComputedStyle.gridTemplateRows);
357
+ }
358
+ if (ionTabsForStyles) {
359
+ const children = Array.from(ionTabsForStyles.children);
360
+ // console.log(' ionTabs children count:', children.length);
361
+ children.forEach((child: any, index) => {
362
+ // console.log(` [${index}] ${child.tagName} slot="${child.getAttribute('slot')}" order="${window.getComputedStyle(child).order}"`);
363
+ });
364
+ }
365
+
366
+ // console.log('[ds-mobile-tab-bar] updateSlot: Computed styles (slot correct but visually wrong):', {
367
+ // tabBar: {
368
+ // position: computedStyle.position,
369
+ // top: computedStyle.top,
370
+ // bottom: computedStyle.bottom,
371
+ // order: computedStyle.order,
372
+ // display: computedStyle.display,
373
+ // zIndex: computedStyle.zIndex,
374
+ // transform: computedStyle.transform,
375
+ // marginTop: computedStyle.marginTop,
376
+ // marginBottom: computedStyle.marginBottom,
377
+ // width: computedStyle.width,
378
+ // height: computedStyle.height
379
+ // },
380
+ // ionTabs: ionTabsComputedStyle ? {
381
+ // display: ionTabsComputedStyle.display,
382
+ // flexDirection: ionTabsComputedStyle.flexDirection,
383
+ // gridTemplateRows: ionTabsComputedStyle.gridTemplateRows,
384
+ // gridTemplateColumns: ionTabsComputedStyle.gridTemplateColumns,
385
+ // position: ionTabsComputedStyle.position,
386
+ // alignItems: ionTabsComputedStyle.alignItems,
387
+ // justifyContent: ionTabsComputedStyle.justifyContent,
388
+ // height: ionTabsComputedStyle.height,
389
+ // minHeight: ionTabsComputedStyle.minHeight
390
+ // } : null,
391
+ // tabBarRect: {
392
+ // top: tabBarRect.top,
393
+ // bottom: tabBarRect.bottom,
394
+ // height: tabBarRect.height,
395
+ // y: tabBarRect.y,
396
+ // left: tabBarRect.left,
397
+ // right: tabBarRect.right,
398
+ // width: tabBarRect.width
399
+ // },
400
+ // windowHeight: window.innerHeight,
401
+ // distanceFromTop: tabBarRect.top,
402
+ // distanceFromBottom: window.innerHeight - tabBarRect.bottom,
403
+ // // Check if tab bar is actually in the DOM at the right position
404
+ // tabBarParent: tabBar.parentElement?.tagName,
405
+ // tabBarNextSibling: tabBar.nextElementSibling?.tagName,
406
+ // tabBarPreviousSibling: tabBar.previousElementSibling?.tagName,
407
+ // // Check all children of ion-tabs to see DOM order
408
+ // ionTabsChildren: ionTabsForStyles ? Array.from(ionTabsForStyles.children).map((child: any) => ({
409
+ // tagName: child.tagName,
410
+ // slot: child.getAttribute('slot'),
411
+ // order: window.getComputedStyle(child).order
412
+ // })) : null
413
+ // });
414
+ }
415
+ }
416
+
417
+ private setupSlotEnforcement(): void {
418
+ const hostElement = this.elementRef.nativeElement;
419
+ const tabBar = this.elementRef.nativeElement.querySelector('ion-tab-bar');
420
+ if (!tabBar) {
421
+ // console.log('[ds-mobile-tab-bar] setupSlotEnforcement: tabBar not found, retrying...');
422
+ // Retry if element not found yet
423
+ setTimeout(() => this.setupSlotEnforcement(), 50);
424
+ return;
425
+ }
426
+
427
+ // console.log('[ds-mobile-tab-bar] setupSlotEnforcement: Setting up MutationObserver');
428
+
429
+ const observer = new MutationObserver((mutations) => {
430
+ mutations.forEach((mutation) => {
431
+ if (mutation.type === 'attributes' && mutation.attributeName === 'slot') {
432
+ const target = mutation.target as HTMLElement;
433
+ const expectedSlot = this.isDesktop() ? 'top' : 'bottom';
434
+
435
+ // Check both host element and tab bar
436
+ if (target === hostElement || target === tabBar) {
437
+ const currentSlot = target.getAttribute('slot');
438
+
439
+ // console.log('[ds-mobile-tab-bar] Slot changed by external source:', {
440
+ // target: target.tagName,
441
+ // currentSlot,
442
+ // expectedSlot,
443
+ // isDesktop: this.isDesktop()
444
+ // });
445
+
446
+ // If Ionic or something else changed it, force it back
447
+ if (currentSlot !== expectedSlot) {
448
+ // console.log('[ds-mobile-tab-bar] Enforcing slot back to:', expectedSlot);
449
+ // Use requestAnimationFrame to avoid infinite loops
450
+ requestAnimationFrame(() => {
451
+ target.setAttribute('slot', expectedSlot);
452
+ (target as any).slot = expectedSlot;
453
+ });
454
+ }
455
+ }
456
+ }
457
+ });
458
+ });
459
+
460
+ // Observe both host element and tab bar for slot changes
461
+ observer.observe(hostElement, {
462
+ attributes: true,
463
+ attributeFilter: ['slot'],
464
+ });
465
+
466
+ observer.observe(tabBar, {
467
+ attributes: true,
468
+ attributeFilter: ['slot'],
469
+ });
470
+
471
+ // Store observer for cleanup
472
+ this.slotEnforcementObserver = observer;
473
+ }
474
+
475
+ ngOnDestroy(): void {
476
+ if (this.mutationObserver) {
477
+ this.mutationObserver.disconnect();
478
+ }
479
+ if (this.slotEnforcementObserver) {
480
+ this.slotEnforcementObserver.disconnect();
481
+ }
482
+ if (this.mediaQuery) {
483
+ this.mediaQuery.removeEventListener('change', this.handleBreakpointChange);
484
+ }
485
+ if (this.routerSubscription) {
486
+ this.routerSubscription.unsubscribe();
487
+ }
488
+ }
489
+
490
+ private setupBreakpointDetection(): void {
491
+ // Use matchMedia for responsive breakpoint detection
492
+ this.mediaQuery = window.matchMedia('(min-width: 768px)');
493
+ this.isDesktop.set(this.mediaQuery.matches);
494
+
495
+ this.handleBreakpointChange = this.handleBreakpointChange.bind(this);
496
+ this.mediaQuery.addEventListener('change', this.handleBreakpointChange);
497
+ }
498
+
499
+ private handleBreakpointChange = (e: MediaQueryListEvent): void => {
500
+ // console.log('[ds-mobile-tab-bar] handleBreakpointChange:', {
501
+ // matches: e.matches,
502
+ // windowWidth: window.innerWidth,
503
+ // previousIsDesktop: this.isDesktop()
504
+ // });
505
+ this.isDesktop.set(e.matches);
506
+ // Force update the slot when breakpoint changes
507
+ this.updateSlot();
508
+ };
509
+
510
+ private setupTitleRemovalObserver(): void {
511
+ const config = {
512
+ attributes: true,
513
+ attributeFilter: ['title'],
514
+ subtree: true,
515
+ childList: true,
516
+ };
517
+
518
+ this.mutationObserver = new MutationObserver((mutations) => {
519
+ mutations.forEach((mutation) => {
520
+ if (mutation.type === 'attributes' && mutation.attributeName === 'title') {
521
+ const target = mutation.target as HTMLElement;
522
+ if (target.tagName === 'ION-TAB-BUTTON' && target.hasAttribute('title')) {
523
+ target.removeAttribute('title');
524
+ }
525
+ }
526
+ });
527
+ // Also do a sweep after any changes
528
+ this.removeTitleAttributes();
529
+ });
530
+
531
+ this.mutationObserver.observe(this.elementRef.nativeElement, config);
532
+ }
533
+
534
+ private removeTitleAttributes(): void {
535
+ const tabButtons = this.elementRef.nativeElement.querySelectorAll('ion-tab-button');
536
+ tabButtons.forEach((button: HTMLElement) => {
537
+ if (button.hasAttribute('title')) {
538
+ button.removeAttribute('title');
539
+ }
540
+ // Also remove from the native button inside shadow DOM
541
+ const nativeButton = button.shadowRoot?.querySelector('button');
542
+ if (nativeButton?.hasAttribute('title')) {
543
+ nativeButton.removeAttribute('title');
544
+ }
545
+ });
546
+ }
547
+
548
+ private setupActiveTabDetection(): void {
549
+ // Find the parent ion-tabs element
550
+ const ionTabs = this.elementRef.nativeElement.closest('ion-tabs');
551
+ if (!ionTabs) {
552
+ console.warn('ds-mobile-tab-bar: Could not find parent ion-tabs element');
553
+ return;
554
+ }
555
+
556
+ // Listen for tab changes using Ionic events
557
+ ionTabs.addEventListener('ionTabsDidChange', (event: any) => {
558
+ const tabRoute = event.detail?.tab;
559
+ if (tabRoute) {
560
+ this.activeTab.set(tabRoute);
561
+ } else {
562
+ // Fallback: check DOM immediately after event
563
+ setTimeout(() => this.updateActiveTabFromDOM(), 0);
564
+ }
565
+ });
566
+
567
+ // Also listen for tab button clicks
568
+ this.elementRef.nativeElement.addEventListener('click', (event: any) => {
569
+ const button = event.target.closest('ion-tab-button');
570
+ if (button) {
571
+ setTimeout(() => this.updateActiveTabFromDOM(), 50);
572
+ }
573
+ });
574
+
575
+ // Get initial selected tab
576
+ this.updateActiveTabFromDOM();
577
+
578
+ // Watch for selected attribute changes on tab buttons (more reliable)
579
+ const observer = new MutationObserver(() => {
580
+ this.updateActiveTabFromDOM();
581
+ });
582
+
583
+ observer.observe(this.elementRef.nativeElement, {
584
+ attributes: true,
585
+ attributeFilter: ['selected', 'tab'],
586
+ subtree: true,
587
+ childList: true,
588
+ });
589
+
590
+ // Also watch the parent ion-tabs for changes
591
+ observer.observe(ionTabs, {
592
+ attributes: true,
593
+ attributeFilter: ['selected', 'tab'],
594
+ subtree: true,
595
+ });
596
+
597
+ // Periodic check as fallback (in case events don't fire)
598
+ setInterval(() => {
599
+ this.updateActiveTabFromDOM();
600
+ }, 100);
601
+ }
602
+
603
+ private updateActiveTabFromDOM(): void {
604
+ // Check parent ion-tabs for selected tab (most reliable)
605
+ const ionTabs = this.elementRef.nativeElement.closest('ion-tabs');
606
+ if (ionTabs) {
607
+ // Method 1: Check for ion-tab with selected attribute
608
+ const selectedTab = ionTabs.querySelector('ion-tab[selected]');
609
+ if (selectedTab) {
610
+ const tabRoute = selectedTab.getAttribute('tab');
611
+ if (tabRoute) {
612
+ this.activeTab.set(tabRoute);
613
+ return;
614
+ }
615
+ }
616
+
617
+ // Method 2: Check for ion-tab without tab-hidden class (Ionic shows active tab)
618
+ const visibleTab = ionTabs.querySelector('ion-tab:not(.tab-hidden)');
619
+ if (visibleTab) {
620
+ const tabRoute = visibleTab.getAttribute('tab');
621
+ if (tabRoute) {
622
+ this.activeTab.set(tabRoute);
623
+ return;
624
+ }
625
+ }
626
+ }
627
+
628
+ // Method 3: Check tab buttons for selected state
629
+ const tabButtons = this.elementRef.nativeElement.querySelectorAll('ion-tab-button');
630
+ tabButtons.forEach((button: any) => {
631
+ // Check Ionic's native selected property
632
+ if (button.selected === true) {
633
+ const tabRoute = button.getAttribute('tab');
634
+ if (tabRoute) {
635
+ this.activeTab.set(tabRoute);
636
+ }
637
+ }
638
+ });
639
+ }
640
+
641
+ trackByTabId(index: number, tab: TabConfig): string {
642
+ return tab.id;
643
+ }
644
+
645
+ isTabActive(tabRoute: string): boolean {
646
+ const currentActive = this.activeTab();
647
+ // Match by route (primary) or by checking if the tab button is selected
648
+ if (currentActive === tabRoute) {
649
+ return true;
650
+ }
651
+
652
+ // Fallback: check if this button is actually selected in the DOM
653
+ const tabButtons = this.elementRef.nativeElement.querySelectorAll('ion-tab-button');
654
+ for (let i = 0; i < tabButtons.length; i++) {
655
+ const button = tabButtons[i] as any;
656
+ if (button.getAttribute('tab') === tabRoute && button.selected === true) {
657
+ return true;
658
+ }
659
+ }
660
+
661
+ return false;
662
+ }
663
+
664
+ /**
665
+ * Handle avatar click - opens profile menu if configured, otherwise just emits avatarClick
666
+ */
667
+ async handleAvatarClick(): Promise<void> {
668
+ // Emit the basic click event (for backwards compatibility)
669
+ this.avatarClick.emit();
670
+
671
+ // Use input if provided, otherwise fall back to service
672
+ const menuItems = this.profileMenuItems || this.userService.profileMenuItems();
673
+
674
+ // If no menu items configured, just emit and return
675
+ if (!menuItems || menuItems.length === 0) {
676
+ return;
677
+ }
678
+
679
+ // Open the bottom sheet with configured menu items
680
+ const sheet = await this.modalController.create({
681
+ component: DsMobileProfileActionsSheetComponent,
682
+ componentProps: {
683
+ actionGroups: menuItems,
684
+ currentLanguage: 'da', // TODO: Get from language service
685
+ availableLanguages: [
686
+ {
687
+ code: 'da',
688
+ nativeName: 'Dansk',
689
+ englishName: 'Danish',
690
+ flagIcon: '/Assets/country-flags/denmark.svg',
691
+ },
692
+ {
693
+ code: 'en',
694
+ nativeName: 'English',
695
+ englishName: 'English',
696
+ flagIcon: '/Assets/country-flags/united kingdom.svg',
697
+ },
698
+ {
699
+ code: 'sv',
700
+ nativeName: 'Svenska',
701
+ englishName: 'Swedish',
702
+ flagIcon: '/Assets/country-flags/sweden.svg',
703
+ },
704
+ {
705
+ code: 'no',
706
+ nativeName: 'Norsk',
707
+ englishName: 'Norwegian',
708
+ flagIcon: '/Assets/country-flags/norway.svg',
709
+ },
710
+ {
711
+ code: 'de',
712
+ nativeName: 'Deutsch',
713
+ englishName: 'German',
714
+ flagIcon: '/Assets/country-flags/germany.svg',
715
+ },
716
+ ],
717
+ },
718
+ breakpoints: [0, 1],
719
+ initialBreakpoint: 1,
720
+ handle: true,
721
+ cssClass: ['ds-bottom-sheet', 'auto-height'],
722
+ });
723
+
724
+ await sheet.present();
725
+ disableModalShadowPointerEvents(sheet);
726
+
727
+ const result = await sheet.onWillDismiss<ActionResult>();
728
+ if (result.data?.action) {
729
+ // Emit the selected action to parent
730
+ this.profileActionSelected.emit(result.data);
731
+ // Also notify globally via UserService
732
+ this.userService.notifyProfileAction(result.data);
733
+ }
734
+ }
735
+ }