@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,457 @@
1
+ import { input, output, Directive, OnInit, OnDestroy, inject, ViewChild } from '@angular/core';
2
+ import { ModalController, IonContent } from '@ionic/angular/standalone';
3
+ import { Keyboard } from '@capacitor/keyboard';
4
+
5
+ /**
6
+ * MobileModalBase
7
+ *
8
+ * Shared base class for mobile modal components.
9
+ * Provides consistent modal behavior, state management, and keyboard handling.
10
+ *
11
+ * **Key Features:**
12
+ * - Loading and error state management
13
+ * - Header configuration (title, meta)
14
+ * - Automatic keyboard height tracking (iOS/Android)
15
+ * - Fixed bottom component support
16
+ * - Consistent close behavior
17
+ *
18
+ * **Usage:**
19
+ * ```typescript
20
+ * export class MyModalComponent extends MobileModalBase {
21
+ * constructor() {
22
+ * super();
23
+ * }
24
+ * }
25
+ * ```
26
+ *
27
+ * @internal This is a base class and should not be used directly.
28
+ */
29
+ @Directive()
30
+ export abstract class MobileModalBase implements OnInit, OnDestroy {
31
+ protected modalController = inject(ModalController);
32
+
33
+ /**
34
+ * Reference to IonContent for accessing scroll element
35
+ */
36
+ @ViewChild(IonContent, { read: IonContent }) protected ionContent?: IonContent;
37
+
38
+ /**
39
+ * ResizeObserver for tracking fixed bottom height
40
+ */
41
+ private fixedBottomObserver?: ResizeObserver;
42
+
43
+ /**
44
+ * Flag to prevent ResizeObserver from updating padding during keyboard animations
45
+ */
46
+ private isKeyboardAnimating = false;
47
+
48
+ /**
49
+ * Loading state - when true, shows loading indicator
50
+ * @default false
51
+ */
52
+ loading = input<boolean>(false);
53
+
54
+ /**
55
+ * Error state - when set, shows error message
56
+ * @default undefined
57
+ */
58
+ error = input<string | undefined>();
59
+
60
+ /**
61
+ * Modal header title
62
+ * @default ''
63
+ */
64
+ headerTitle = input<string>('');
65
+
66
+ /**
67
+ * Modal header metadata (subtitle/secondary text)
68
+ * @default ''
69
+ */
70
+ headerMeta = input<string>('');
71
+
72
+ /**
73
+ * Accessibility label for close button
74
+ * @default 'Close'
75
+ */
76
+ closeButtonLabel = input<string>('Close');
77
+
78
+ /**
79
+ * Enable automatic keyboard height tracking
80
+ * When enabled, sets --keyboard-height CSS variable for sliding content
81
+ * @default false
82
+ */
83
+ enableKeyboardHandling = input<boolean>(false);
84
+
85
+ /**
86
+ * Whether modal has a fixed bottom component
87
+ * Used to manage spacing and keyboard interactions
88
+ * @default false
89
+ */
90
+ hasFixedBottom = input<boolean>(false);
91
+
92
+ /**
93
+ * Content padding for modal content
94
+ * Controls padding inside .modal-main-content
95
+ * - '0' (default) - No padding, use ds-mobile-section for content organization
96
+ * - '20px' - Legacy padding for content without sections
97
+ * - Any custom CSS padding value
98
+ *
99
+ * @default '0'
100
+ */
101
+ contentPadding = input<string>('0');
102
+
103
+ /**
104
+ * Enable auto-height behavior for bottom sheets
105
+ * When true, sets [fullscreen]="false" on ion-content and enforces flex: 0 0 auto
106
+ * @default false
107
+ */
108
+ isAutoHeight = input<boolean>(false);
109
+
110
+ /**
111
+ * Controls how modal content behaves when the keyboard opens.
112
+ * - 'follow': content is pushed to follow keyboard movement
113
+ * - 'overlay': keyboard/footer overlays lower content (no auto scroll push)
114
+ * @default 'follow'
115
+ */
116
+ keyboardContentBehavior = input<'follow' | 'overlay'>('follow');
117
+
118
+ /**
119
+ * Emitted when modal is closed
120
+ */
121
+ closed = output<void>();
122
+
123
+ /**
124
+ * Emitted when keyboard is about to show
125
+ * Provides keyboard height in pixels
126
+ * Child components can listen to this to react to keyboard appearance
127
+ */
128
+ keyboardWillShow = output<number>();
129
+
130
+ /**
131
+ * Emitted when keyboard is about to hide
132
+ * Child components can listen to this to react to keyboard dismissal
133
+ */
134
+ keyboardWillHide = output<void>();
135
+
136
+ ngOnInit(): void {
137
+ if (this.enableKeyboardHandling()) {
138
+ this.setupKeyboardListeners();
139
+ }
140
+
141
+ if (this.hasFixedBottom()) {
142
+ this.setupFixedBottomObserver();
143
+ }
144
+ }
145
+
146
+ ngOnDestroy(): void {
147
+ if (this.enableKeyboardHandling()) {
148
+ this.cleanupKeyboardListeners();
149
+ }
150
+
151
+ if (this.fixedBottomObserver) {
152
+ this.fixedBottomObserver.disconnect();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Close the modal
158
+ * Emits closed event and dismisses the modal
159
+ */
160
+ close(): void {
161
+ this.closed.emit();
162
+ this.modalController.dismiss();
163
+ }
164
+
165
+ /**
166
+ * Set up keyboard event listeners to adjust component position
167
+ * Uses --keyboard-height for fixed bottom composer and adds padding to scroll area.
168
+ * Adjusts scroll position so content smoothly follows the keyboard up (like Messenger/Telegram).
169
+ * Only responds if this modal is the currently active (top-most) modal.
170
+ * @protected
171
+ */
172
+ protected setupKeyboardListeners(): void {
173
+ Keyboard.addListener('keyboardWillShow', async (info) => {
174
+ // Check if this modal is the top-most modal
175
+ const topModal = await this.modalController.getTop();
176
+ if (!topModal || !this.isThisModal(topModal)) {
177
+ // console.log('[MobileModalBase] Keyboard event ignored - not top modal');
178
+ return; // Not the active modal, ignore keyboard event
179
+ }
180
+
181
+ // console.log('[MobileModalBase] 🎹 Keyboard showing, height:', info.keyboardHeight);
182
+ // Set flag to prevent ResizeObserver from interfering
183
+ this.isKeyboardAnimating = true;
184
+
185
+ // Set global keyboard height FIRST so ResizeObserver can use it
186
+ document.documentElement.style.setProperty('--keyboard-height', `${info.keyboardHeight}px`);
187
+
188
+ // Update padding immediately to include keyboard height
189
+ if (this.ionContent) {
190
+ try {
191
+ const scrollElement = await this.ionContent.getScrollElement();
192
+ if (scrollElement) {
193
+ // Get current fixed bottom height and add keyboard height
194
+ const fixedBottomHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--fixed-bottom-height') || '0');
195
+ const totalPadding = this.getScrollPadding(fixedBottomHeight, info.keyboardHeight);
196
+ scrollElement.style.paddingBottom = `${totalPadding}px`;
197
+ // console.log('[MobileModalBase] Updated padding:', totalPadding, '(fixed:', fixedBottomHeight, '+ keyboard:', info.keyboardHeight, ')');
198
+
199
+ // In overlay mode, keep content stationary and let keyboard/footer overlap.
200
+ if (this.isOverlayBehavior()) {
201
+ this.isKeyboardAnimating = false;
202
+ } else {
203
+ // Store current scroll position before keyboard animation
204
+ const currentScrollTop = scrollElement.scrollTop;
205
+ // Animate scroll position to match keyboard
206
+ const startTime = performance.now();
207
+ const duration = 300; // Match keyboard animation duration
208
+
209
+ const animateScroll = (currentTime: number) => {
210
+ const elapsed = currentTime - startTime;
211
+ const progress = Math.min(elapsed / duration, 1);
212
+
213
+ // Ease-out cubic (matches iOS/Android keyboard animation curve)
214
+ const easeProgress = 1 - Math.pow(1 - progress, 3);
215
+
216
+ // Scroll down by keyboard height so content "follows" keyboard up
217
+ scrollElement.scrollTop = currentScrollTop + info.keyboardHeight * easeProgress;
218
+
219
+ if (progress < 1) {
220
+ requestAnimationFrame(animateScroll);
221
+ } else {
222
+ // Animation complete - re-enable ResizeObserver
223
+ // console.log('[MobileModalBase] ✅ Keyboard animation complete');
224
+ this.isKeyboardAnimating = false;
225
+ }
226
+ };
227
+
228
+ requestAnimationFrame(animateScroll);
229
+ }
230
+ }
231
+ } catch (e) {
232
+ // console.log('[MobileModalBase] Could not access scroll element:', e);
233
+ this.isKeyboardAnimating = false;
234
+ }
235
+ } else {
236
+ this.isKeyboardAnimating = false;
237
+ }
238
+
239
+ // Emit event for child components to react to keyboard appearance
240
+ this.keyboardWillShow.emit(info.keyboardHeight);
241
+ }).catch((e) => {
242
+ // console.log('[MobileModalBase] Keyboard listeners not available:', e);
243
+ });
244
+
245
+ Keyboard.addListener('keyboardWillHide', async () => {
246
+ // Check if this modal is the top-most modal
247
+ const topModal = await this.modalController.getTop();
248
+ if (!topModal || !this.isThisModal(topModal)) {
249
+ // console.log('[MobileModalBase] Keyboard hide event ignored - not top modal');
250
+ return; // Not the active modal, ignore keyboard event
251
+ }
252
+
253
+ // console.log('[MobileModalBase] 🎹 Keyboard hiding');
254
+ // Set flag to prevent ResizeObserver from interfering
255
+ this.isKeyboardAnimating = true;
256
+
257
+ // Get keyboard height before resetting (for scroll position adjustment)
258
+ const keyboardHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--keyboard-height') || '0');
259
+
260
+ // Reset keyboard height variable FIRST
261
+ document.documentElement.style.setProperty('--keyboard-height', '0px');
262
+
263
+ // Update padding immediately to remove keyboard height
264
+ if (this.ionContent) {
265
+ try {
266
+ const scrollElement = await this.ionContent.getScrollElement();
267
+ if (scrollElement) {
268
+ // Get current fixed bottom height (without keyboard)
269
+ const fixedBottomHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--fixed-bottom-height') || '0');
270
+ const totalPadding = this.getScrollPadding(fixedBottomHeight, 0);
271
+ scrollElement.style.paddingBottom = `${totalPadding}px`;
272
+ // console.log('[MobileModalBase] Removed keyboard padding, now:', fixedBottomHeight);
273
+
274
+ // In overlay mode, keep content stationary and let keyboard/footer overlap.
275
+ if (this.isOverlayBehavior()) {
276
+ this.isKeyboardAnimating = false;
277
+ } else {
278
+ // Store current scroll position
279
+ const currentScrollTop = scrollElement.scrollTop;
280
+ // Animate scroll position back
281
+ const startTime = performance.now();
282
+ const duration = 300;
283
+
284
+ const animateScroll = (currentTime: number) => {
285
+ const elapsed = currentTime - startTime;
286
+ const progress = Math.min(elapsed / duration, 1);
287
+
288
+ // Ease-out cubic
289
+ const easeProgress = 1 - Math.pow(1 - progress, 3);
290
+
291
+ // Scroll up by keyboard height (reverse the push)
292
+ const newScrollTop = currentScrollTop - keyboardHeight * easeProgress;
293
+ scrollElement.scrollTop = Math.max(0, newScrollTop);
294
+
295
+ if (progress < 1) {
296
+ requestAnimationFrame(animateScroll);
297
+ } else {
298
+ // Animation complete - re-enable ResizeObserver
299
+ // console.log('[MobileModalBase] ✅ Keyboard hide animation complete');
300
+ this.isKeyboardAnimating = false;
301
+ }
302
+ };
303
+
304
+ requestAnimationFrame(animateScroll);
305
+ }
306
+ }
307
+ } catch (e) {
308
+ // console.log('[MobileModalBase] Could not access scroll element:', e);
309
+ this.isKeyboardAnimating = false;
310
+ }
311
+ } else {
312
+ this.isKeyboardAnimating = false;
313
+ }
314
+
315
+ // Emit event for child components to react to keyboard dismissal
316
+ this.keyboardWillHide.emit();
317
+ }).catch((e) => {
318
+ // console.log('[MobileModalBase] Keyboard listeners not available:', e);
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Set up ResizeObserver to track fixed bottom height and apply as CSS variable
324
+ * This allows dynamic bottom padding that adjusts to content
325
+ * @protected
326
+ */
327
+ protected async setupFixedBottomObserver(): Promise<void> {
328
+ // Use measured fixed-bottom height directly; safe-area tuning is handled in CSS.
329
+ const offset = 0;
330
+
331
+ // Small delay to ensure DOM is ready
332
+ setTimeout(async () => {
333
+ const fixedBottom = document.querySelector('.modal-fixed-bottom');
334
+ // console.log('[MobileModalBase] Fixed bottom element:', fixedBottom, 'Offset:', offset);
335
+
336
+ if (fixedBottom) {
337
+ this.fixedBottomObserver = new ResizeObserver(async (entries) => {
338
+ // Skip updates during keyboard animations to prevent conflicts
339
+ if (this.isKeyboardAnimating) {
340
+ // console.log('[MobileModalBase] 🚫 ResizeObserver - skipping update during keyboard animation');
341
+ return;
342
+ }
343
+
344
+ // console.log('[MobileModalBase] ✅ ResizeObserver - processing update');
345
+ for (const entry of entries) {
346
+ // Use getBoundingClientRect to get full border-box height (includes padding)
347
+ const height = (entry.target as HTMLElement).getBoundingClientRect().height;
348
+ // Add platform-specific offset
349
+ const totalHeight = height + offset;
350
+
351
+ // CRITICAL: Include keyboard height if keyboard is visible!
352
+ const keyboardHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--keyboard-height') || '0');
353
+ const paddingWithKeyboard = this.getScrollPadding(totalHeight, keyboardHeight);
354
+
355
+ // console.log('[MobileModalBase] ResizeObserver - height:', height, 'totalHeight:', totalHeight, 'keyboard:', keyboardHeight, 'finalPadding:', paddingWithKeyboard);
356
+ document.documentElement.style.setProperty('--fixed-bottom-height', `${totalHeight}px`);
357
+
358
+ // Also update scroll element padding (including keyboard height!)
359
+ if (this.ionContent) {
360
+ try {
361
+ const scrollElement = await this.ionContent.getScrollElement();
362
+ if (scrollElement) {
363
+ // Get PREVIOUS padding to calculate the difference
364
+ const previousPadding = parseFloat(scrollElement.style.paddingBottom || '0');
365
+ const paddingDifference = paddingWithKeyboard - previousPadding;
366
+
367
+ // Update padding
368
+ scrollElement.style.paddingBottom = `${paddingWithKeyboard}px`;
369
+
370
+ // CRITICAL: Adjust scroll to maintain visual position
371
+ // When padding increases, we need to scroll down by the same amount
372
+ if (!this.isOverlayBehavior() && paddingDifference !== 0) {
373
+ const currentScrollTop = scrollElement.scrollTop;
374
+ scrollElement.scrollTop = currentScrollTop + paddingDifference;
375
+ // console.log('[MobileModalBase] Adjusted scroll by', paddingDifference, 'px (from', currentScrollTop, 'to', scrollElement.scrollTop, ')');
376
+ }
377
+ }
378
+ } catch (e) {
379
+ // console.log('[MobileModalBase] Could not update scroll element padding:', e);
380
+ }
381
+ }
382
+ }
383
+ });
384
+
385
+ this.fixedBottomObserver.observe(fixedBottom);
386
+
387
+ // Set initial height immediately (with platform-specific offset)
388
+ const initialHeight = fixedBottom.getBoundingClientRect().height;
389
+ const initialTotal = initialHeight + offset;
390
+ // console.log('[MobileModalBase] Initial height:', initialHeight, 'initialTotal:', initialTotal);
391
+ document.documentElement.style.setProperty('--fixed-bottom-height', `${initialTotal}px`);
392
+
393
+ // Set initial scroll element padding
394
+ if (this.ionContent) {
395
+ try {
396
+ const scrollElement = await this.ionContent.getScrollElement();
397
+ if (scrollElement) {
398
+ // Include keyboard height if present
399
+ const keyboardHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--keyboard-height') || '0');
400
+ const paddingWithKeyboard = this.getScrollPadding(initialTotal, keyboardHeight);
401
+ scrollElement.style.paddingBottom = `${paddingWithKeyboard}px`;
402
+ }
403
+ } catch (e) {
404
+ // console.log('[MobileModalBase] Could not set initial scroll element padding:', e);
405
+ }
406
+ }
407
+ }
408
+ }, 100);
409
+ }
410
+
411
+ /**
412
+ * Clean up keyboard event listeners
413
+ * @protected
414
+ */
415
+ protected cleanupKeyboardListeners(): void {
416
+ Keyboard.removeAllListeners().catch((e) => {
417
+ // console.log('[MobileModalBase] Keyboard cleanup not available:', e);
418
+ });
419
+ }
420
+
421
+ /**
422
+ * Check if the given modal element is this modal component's parent modal
423
+ * @protected
424
+ */
425
+ protected isThisModal(modalElement: HTMLIonModalElement): boolean {
426
+ // The modal element should contain this component
427
+ // We traverse up from the component to find the ion-modal
428
+ let element = document.querySelector('ds-mobile-modal-base')?.parentElement;
429
+ while (element) {
430
+ if (element === modalElement) {
431
+ return true;
432
+ }
433
+ if (element.tagName === 'ION-MODAL') {
434
+ return element === modalElement;
435
+ }
436
+ element = element.parentElement;
437
+ }
438
+ return false;
439
+ }
440
+
441
+ /**
442
+ * Returns true when keyboard should overlay content without push-scrolling.
443
+ */
444
+ private isOverlayBehavior(): boolean {
445
+ return this.keyboardContentBehavior() === 'overlay';
446
+ }
447
+
448
+ /**
449
+ * Computes scroll bottom inset for current keyboard behavior.
450
+ */
451
+ private getScrollPadding(fixedBottomHeight: number, keyboardHeight: number): number {
452
+ if (this.isOverlayBehavior()) {
453
+ return fixedBottomHeight;
454
+ }
455
+ return fixedBottomHeight + keyboardHeight;
456
+ }
457
+ }
@@ -0,0 +1,204 @@
1
+ import { input, computed, signal, Directive, OnDestroy } from '@angular/core';
2
+ import { Network } from '@capacitor/network';
3
+
4
+ /**
5
+ * Content width preset values
6
+ * - 'narrow' - 640px max width (reading content)
7
+ * - 'standard' - 1024px max width (default)
8
+ * - 'wide' - 1440px max width (dashboards)
9
+ * - 'full' - 100% width (no max)
10
+ */
11
+ export type ContentWidth = 'narrow' | 'standard' | 'wide' | 'full';
12
+
13
+ /**
14
+ * Network status type
15
+ */
16
+ export type NetworkStatus = 'online' | 'offline' | 'unknown';
17
+
18
+ /**
19
+ * MobilePageBase
20
+ *
21
+ * Shared base class for mobile page components (ds-mobile-page-main, ds-mobile-page-details).
22
+ * Provides consistent content width control and network status monitoring across all page types.
23
+ *
24
+ * **Padding Strategy:**
25
+ * - All pages use 20px horizontal padding globally
26
+ * - For tappable lists, use negative margins (e.g., margin: 0 -8px) to create full-width sections
27
+ * - This approach simplifies padding management and provides consistency
28
+ *
29
+ * **Network Monitoring:**
30
+ * - Tracks online/offline status via Capacitor Network plugin (native) or browser API (web)
31
+ * - Exposes `isOffline()` computed signal for easy consumption by child components
32
+ * - Pages can use this to conditionally disable features or show offline indicators
33
+ *
34
+ * @internal This is a base class and should not be used directly.
35
+ */
36
+ @Directive()
37
+ export abstract class MobilePageBase implements OnDestroy {
38
+ /**
39
+ * Shows a loading overlay above page content area.
40
+ *
41
+ * Non-breaking: defaults to false, so existing pages are unchanged
42
+ * until they explicitly opt in.
43
+ */
44
+ contentLoading = input<boolean>(false);
45
+
46
+ /**
47
+ * Maximum content width (desktop only)
48
+ *
49
+ * **Options:**
50
+ * - `'narrow'` (640px) - For reading content, forms
51
+ * - `'standard'` (1024px) - Default for most pages
52
+ * - `'wide'` (1440px) - For dashboards, tables
53
+ * - `'full'` - No max-width constraint
54
+ *
55
+ * **Note:** Only applies on desktop (>= 768px). Mobile is always full width.
56
+ *
57
+ * @default 'standard'
58
+ *
59
+ * @example
60
+ * ```html
61
+ * <!-- Narrow reading layout -->
62
+ * <ds-mobile-page-main title="Article" contentWidth="narrow">
63
+ *
64
+ * <!-- Wide dashboard -->
65
+ * <ds-mobile-page-main title="Dashboard" contentWidth="wide">
66
+ * ```
67
+ */
68
+ contentWidth = input<ContentWidth>('standard');
69
+
70
+ /**
71
+ * Resolved max-width value (computed)
72
+ * Maps preset values to pixel values
73
+ *
74
+ * @internal
75
+ */
76
+ protected maxWidthValue = computed(() => {
77
+ const w = this.contentWidth();
78
+
79
+ const widthMap: Record<ContentWidth, string> = {
80
+ 'narrow': '640px',
81
+ 'standard': '1024px',
82
+ 'wide': '1440px',
83
+ 'full': '100%'
84
+ };
85
+
86
+ return widthMap[w];
87
+ });
88
+
89
+ /**
90
+ * Network status signal
91
+ * Tracks current online/offline state
92
+ *
93
+ * @internal
94
+ */
95
+ protected networkStatus = signal<NetworkStatus>('unknown');
96
+
97
+ /**
98
+ * Is the device currently offline?
99
+ * Public computed signal for consumption by child components and pages
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * // In a page component
104
+ * @ViewChild(DsMobilePageMainComponent) pageComponent!: DsMobilePageMainComponent;
105
+ *
106
+ * get isOffline() {
107
+ * return this.pageComponent?.isOffline();
108
+ * }
109
+ * ```
110
+ */
111
+ public isOffline = computed(() => this.networkStatus() === 'offline');
112
+
113
+ /**
114
+ * Is the device currently online?
115
+ * Public computed signal for consumption by child components and pages
116
+ */
117
+ public isOnline = computed(() => this.networkStatus() === 'online');
118
+
119
+ /**
120
+ * Network listener ID for Capacitor Network plugin
121
+ * Used to clean up listener on destroy
122
+ *
123
+ * @internal
124
+ */
125
+ private networkListenerId?: string;
126
+
127
+ /**
128
+ * Browser API event handlers
129
+ * Stored as class properties for proper cleanup
130
+ *
131
+ * @internal
132
+ */
133
+ private handleOnline = () => {
134
+ this.networkStatus.set('online');
135
+ };
136
+
137
+ private handleOffline = () => {
138
+ this.networkStatus.set('offline');
139
+ };
140
+
141
+ /**
142
+ * Initialize network status monitoring
143
+ * Uses Capacitor Network plugin for native apps, browser API for web
144
+ *
145
+ * @param isNative - Whether running on native platform (iOS/Android)
146
+ * @internal Called by child components in ngAfterViewInit
147
+ */
148
+ protected async initNetworkMonitoring(isNative: boolean): Promise<void> {
149
+ if (isNative) {
150
+ try {
151
+ // Try to use Capacitor Network plugin for native apps
152
+ const status = await Network.getStatus();
153
+ this.networkStatus.set(status.connected ? 'online' : 'offline');
154
+
155
+ // Listen for network status changes
156
+ const listener = await Network.addListener('networkStatusChange', (status: any) => {
157
+ this.networkStatus.set(status.connected ? 'online' : 'offline');
158
+ });
159
+
160
+ // Store listener ID for cleanup
161
+ this.networkListenerId = 'networkStatusChange';
162
+ } catch (error) {
163
+ // Fallback to browser API if Capacitor plugin fails
164
+ console.warn('Capacitor Network plugin not available, falling back to browser API', error);
165
+ this.initBrowserNetworkMonitoring();
166
+ }
167
+ } else {
168
+ // Use browser API for web/PWA
169
+ this.initBrowserNetworkMonitoring();
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Initialize browser-based network monitoring
175
+ * Uses navigator.onLine and window events
176
+ *
177
+ * @internal
178
+ */
179
+ private initBrowserNetworkMonitoring(): void {
180
+ // Set initial status from browser
181
+ this.networkStatus.set(navigator.onLine ? 'online' : 'offline');
182
+
183
+ // Listen for online/offline events
184
+ window.addEventListener('online', this.handleOnline);
185
+ window.addEventListener('offline', this.handleOffline);
186
+ }
187
+
188
+ /**
189
+ * Cleanup network monitoring listeners
190
+ * Called automatically on component destroy
191
+ */
192
+ ngOnDestroy(): void {
193
+ // Clean up Capacitor listener if it exists
194
+ if (this.networkListenerId) {
195
+ Network.removeAllListeners().catch((err: any) => {
196
+ console.warn('Failed to remove network listeners', err);
197
+ });
198
+ }
199
+
200
+ // Clean up browser event listeners
201
+ window.removeEventListener('online', this.handleOnline);
202
+ window.removeEventListener('offline', this.handleOffline);
203
+ }
204
+ }