@propbinder/mobile-design 0.2.50 → 0.2.53

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/fesm2022/propbinder-mobile-design.mjs +26206 -0
  2. package/fesm2022/propbinder-mobile-design.mjs.map +1 -0
  3. package/index.d.ts +8193 -0
  4. package/package.json +39 -3
  5. package/ng-package.json +0 -24
  6. package/src/animations/page-transitions.ts +0 -165
  7. package/src/components/action-list-item/ds-mobile-action-list-item.ts +0 -102
  8. package/src/components/action-list-item/index.ts +0 -2
  9. package/src/components/app-icon/ds-app-icon.ts +0 -133
  10. package/src/components/app-icon/index.ts +0 -2
  11. package/src/components/attachment-preview/ds-mobile-attachment-preview.css +0 -139
  12. package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +0 -164
  13. package/src/components/attachment-preview/index.ts +0 -1
  14. package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +0 -142
  15. package/src/components/avatar-with-badge/index.ts +0 -2
  16. package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +0 -71
  17. package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +0 -121
  18. package/src/components/booking-modal/ds-mobile-booking-modal.ts +0 -598
  19. package/src/components/booking-modal/ds-mobile-booking-summary.ts +0 -161
  20. package/src/components/booking-modal/index.ts +0 -4
  21. package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +0 -266
  22. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +0 -146
  23. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +0 -156
  24. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +0 -101
  25. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +0 -169
  26. package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +0 -211
  27. package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +0 -578
  28. package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +0 -614
  29. package/src/components/bottom-sheet/index.ts +0 -8
  30. package/src/components/bottom-sheet/modal-shadow-fix.ts +0 -42
  31. package/src/components/card-inline/ds-mobile-card-inline.ts +0 -301
  32. package/src/components/card-inline/index.ts +0 -2
  33. package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +0 -118
  34. package/src/components/card-inline-banner/index.ts +0 -1
  35. package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +0 -120
  36. package/src/components/card-inline-contact/index.ts +0 -1
  37. package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +0 -141
  38. package/src/components/card-inline-file/index.ts +0 -1
  39. package/src/components/chat-modal/ds-mobile-chat-modal.css +0 -159
  40. package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +0 -105
  41. package/src/components/chat-modal/ds-mobile-chat-modal.ts +0 -918
  42. package/src/components/chat-modal/index.ts +0 -8
  43. package/src/components/comment/ds-mobile-comment.ts +0 -568
  44. package/src/components/comment/index.ts +0 -2
  45. package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +0 -182
  46. package/src/components/contact-list-item/index.ts +0 -2
  47. package/src/components/content/ds-mobile-content.ts +0 -139
  48. package/src/components/content/index.ts +0 -2
  49. package/src/components/dropdown/ds-mobile-dropdown.css +0 -199
  50. package/src/components/dropdown/ds-mobile-dropdown.ts +0 -340
  51. package/src/components/dropdown/index.ts +0 -2
  52. package/src/components/ds-mobile-tabs.css +0 -407
  53. package/src/components/ds-mobile-tabs.ts +0 -216
  54. package/src/components/empty-state/ds-mobile-empty-state.ts +0 -120
  55. package/src/components/empty-state/index.ts +0 -2
  56. package/src/components/fab/ds-mobile-fab.ts +0 -315
  57. package/src/components/fab/index.ts +0 -1
  58. package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +0 -121
  59. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +0 -189
  60. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +0 -135
  61. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +0 -656
  62. package/src/components/facility-creation-modal/index.ts +0 -9
  63. package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +0 -105
  64. package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +0 -188
  65. package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +0 -460
  66. package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +0 -134
  67. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +0 -69
  68. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +0 -379
  69. package/src/components/facility-detail-modal/index.ts +0 -2
  70. package/src/components/file-attachment/ds-mobile-file-attachment.ts +0 -164
  71. package/src/components/file-attachment/index.ts +0 -2
  72. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +0 -214
  73. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +0 -84
  74. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +0 -424
  75. package/src/components/handbook-detail-modal/index.ts +0 -3
  76. package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +0 -175
  77. package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +0 -533
  78. package/src/components/handbook-folder/index.ts +0 -4
  79. package/src/components/header-content/ds-mobile-header-content.ts +0 -222
  80. package/src/components/header-content/index.ts +0 -2
  81. package/src/components/illustration/ds-mobile-illustration.ts +0 -124
  82. package/src/components/illustration/index.ts +0 -2
  83. package/src/components/index.ts +0 -124
  84. package/src/components/inline-photo/ds-mobile-inline-photo.ts +0 -361
  85. package/src/components/inline-photo/index.ts +0 -1
  86. package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +0 -132
  87. package/src/components/inline-tabs/index.ts +0 -2
  88. package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +0 -350
  89. package/src/components/interactive-list-item-booking/index.ts +0 -1
  90. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +0 -321
  91. package/src/components/interactive-list-item-inquiry/index.ts +0 -2
  92. package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +0 -237
  93. package/src/components/interactive-list-item-message/index.ts +0 -2
  94. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +0 -549
  95. package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +0 -124
  96. package/src/components/interactive-list-item-post/index.ts +0 -13
  97. package/src/components/lightbox/ds-mobile-lightbox-footer.ts +0 -315
  98. package/src/components/lightbox/ds-mobile-lightbox-header.ts +0 -202
  99. package/src/components/lightbox/ds-mobile-lightbox-image.ts +0 -484
  100. package/src/components/lightbox/ds-mobile-lightbox-pdf.css +0 -377
  101. package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +0 -374
  102. package/src/components/lightbox/ds-mobile-lightbox.css +0 -587
  103. package/src/components/lightbox/ds-mobile-lightbox.service.ts +0 -296
  104. package/src/components/lightbox/ds-mobile-lightbox.ts +0 -529
  105. package/src/components/lightbox/index.ts +0 -22
  106. package/src/components/list-item/ds-mobile-list-item.ts +0 -603
  107. package/src/components/list-item/index.ts +0 -2
  108. package/src/components/list-item-static/ds-mobile-list-item-static.ts +0 -133
  109. package/src/components/list-item-static/index.ts +0 -2
  110. package/src/components/loader-overlay/ds-mobile-loader-overlay.css +0 -49
  111. package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +0 -77
  112. package/src/components/loader-overlay/index.ts +0 -1
  113. package/src/components/logo/ds-logo.ts +0 -95
  114. package/src/components/logo/index.ts +0 -2
  115. package/src/components/message-bubble/ds-mobile-message-bubble.ts +0 -633
  116. package/src/components/message-bubble/index.ts +0 -7
  117. package/src/components/message-composer/ds-mobile-message-composer.ts +0 -1146
  118. package/src/components/message-composer/index.ts +0 -7
  119. package/src/components/modal/ds-mobile-modal.css +0 -163
  120. package/src/components/modal/ds-mobile-modal.service.ts +0 -329
  121. package/src/components/modal/index.ts +0 -8
  122. package/src/components/modal-base/ds-mobile-modal-base.css +0 -378
  123. package/src/components/modal-base/ds-mobile-modal-base.ts +0 -261
  124. package/src/components/modal-base/index.ts +0 -2
  125. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +0 -112
  126. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +0 -93
  127. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +0 -442
  128. package/src/components/new-inquiry-modal/index.ts +0 -4
  129. package/src/components/offline-banner/ds-mobile-offline-banner.ts +0 -135
  130. package/src/components/offline-banner/index.ts +0 -1
  131. package/src/components/page-details/ds-mobile-page-details.css +0 -83
  132. package/src/components/page-details/ds-mobile-page-details.ts +0 -282
  133. package/src/components/page-details/index.ts +0 -2
  134. package/src/components/page-main/ds-mobile-page-main.css +0 -68
  135. package/src/components/page-main/ds-mobile-page-main.ts +0 -421
  136. package/src/components/page-main/index.ts +0 -2
  137. package/src/components/post-composer/ds-mobile-post-composer.ts +0 -140
  138. package/src/components/post-composer/index.ts +0 -2
  139. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +0 -390
  140. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +0 -108
  141. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +0 -722
  142. package/src/components/post-detail-modal/index.ts +0 -9
  143. package/src/components/property-banner/ds-mobile-property-banner.ts +0 -95
  144. package/src/components/property-banner/index.ts +0 -2
  145. package/src/components/section/ds-mobile-section.ts +0 -263
  146. package/src/components/section/index.ts +0 -2
  147. package/src/components/shared/directives/index.ts +0 -2
  148. package/src/components/shared/directives/long-press.directive.ts +0 -212
  149. package/src/components/shared/index.ts +0 -3
  150. package/src/components/shared/mobile-modal-base.ts +0 -457
  151. package/src/components/shared/mobile-page-base.ts +0 -204
  152. package/src/components/swiper/ds-mobile-swiper-with-nav.ts +0 -160
  153. package/src/components/swiper/ds-mobile-swiper.ts +0 -327
  154. package/src/components/swiper/index.ts +0 -3
  155. package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +0 -129
  156. package/src/components/system-message-banner/index.ts +0 -2
  157. package/src/components/tab-bar/ds-mobile-tab-bar.css +0 -533
  158. package/src/components/tab-bar/ds-mobile-tab-bar.ts +0 -735
  159. package/src/components/tab-bar/index.ts +0 -2
  160. package/src/components/tabs/ds-mobile-tabs.css +0 -25
  161. package/src/components/tabs/ds-mobile-tabs.ts +0 -89
  162. package/src/components/tabs/index.ts +0 -2
  163. package/src/components/text-input/ds-text-input.ts +0 -287
  164. package/src/components/text-input/index.ts +0 -2
  165. package/src/examples/booking.page.ts +0 -434
  166. package/src/examples/community.page.ts +0 -776
  167. package/src/examples/handbook.page.ts +0 -324
  168. package/src/examples/home.page.ts +0 -347
  169. package/src/examples/index.ts +0 -12
  170. package/src/examples/inquiries.example.ts +0 -273
  171. package/src/examples/inquiry-detail.example.css +0 -189
  172. package/src/examples/inquiry-detail.example.ts +0 -415
  173. package/src/examples/mobile-tabs-example.component.ts +0 -208
  174. package/src/examples/post-create.page.ts +0 -311
  175. package/src/examples/post-detail.page.ts +0 -296
  176. package/src/examples/sign-in.page.ts +0 -291
  177. package/src/examples/whitelabel-demo-modal.component.ts +0 -1094
  178. package/src/examples/whitelabel-demo-modal.service.ts +0 -77
  179. package/src/models/index.ts +0 -7
  180. package/src/models/post.model.ts +0 -41
  181. package/src/pages/community.page.ts +0 -769
  182. package/src/pages/handbook.page.ts +0 -388
  183. package/src/pages/home.page.ts +0 -303
  184. package/src/pages/index.ts +0 -11
  185. package/src/pages/inquiries.example.ts +0 -273
  186. package/src/pages/inquiry-detail.example.css +0 -189
  187. package/src/pages/inquiry-detail.example.ts +0 -415
  188. package/src/pages/mobile-tabs-example.component.ts +0 -179
  189. package/src/pages/post-create.page.ts +0 -311
  190. package/src/pages/post-detail.page.ts +0 -296
  191. package/src/pages/sign-in.page.ts +0 -291
  192. package/src/pages/whitelabel-demo-modal.component.ts +0 -1094
  193. package/src/pages/whitelabel-demo-modal.service.ts +0 -77
  194. package/src/public-api.ts +0 -6
  195. package/src/services/base-modal.service.ts +0 -101
  196. package/src/services/index.ts +0 -11
  197. package/src/services/posts.service.ts +0 -542
  198. package/src/services/tracking-permission.service.ts +0 -88
  199. package/src/services/user.service.ts +0 -60
  200. package/src/services/whitelabel.service.ts +0 -675
  201. package/tsconfig.lib.json +0 -17
  202. package/tsconfig.lib.prod.json +0 -9
  203. package/tsconfig.spec.json +0 -13
  204. /package/{src/assets → assets}/fonts/Brockmann-Bold.otf +0 -0
  205. /package/{src/assets → assets}/fonts/Brockmann-BoldItalic.otf +0 -0
  206. /package/{src/assets → assets}/fonts/Brockmann-Medium.otf +0 -0
  207. /package/{src/assets → assets}/fonts/Brockmann-MediumItalic.otf +0 -0
  208. /package/{src/assets → assets}/fonts/Brockmann-Regular.otf +0 -0
  209. /package/{src/assets → assets}/fonts/Brockmann-RegularItalic.otf +0 -0
  210. /package/{src/assets → assets}/fonts/Brockmann-SemiBold.otf +0 -0
  211. /package/{src/assets → assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
  212. /package/{src/assets → assets}/fonts/Brockmann_desktop_license.pdf +0 -0
  213. /package/{src/assets → assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
  214. /package/{src/assets → assets}/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
  215. /package/{src/assets → assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
  216. /package/{src/assets → assets}/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
  217. /package/{src/assets → assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
  218. /package/{src/assets → assets}/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
  219. /package/{src/styles → styles}/ionic.css +0 -0
  220. /package/{src/components/shared → styles}/mobile-common.css +0 -0
  221. /package/{src/components/shared → styles}/mobile-page-base.css +0 -0
@@ -1,735 +0,0 @@
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
- }