@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,578 @@
1
+ import { Component, signal, ViewChild, ElementRef, AfterViewInit, OnInit, inject } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { ModalController, IonContent } from '@ionic/angular/standalone';
5
+ import { Capacitor } from '@capacitor/core';
6
+ import { Keyboard } from '@capacitor/keyboard';
7
+ import { FilePicker } from '@capawesome/capacitor-file-picker';
8
+ import { StatusBar } from '@capacitor/status-bar';
9
+ import { DsIconButtonComponent } from '@propbinder/design-system';
10
+ import { WhitelabelService } from '../../services/whitelabel.service';
11
+ import { DsMobileBottomSheetHeaderComponent } from './ds-mobile-bottom-sheet-header';
12
+
13
+ /**
14
+ * DsMobilePostCreateBottomSheetComponent
15
+ *
16
+ * Bottom sheet modal for creating new posts in the community feed.
17
+ * This is the modal content that gets displayed in the bottom sheet.
18
+ * Features Threads-inspired interface with rich text editing capabilities.
19
+ *
20
+ * Auto-focuses the textarea and brings up the keyboard when opened.
21
+ *
22
+ * Usage: Use with DsMobileBottomSheetService to present as a bottom sheet
23
+ */
24
+ @Component({
25
+ selector: 'ds-mobile-post-create-bottom-sheet',
26
+ standalone: true,
27
+ imports: [
28
+ CommonModule,
29
+ FormsModule,
30
+ IonContent,
31
+ DsIconButtonComponent,
32
+ DsMobileBottomSheetHeaderComponent
33
+ ],
34
+ styles: [`
35
+ :host {
36
+ display: flex;
37
+ flex-direction: column;
38
+ height: 100%;
39
+ }
40
+
41
+ /* ============================================
42
+ CONTENT AREA
43
+ ============================================ */
44
+
45
+ ion-content {
46
+ --background: var(--color-background-neutral-primary, #ffffff);
47
+ --padding-top: 0;
48
+ --padding-bottom: 0;
49
+ }
50
+
51
+ .post-create-container {
52
+ padding: 24px 16px 16px;
53
+ max-width: 640px;
54
+ margin: 0 auto;
55
+ }
56
+
57
+ .post-composer {
58
+ display: flex;
59
+ gap: 12px;
60
+ align-items: flex-start;
61
+ }
62
+
63
+ .post-composer__main {
64
+ flex: 1;
65
+ min-width: 0;
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 12px;
69
+ }
70
+
71
+ .post-composer__header {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 8px;
75
+ height: 32px;
76
+ }
77
+
78
+ .post-composer__username {
79
+ font-family: 'Brockmann', sans-serif;
80
+ font-size: var(--font-size-base);
81
+ font-weight: 600;
82
+ line-height: 20px;
83
+ letter-spacing: -0.3px;
84
+ color: var(--color-text-primary, #1a1a1a);
85
+ }
86
+
87
+ .post-composer__textarea {
88
+ width: 100%;
89
+ min-height: 60px;
90
+ max-height: 400px;
91
+ border: none;
92
+ outline: none;
93
+ resize: none;
94
+ font-family: 'Brockmann', sans-serif;
95
+ font-size: var(--font-size-base);
96
+ font-weight: 400;
97
+ line-height: 22px;
98
+ letter-spacing: -0.3px;
99
+ color: var(--color-text-primary, #1a1a1a);
100
+ background: transparent;
101
+ padding: 0;
102
+ cursor: text;
103
+ overflow-y: auto;
104
+ /* Auto-resize as user types */
105
+ field-sizing: content;
106
+ }
107
+
108
+ .post-composer__textarea::placeholder {
109
+ color: var(--color-text-tertiary, #999999);
110
+ }
111
+
112
+ /* Visual focus indicator - helps users see the textarea is ready */
113
+ .post-composer__textarea:focus {
114
+ outline: none;
115
+ }
116
+
117
+ /* Subtle animation to draw attention when empty */
118
+ @keyframes gentlePulse {
119
+ 0%, 100% { opacity: 1; }
120
+ 50% { opacity: 0.6; }
121
+ }
122
+
123
+ .post-composer__textarea:not(:focus):empty + .focus-hint {
124
+ animation: gentlePulse 2s ease-in-out 1;
125
+ }
126
+
127
+ .post-composer__actions {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 8px;
131
+ padding-top: 12px;
132
+ }
133
+
134
+ .post-composer__actions ds-icon-button::ng-deep button {
135
+ width: 44px;
136
+ height: 44px;
137
+ border-radius: 50%;
138
+ }
139
+
140
+ /* ============================================
141
+ IMAGE PREVIEW
142
+ ============================================ */
143
+
144
+ .image-previews {
145
+ display: flex;
146
+ flex-wrap: wrap;
147
+ gap: 8px;
148
+ margin-top: 12px;
149
+ }
150
+
151
+ .image-preview {
152
+ position: relative;
153
+ width: 96px;
154
+ height: 96px;
155
+ border-radius: 12px;
156
+ overflow: visible;
157
+ }
158
+
159
+ .preview-image {
160
+ width: 100%;
161
+ height: 100%;
162
+ display: block;
163
+ border-radius: 12px;
164
+ border: 1px solid var(--border-color-default);
165
+ object-fit: cover;
166
+ }
167
+
168
+ .remove-image-btn {
169
+ position: absolute;
170
+ top: -8px;
171
+ right: -8px;
172
+ width: 24px;
173
+ height: 24px;
174
+ border-radius: 50%;
175
+ background: rgba(0, 0, 0, 0.6);
176
+ backdrop-filter: blur(8px);
177
+ border: 2px solid white;
178
+ color: white;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ cursor: pointer;
183
+ transition: all 0.2s ease;
184
+ padding: 0;
185
+ }
186
+
187
+ .remove-image-btn:hover {
188
+ background: rgba(0, 0, 0, 0.8);
189
+ transform: scale(1.05);
190
+ }
191
+
192
+ .remove-image-btn:active {
193
+ transform: scale(0.95);
194
+ }
195
+
196
+ /* ============================================
197
+ MOBILE OPTIMIZATIONS
198
+ ============================================ */
199
+
200
+ @media (max-width: 768px) {
201
+ .post-create-container {
202
+ padding: 12px 16px 24px;
203
+ }
204
+
205
+ .post-composer__textarea {
206
+ min-height: 60px;
207
+ max-height: 300px;
208
+ /* Make tap target larger on mobile */
209
+ padding: 8px;
210
+ margin: -8px;
211
+ }
212
+ }
213
+ `],
214
+ template: `
215
+ <!-- Header with cancel and post buttons -->
216
+ <ds-mobile-bottom-sheet-header
217
+ [title]="modalTitle()"
218
+ leftButtonLabel="Annuller"
219
+ [rightButtonLabel]="submitButtonLabel()"
220
+ [rightButtonDisabled]="!canPost()"
221
+ (leftButtonClick)="handleCancel()"
222
+ (rightButtonClick)="handlePost()">
223
+ </ds-mobile-bottom-sheet-header>
224
+
225
+ <!-- Content -->
226
+ <ion-content>
227
+ <div class="post-create-container">
228
+ <div class="post-composer">
229
+ <div class="post-composer__main">
230
+ <textarea
231
+ #textareaInput
232
+ class="post-composer__textarea"
233
+ [(ngModel)]="postContent"
234
+ [placeholder]="placeholder()"
235
+ [readonly]="isReadonly"
236
+ (input)="handleInput()"
237
+ (focus)="handleFocus()"
238
+ inputmode="text"
239
+ enterkeyhint="done"
240
+ rows="1">
241
+ </textarea>
242
+
243
+ <!-- Image Previews -->
244
+ @if (selectedImages().length > 0) {
245
+ <div class="image-previews">
246
+ @for (image of selectedImages(); track image; let i = $index) {
247
+ <div class="image-preview">
248
+ <img [src]="image" alt="Selected image" class="preview-image" />
249
+ <button
250
+ class="remove-image-btn"
251
+ (click)="handleRemoveImage(i)"
252
+ type="button"
253
+ aria-label="Fjern billede">
254
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
255
+ <path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
256
+ </svg>
257
+ </button>
258
+ </div>
259
+ }
260
+ </div>
261
+ }
262
+
263
+ <div class="post-composer__actions">
264
+ <ds-icon-button
265
+ icon="remixImageLine"
266
+ variant="secondary"
267
+ size="md"
268
+ (clicked)="handleAddImage()"
269
+ aria-label="Tilføj billede">
270
+ </ds-icon-button>
271
+ <ds-icon-button
272
+ icon="remixAttachmentLine"
273
+ variant="secondary"
274
+ size="md"
275
+ (clicked)="handleAddAttachment()"
276
+ aria-label="Tilføj vedhæftning">
277
+ </ds-icon-button>
278
+
279
+ <!-- Hidden file input for file selection -->
280
+ <input
281
+ #fileInput
282
+ type="file"
283
+ accept="*/*"
284
+ multiple
285
+ (change)="handleFileSelect($event)"
286
+ style="display: none;"
287
+ aria-hidden="true"
288
+ />
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </ion-content>
294
+ `
295
+ })
296
+ export class DsMobilePostCreateBottomSheetComponent implements AfterViewInit, OnInit {
297
+ @ViewChild('textareaInput') textareaInput?: ElementRef<HTMLTextAreaElement>;
298
+ @ViewChild('fileInput') fileInput?: ElementRef<HTMLInputElement>;
299
+
300
+ private whitelabelService = inject(WhitelabelService);
301
+
302
+ // Optional input to control auto-focus behavior
303
+ autoFocus = true;
304
+
305
+ // Control readonly state for keyboard trick
306
+ isReadonly = true;
307
+
308
+ // Edit mode properties - can be set via componentProps
309
+ isEditMode = false;
310
+ postId?: string;
311
+ initialContent = '';
312
+
313
+ postContent = '';
314
+ selectedImages = signal<string[]>([]);
315
+ username = signal('Lars Mikkelsen');
316
+ placeholder = signal("Hvad er nyt?");
317
+ modalTitle = signal('Nyt opslag');
318
+ submitButtonLabel = signal('Slå op');
319
+
320
+ constructor(
321
+ private modalController: ModalController,
322
+ private elementRef: ElementRef
323
+ ) {}
324
+
325
+ /**
326
+ * Ensure toolbar doesn't have unnecessary padding
327
+ * Modal is already positioned below status bar, so no extra safe area needed
328
+ */
329
+ private applySafeAreaToToolbar(): void {
330
+ try {
331
+ const hostElement = this.elementRef?.nativeElement;
332
+ if (hostElement) {
333
+ const header = hostElement.querySelector('ion-header');
334
+ if (header) {
335
+ const toolbar = header.querySelector('ion-toolbar');
336
+ if (toolbar) {
337
+ const toolbarElement = toolbar as HTMLElement;
338
+ // Ensure toolbar uses standard padding (no safe area since modal is already offset)
339
+ toolbarElement.style.setProperty('--padding-top', '12px', 'important');
340
+ toolbarElement.style.setProperty('--min-height', '56px', 'important');
341
+ }
342
+ }
343
+ }
344
+ } catch (e) {
345
+ console.log('[SafeArea] Failed to apply to toolbar:', e);
346
+ }
347
+ }
348
+
349
+ ngOnInit(): void {
350
+ // Initialize edit mode if provided
351
+ if (this.isEditMode && this.initialContent) {
352
+ this.postContent = this.initialContent;
353
+ this.modalTitle.set('Rediger opslag');
354
+ this.submitButtonLabel.set('Gem');
355
+ }
356
+ }
357
+
358
+ ngAfterViewInit(): void {
359
+ // Apply safe area padding immediately to prevent corruption
360
+ this.applySafeAreaToToolbar();
361
+
362
+ // Auto-resize textarea if there's initial content (edit mode)
363
+ if (this.postContent && this.textareaInput) {
364
+ setTimeout(() => {
365
+ this.resizeTextarea();
366
+ }, 0);
367
+ }
368
+
369
+ // Try to focus IMMEDIATELY - no delay
370
+ // This maximizes our chance of being in user gesture context
371
+ if (this.autoFocus && this.textareaInput) {
372
+ const textarea = this.textareaInput.nativeElement;
373
+
374
+ // Remove readonly immediately
375
+ this.isReadonly = false;
376
+
377
+ // Try focusing with minimal delay
378
+ setTimeout(() => {
379
+ textarea.focus();
380
+ textarea.click();
381
+
382
+ // Explicitly show keyboard
383
+ Keyboard.show().catch(e => console.log('Keyboard.show() not available'));
384
+
385
+ // iOS sometimes needs a second attempt
386
+ setTimeout(() => {
387
+ textarea.focus();
388
+ Keyboard.show().catch(e => console.log('Keyboard.show() not available'));
389
+ }, 100);
390
+ }, 10);
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Ionic lifecycle hook - called when modal enters view
396
+ * At 95% height, this acts more like a page than a modal
397
+ * which might allow keyboard to open
398
+ */
399
+ ionViewDidEnter(): void {
400
+ // Resize textarea in case initial attempt didn't work
401
+ if (this.postContent && this.textareaInput) {
402
+ this.resizeTextarea();
403
+ }
404
+
405
+ // Final focus attempt when view fully enters
406
+ if (this.autoFocus && this.textareaInput) {
407
+ this.isReadonly = false;
408
+ const textarea = this.textareaInput.nativeElement;
409
+
410
+ // Try to focus as if this was a page navigation
411
+ textarea.focus();
412
+ textarea.click();
413
+
414
+ // Explicitly show keyboard
415
+ Keyboard.show().catch(e => console.log('Keyboard.show() not available'));
416
+
417
+ // Set cursor position
418
+ const length = textarea.value.length;
419
+ textarea.setSelectionRange(length, length);
420
+ }
421
+ }
422
+
423
+ handleFocus(): void {
424
+ // When user focuses (or we focus programmatically), remove readonly
425
+ this.isReadonly = false;
426
+ // Explicitly show keyboard
427
+ Keyboard.show().catch(e => console.log('Keyboard.show() not available'));
428
+ }
429
+
430
+ handleInput(): void {
431
+ this.resizeTextarea();
432
+ }
433
+
434
+ /**
435
+ * Auto-resize textarea based on content
436
+ */
437
+ private resizeTextarea(): void {
438
+ if (this.textareaInput) {
439
+ const textarea = this.textareaInput.nativeElement;
440
+ // Reset height to auto to get the correct scrollHeight
441
+ textarea.style.height = 'auto';
442
+ // Set height based on content, respecting min/max from CSS
443
+ textarea.style.height = Math.min(textarea.scrollHeight, 400) + 'px';
444
+ }
445
+ }
446
+
447
+ canPost(): boolean {
448
+ return this.postContent.trim().length > 0 || this.selectedImages().length > 0;
449
+ }
450
+
451
+ async handleCancel(): Promise<void> {
452
+ if (this.postContent.trim().length > 0 || this.selectedImages().length > 0) {
453
+ // Show confirmation
454
+ const confirmed = confirm('Kassér dette opslag?');
455
+ if (confirmed) {
456
+ await this.modalController.dismiss(null, 'cancel');
457
+ }
458
+ } else {
459
+ await this.modalController.dismiss(null, 'cancel');
460
+ }
461
+ }
462
+
463
+ async handlePost(): Promise<void> {
464
+ if (!this.canPost()) return;
465
+
466
+ if (this.isEditMode) {
467
+ console.log('Updating post:', this.postId, this.postContent);
468
+ } else {
469
+ console.log('Creating post:', this.postContent, 'with images:', this.selectedImages().length);
470
+ }
471
+
472
+ // Pass the post content, images, and edit info back to the parent
473
+ await this.modalController.dismiss(
474
+ {
475
+ content: this.postContent,
476
+ images: this.selectedImages(),
477
+ timestamp: new Date(),
478
+ isEdit: this.isEditMode,
479
+ postId: this.postId
480
+ },
481
+ 'post'
482
+ );
483
+ }
484
+
485
+ async handleAddImage(): Promise<void> {
486
+ console.log('Add image button clicked');
487
+
488
+ // Re-apply safe area padding before opening camera (preventive)
489
+ // This ensures the value is locked in before iOS corrupts it
490
+ this.applySafeAreaToToolbar();
491
+
492
+ try {
493
+ console.log('Requesting photo from library...');
494
+
495
+ const result = await FilePicker.pickImages({
496
+ limit: 1,
497
+ });
498
+ const image = result.files?.[0];
499
+
500
+ console.log('Photo selected successfully:', image);
501
+
502
+ // Add the image path to the array
503
+ if (image) {
504
+ const imageSrc = image.path ? Capacitor.convertFileSrc(image.path) : (image.blob ? URL.createObjectURL(image.blob) : '');
505
+ if (imageSrc) {
506
+ this.selectedImages.update(images => [...images, imageSrc]);
507
+ console.log('Image added to preview:', imageSrc);
508
+ }
509
+ }
510
+
511
+ // Re-apply safe area padding immediately after returning
512
+ // Since we're using fixed values, this won't cause flickering
513
+ requestAnimationFrame(() => {
514
+ this.applySafeAreaToToolbar();
515
+ });
516
+
517
+ // NOTE: Removed restoreStatusBar() call - WhitelabelService now handles
518
+ // status bar state globally, no need to restore after photo selection
519
+
520
+ } catch (error) {
521
+ console.error('Photo selection error:', error);
522
+ // Only show alert for non-cancellation errors
523
+ if (error && typeof error === 'object' && 'message' in error) {
524
+ const errorMessage = (error as any).message;
525
+ if (!errorMessage.includes('cancel')) {
526
+ alert(`Error selecting photo: ${JSON.stringify(error)}`);
527
+ }
528
+ }
529
+ }
530
+ }
531
+
532
+
533
+ handleRemoveImage(index: number): void {
534
+ console.log('Removing image at index:', index);
535
+ this.selectedImages.update(images => images.filter((_, i) => i !== index));
536
+ }
537
+
538
+ handleAddAttachment(): void {
539
+ console.log('Add attachment button clicked');
540
+ // Trigger the hidden file input
541
+ if (this.fileInput) {
542
+ this.fileInput.nativeElement.click();
543
+ }
544
+ }
545
+
546
+ handleFileSelect(event: Event): void {
547
+ const input = event.target as HTMLInputElement;
548
+ const files = input.files;
549
+
550
+ if (!files || files.length === 0) {
551
+ console.log('No files selected');
552
+ return;
553
+ }
554
+
555
+ console.log('Files selected:', files.length);
556
+
557
+ // Process each selected file
558
+ Array.from(files).forEach(file => {
559
+ console.log('File:', file.name, file.type, file.size);
560
+
561
+ // Create a data URL for preview (for images and other files)
562
+ const reader = new FileReader();
563
+ reader.onload = (e) => {
564
+ const result = e.target?.result as string;
565
+ if (result) {
566
+ // Add to selectedImages array for preview
567
+ this.selectedImages.update(images => [...images, result]);
568
+ console.log('File added to preview:', file.name);
569
+ }
570
+ };
571
+ reader.readAsDataURL(file);
572
+ });
573
+
574
+ // Reset the input so the same file can be selected again
575
+ input.value = '';
576
+ }
577
+ }
578
+