@propbinder/mobile-design 0.0.1 → 0.0.2

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