@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,516 @@
1
+ import { Component, input, output, model, signal, computed } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { DsAvatarComponent } from '@propbinder/design-system';
4
+ import { DsIconComponent } from '@propbinder/design-system';
5
+ import { DsIconButtonComponent } from '@propbinder/design-system';
6
+ import { Haptics, ImpactStyle } from '@capacitor/haptics';
7
+
8
+ /**
9
+ * DsMobileCommentComponent
10
+ *
11
+ * Individual comment component for post discussions.
12
+ * Displays user comments with avatar, content, and like action.
13
+ *
14
+ * @example
15
+ * ```html
16
+ * <ds-mobile-comment
17
+ * [authorName]="'John Doe'"
18
+ * [authorRole]="'Tenant'"
19
+ * [timestamp]="'1h ago'"
20
+ * [avatarInitials]="'JD'"
21
+ * [content]="'Great post!'">
22
+ * </ds-mobile-comment>
23
+ * ```
24
+ */
25
+ @Component({
26
+ selector: 'ds-mobile-comment',
27
+ standalone: true,
28
+ imports: [CommonModule, DsAvatarComponent, DsIconComponent, DsIconButtonComponent],
29
+ styleUrls: ['../shared/mobile-common.css'],
30
+ host: {
31
+ '[class.clickable]': 'clickable()',
32
+ '(click)': 'handleCommentClick($event)',
33
+ '(touchstart)': 'handleTouchStart($event)',
34
+ '(touchend)': 'handleTouchEnd($event)',
35
+ '(touchmove)': 'handleTouchMove($event)',
36
+ '(contextmenu)': 'handleContextMenu($event)'
37
+ },
38
+ styles: [`
39
+ :host {
40
+ display: flex;
41
+ gap: 12px;
42
+ padding: 8px;
43
+ position: relative;
44
+ border-radius: 16px;
45
+ transition: all 0.2s ease;
46
+ background: var(--color-background-primary, #ffffff);
47
+ margin-bottom: 8px;
48
+ margin-left: -8px;
49
+ margin-right: -8px;
50
+ }
51
+
52
+ :host:last-child {
53
+ margin-bottom: 0;
54
+ }
55
+
56
+ :host::after {
57
+ content: '';
58
+ position: absolute;
59
+ bottom: -4px;
60
+ /* Align with comment content: padding (8px) + avatar (32px) + gap (12px) */
61
+ left: 44px;
62
+ /* Align with comment content right edge: padding (8px) from right */
63
+ right: 8px;
64
+ height: 1px;
65
+ background: var(--border-color-default);
66
+ }
67
+
68
+ :host:last-child::after {
69
+ display: none;
70
+ }
71
+
72
+ :host.clickable {
73
+ cursor: pointer;
74
+ }
75
+
76
+ :host.clickable:active {
77
+ background: var(--color-background-neutral-primary-hover, #f5f5f5);
78
+ }
79
+
80
+ .avatar-wrapper {
81
+ position: relative;
82
+ display: flex;
83
+ align-items: flex-start;
84
+ justify-content: center;
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .comment-content {
89
+ flex: 1;
90
+ min-width: 0;
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 4px;
94
+ }
95
+
96
+ .comment-header {
97
+ display: flex;
98
+ align-items: baseline;
99
+ gap: 6px;
100
+ flex-wrap: wrap;
101
+ }
102
+
103
+ /* Wrapper for like and more actions */
104
+ .header-actions {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 4px;
108
+ margin-left: auto;
109
+ }
110
+
111
+ /* Desktop more actions button - using ds-icon-button */
112
+ .desktop-more-button {
113
+ display: none; /* Hidden by default (mobile) */
114
+ }
115
+
116
+ /* Make button circular */
117
+ .desktop-more-button::ng-deep button {
118
+ border-radius: 50% !important;
119
+ }
120
+
121
+ /* Show only on desktop with hover capability */
122
+ @media (hover: hover) and (pointer: fine) {
123
+ .desktop-more-button {
124
+ display: block;
125
+ }
126
+ }
127
+
128
+ /* Author styles imported from mobile-common.css */
129
+
130
+ .action-like {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 2px;
134
+ color: var(--color-text-secondary, #737373);
135
+ cursor: pointer;
136
+ transition: color 0.2s ease;
137
+ user-select: none;
138
+ -webkit-tap-highlight-color: transparent;
139
+ font-family: 'Brockmann', sans-serif;
140
+ font-size: var(--font-size-xs);
141
+ font-weight: 500;
142
+ line-height: 18px;
143
+ }
144
+
145
+ .like-count {
146
+ opacity: 1;
147
+ }
148
+
149
+ .like-count.hidden {
150
+ opacity: 0;
151
+ }
152
+
153
+ .action-like.active {
154
+ color: #f91880;
155
+ }
156
+
157
+ .icon-wrapper {
158
+ position: relative;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ }
163
+
164
+ .icon-pulse {
165
+ position: absolute;
166
+ top: 50%;
167
+ left: 50%;
168
+ transform: translate(-50%, -50%);
169
+ opacity: 0;
170
+ pointer-events: none;
171
+ }
172
+
173
+ .icon-pulse.animating {
174
+ animation: pulse 0.4s cubic-bezier(0.4, 0, 0.2, 1);
175
+ }
176
+
177
+ @keyframes pulse {
178
+ 0% {
179
+ transform: translate(-50%, -50%) scale(1);
180
+ opacity: 0.8;
181
+ }
182
+ 100% {
183
+ transform: translate(-50%, -50%) scale(2.5);
184
+ opacity: 0;
185
+ }
186
+ }
187
+
188
+ .comment-text {
189
+ font-family: 'Brockmann', sans-serif;
190
+ font-size: var(--font-size-sm);
191
+ font-weight: 400;
192
+ line-height: 22px;
193
+ letter-spacing: -0.3px;
194
+ color: var(--color-text-primary, #1a1a1a);
195
+ white-space: pre-wrap;
196
+ word-wrap: break-word;
197
+ }
198
+
199
+ .comment-text ::ng-deep .mention {
200
+ color: var(--color-brand-base, #6B5FF5) !important;
201
+ font-weight: 600;
202
+ }
203
+
204
+ .comment-actions {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 12px;
208
+ margin-top: 4px;
209
+ }
210
+
211
+ .action-reply {
212
+ font-family: 'Brockmann', sans-serif;
213
+ font-size: var(--font-size-sm);
214
+ font-weight: 500;
215
+ line-height: 18px;
216
+ color: var(--color-text-secondary, #737373);
217
+ cursor: pointer;
218
+ user-select: none;
219
+ -webkit-tap-highlight-color: transparent;
220
+ transition: color 0.2s ease;
221
+ }
222
+
223
+ .action-reply:hover {
224
+ color: var(--color-text-primary, #1a1a1a);
225
+ }
226
+
227
+ .action-edit {
228
+ font-family: 'Brockmann', sans-serif;
229
+ font-size: var(--font-size-sm);
230
+ font-weight: 500;
231
+ line-height: 18px;
232
+ color: var(--color-text-secondary, #737373);
233
+ cursor: pointer;
234
+ user-select: none;
235
+ -webkit-tap-highlight-color: transparent;
236
+ transition: color 0.2s ease;
237
+ }
238
+
239
+ .action-edit:hover {
240
+ color: var(--color-text-primary, #1a1a1a);
241
+ }
242
+ `],
243
+ template: `
244
+ <div class="avatar-wrapper">
245
+ <ds-avatar
246
+ [initials]="avatarInitials()"
247
+ [type]="avatarType()"
248
+ size="sm" />
249
+ </div>
250
+
251
+ <div class="comment-content">
252
+ <div class="comment-header">
253
+ <span class="author-name">{{ authorName() }}</span>
254
+ <span class="author-meta">{{ authorRole() }} · {{ timestamp() }}</span>
255
+
256
+ <!-- Wrapper for like and more actions -->
257
+ <div class="header-actions">
258
+ <div
259
+ class="action-like"
260
+ [class.active]="isLiked()"
261
+ (click)="toggleLike()">
262
+ <span class="like-count" [class.hidden]="likeCount() === 0">{{ likeCount() }}</span>
263
+ <div class="icon-wrapper">
264
+ <ds-icon
265
+ class="icon-pulse"
266
+ [class.animating]="isPulsing()"
267
+ [name]="isLiked() ? 'remixHeart3Fill' : 'remixHeart3Line'"
268
+ size="16px" />
269
+ <ds-icon
270
+ [name]="isLiked() ? 'remixHeart3Fill' : 'remixHeart3Line'"
271
+ size="16px" />
272
+ </div>
273
+ </div>
274
+
275
+ <!-- Desktop more button -->
276
+ <ds-icon-button
277
+ class="desktop-more-button"
278
+ icon="remixMoreFill"
279
+ variant="secondary"
280
+ size="sm"
281
+ (clicked)="handleMoreButtonClick($event)"
282
+ aria-label="More options">
283
+ </ds-icon-button>
284
+ </div>
285
+ </div>
286
+
287
+ <div class="comment-text" [innerHTML]="formattedContent()"></div>
288
+
289
+ <div class="comment-actions">
290
+ <div class="action-reply" (click)="handleReply()">
291
+ Reply
292
+ </div>
293
+ @if (isOwnComment()) {
294
+ <div class="action-edit" (click)="handleEdit()">
295
+ Edit
296
+ </div>
297
+ }
298
+ </div>
299
+ </div>
300
+ `
301
+ })
302
+ export class DsMobileCommentComponent {
303
+ /**
304
+ * Author's display name
305
+ */
306
+ authorName = input.required<string>();
307
+
308
+ /**
309
+ * Author's role (e.g., "Tenant", "Property Manager")
310
+ */
311
+ authorRole = input.required<string>();
312
+
313
+ /**
314
+ * Timestamp text (e.g., "1h ago", "2d ago")
315
+ */
316
+ timestamp = input.required<string>();
317
+
318
+ /**
319
+ * Comment content text
320
+ */
321
+ content = input.required<string>();
322
+
323
+ /**
324
+ * Avatar initials
325
+ */
326
+ avatarInitials = input<string>('');
327
+
328
+ /**
329
+ * Avatar type
330
+ */
331
+ avatarType = input<'initials' | 'photo' | 'icon'>('initials');
332
+
333
+ /**
334
+ * Whether the comment is clickable
335
+ */
336
+ clickable = input<boolean>(false);
337
+
338
+ /**
339
+ * Whether this comment belongs to the current user
340
+ */
341
+ isOwnComment = input<boolean>(false);
342
+
343
+ /**
344
+ * Whether the comment is liked by current user
345
+ */
346
+ isLiked = model<boolean>(false);
347
+
348
+ /**
349
+ * Number of likes
350
+ */
351
+ likeCount = model<number>(0);
352
+
353
+ /**
354
+ * Signal to control pulse animation
355
+ */
356
+ isPulsing = signal(false);
357
+
358
+ /**
359
+ * Computed property to format content with @mentions
360
+ */
361
+ formattedContent = computed(() => {
362
+ const text = this.content();
363
+ // Replace @mentions with styled spans
364
+ // Matches @FirstName or @FirstName LastName (max 2 words)
365
+ return text.replace(/@([A-Za-z]+(?:\s+[A-Za-z]+)?)\b/g, '<span class="mention">@$1</span>');
366
+ });
367
+
368
+ /**
369
+ * Emits when the comment card is clicked (if clickable)
370
+ */
371
+ commentClick = output<void>();
372
+
373
+ /**
374
+ * Emits when reply is clicked
375
+ */
376
+ replyClick = output<void>();
377
+
378
+ /**
379
+ * Emits when edit is clicked
380
+ */
381
+ editClick = output<void>();
382
+
383
+ /**
384
+ * Emits when the comment is long-pressed
385
+ */
386
+ longPress = output<void>();
387
+
388
+ /**
389
+ * Long press tracking
390
+ */
391
+ private longPressTimer: any = null;
392
+ private longPressTriggered = false;
393
+ private touchStartX = 0;
394
+ private touchStartY = 0;
395
+ private readonly LONG_PRESS_DURATION = 500; // ms
396
+ private readonly MOVE_THRESHOLD = 10; // px
397
+
398
+ handleCommentClick(event: Event): void {
399
+ // Only emit if clickable and not clicking on action buttons
400
+ if (this.clickable() && !(event.target as HTMLElement).closest('.comment-actions')) {
401
+ this.commentClick.emit();
402
+ }
403
+ }
404
+
405
+ async toggleLike(): Promise<void> {
406
+ const newLiked = !this.isLiked();
407
+ this.isLiked.set(newLiked);
408
+
409
+ const newCount = newLiked ? this.likeCount() + 1 : this.likeCount() - 1;
410
+ this.likeCount.set(Math.max(0, newCount));
411
+
412
+ // Trigger pulse animation only when liking
413
+ if (newLiked) {
414
+ this.isPulsing.set(true);
415
+ setTimeout(() => this.isPulsing.set(false), 400);
416
+ }
417
+
418
+ // Haptic feedback for like/unlike
419
+ try {
420
+ await Haptics.impact({ style: ImpactStyle.Light });
421
+ } catch {
422
+ // Fallback to Web Vibration API if Capacitor Haptics is not available
423
+ if ('vibrate' in navigator) {
424
+ navigator.vibrate(50);
425
+ }
426
+ }
427
+ }
428
+
429
+ handleReply(): void {
430
+ this.replyClick.emit();
431
+ }
432
+
433
+ handleEdit(): void {
434
+ this.editClick.emit();
435
+ }
436
+
437
+ /**
438
+ * Handle touch start for long press detection
439
+ */
440
+ handleTouchStart(event: TouchEvent): void {
441
+ this.longPressTriggered = false;
442
+ this.touchStartX = event.touches[0].clientX;
443
+ this.touchStartY = event.touches[0].clientY;
444
+
445
+ // Start long press timer
446
+ this.longPressTimer = setTimeout(async () => {
447
+ this.longPressTriggered = true;
448
+ this.longPress.emit();
449
+
450
+ // Haptic feedback for long press
451
+ try {
452
+ await Haptics.impact({ style: ImpactStyle.Medium });
453
+ } catch {
454
+ // Fallback to Web Vibration API if Capacitor Haptics is not available
455
+ if ('vibrate' in navigator) {
456
+ navigator.vibrate(50);
457
+ }
458
+ }
459
+ }, this.LONG_PRESS_DURATION);
460
+ }
461
+
462
+ /**
463
+ * Handle touch end to clear long press timer
464
+ */
465
+ handleTouchEnd(event: TouchEvent): void {
466
+ if (this.longPressTimer) {
467
+ clearTimeout(this.longPressTimer);
468
+ this.longPressTimer = null;
469
+ }
470
+
471
+ // Prevent normal click if long press was triggered
472
+ if (this.longPressTriggered) {
473
+ event.preventDefault();
474
+ event.stopPropagation();
475
+ this.longPressTriggered = false;
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Handle touch move to cancel long press if moved too much
481
+ */
482
+ handleTouchMove(event: TouchEvent): void {
483
+ if (!this.longPressTimer) return;
484
+
485
+ const touch = event.touches[0];
486
+ const deltaX = Math.abs(touch.clientX - this.touchStartX);
487
+ const deltaY = Math.abs(touch.clientY - this.touchStartY);
488
+
489
+ // Cancel long press if moved too far
490
+ if (deltaX > this.MOVE_THRESHOLD || deltaY > this.MOVE_THRESHOLD) {
491
+ clearTimeout(this.longPressTimer);
492
+ this.longPressTimer = null;
493
+ this.longPressTriggered = false;
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Handle context menu (right-click on desktop) to trigger long press action
499
+ */
500
+ handleContextMenu(event: Event): void {
501
+ event.preventDefault();
502
+ this.longPress.emit();
503
+ }
504
+
505
+ /**
506
+ * Handle desktop more button click
507
+ * Stops propagation and triggers long press action
508
+ */
509
+ handleMoreButtonClick(event: Event): void {
510
+ console.log('[Comment] Desktop more button clicked');
511
+ event.stopPropagation();
512
+ event.preventDefault();
513
+ this.longPress.emit();
514
+ }
515
+ }
516
+
@@ -0,0 +1,2 @@
1
+ export { DsMobileCommentComponent } from './ds-mobile-comment';
2
+
@@ -0,0 +1,182 @@
1
+ import { Component, input, output } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { DsIconComponent } from '@propbinder/design-system';
4
+ import { DsAvatarComponent } from '@propbinder/design-system';
5
+
6
+ /**
7
+ * DsMobileContactListItemComponent
8
+ *
9
+ * Specialized interactive component for displaying contacts.
10
+ * Displays contact name with avatar initials and metadata (person name + phone number).
11
+ * Similar styling to file attachments with rounded corners and hover states.
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <ds-mobile-contact-list-item
16
+ * [name]="'Mortensen & Søn ApS'"
17
+ * [initials]="'M'"
18
+ * [contactPerson]="'John Mortensen'"
19
+ * [phoneNumber]="'+45 12 34 56 78'"
20
+ * [clickable]="true"
21
+ * (contactClick)="openContact()">
22
+ * </ds-mobile-contact-list-item>
23
+ * ```
24
+ */
25
+ @Component({
26
+ selector: 'ds-mobile-contact-list-item',
27
+ standalone: true,
28
+ imports: [CommonModule, DsIconComponent, DsAvatarComponent],
29
+ host: {
30
+ '[class.clickable]': 'clickable()',
31
+ '(click)': 'handleContactClick()'
32
+ },
33
+ styles: [`
34
+ :host {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 12px;
38
+ padding: 10px 12px;
39
+ background: var(--color-background-neutral-secondary, #f5f5f5);
40
+ border-radius: 16px;
41
+ transition: all 0.2s ease;
42
+ }
43
+
44
+ :host.clickable {
45
+ cursor: pointer;
46
+ }
47
+
48
+ :host.clickable:hover {
49
+ background: var(--color-background-neutral-secondary-hover, #ebebeb);
50
+ }
51
+
52
+ :host.clickable:active {
53
+ transform: scale(0.98);
54
+ }
55
+
56
+ .contact-avatar {
57
+ flex-shrink: 0;
58
+ }
59
+
60
+ .contact-content {
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: center;
64
+ gap: 2px;
65
+ flex: 1;
66
+ min-width: 0;
67
+ }
68
+
69
+ .contact-name {
70
+ font-family: 'Brockmann', sans-serif;
71
+ font-size: var(--font-size-sm);
72
+ font-weight: 600;
73
+ line-height: 20px;
74
+ letter-spacing: -0.3px;
75
+ color: var(--color-text-primary, #1a1a1a);
76
+ margin: 0;
77
+ white-space: nowrap;
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ }
81
+
82
+ .contact-meta {
83
+ font-family: 'Brockmann', sans-serif;
84
+ font-size: var(--font-size-xs);
85
+ font-weight: 400;
86
+ line-height: 1.2;
87
+ letter-spacing: -0.26px;
88
+ color: var(--color-text-tertiary, #737373);
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 4px;
92
+ margin: 0;
93
+ }
94
+
95
+ .meta-separator {
96
+ color: var(--color-text-tertiary, #a0a0a0);
97
+ }
98
+
99
+ .contact-trailing {
100
+ display: flex;
101
+ align-items: center;
102
+ color: var(--color-text-tertiary, #a3a3a3);
103
+ flex-shrink: 0;
104
+ }
105
+ `],
106
+ template: `
107
+ <div class="contact-avatar">
108
+ <ds-avatar
109
+ [initials]="initials()"
110
+ type="initials"
111
+ size="md"
112
+ />
113
+ </div>
114
+
115
+ <div class="contact-content">
116
+ <div class="contact-name">{{ name() }}</div>
117
+
118
+ @if (contactPerson() || phoneNumber()) {
119
+ <div class="contact-meta">
120
+ @if (contactPerson()) {
121
+ <span>{{ contactPerson() }}</span>
122
+ }
123
+ @if (contactPerson() && phoneNumber()) {
124
+ <span class="meta-separator">·</span>
125
+ }
126
+ @if (phoneNumber()) {
127
+ <span>{{ phoneNumber() }}</span>
128
+ }
129
+ </div>
130
+ }
131
+ </div>
132
+
133
+ @if (showChevron()) {
134
+ <div class="contact-trailing">
135
+ <ds-icon name="remixArrowRightSLine" size="20px" />
136
+ </div>
137
+ }
138
+ `
139
+ })
140
+ export class DsMobileContactListItemComponent {
141
+ /**
142
+ * Contact/company name
143
+ */
144
+ name = input.required<string>();
145
+
146
+ /**
147
+ * Avatar initials (usually 1-2 letters)
148
+ */
149
+ initials = input.required<string>();
150
+
151
+ /**
152
+ * Contact person name (optional)
153
+ */
154
+ contactPerson = input<string>('');
155
+
156
+ /**
157
+ * Phone number (optional)
158
+ */
159
+ phoneNumber = input<string>('');
160
+
161
+ /**
162
+ * Whether the contact item is clickable
163
+ */
164
+ clickable = input<boolean>(true);
165
+
166
+ /**
167
+ * Whether to show chevron icon
168
+ */
169
+ showChevron = input<boolean>(true);
170
+
171
+ /**
172
+ * Emits when the contact item is clicked (if clickable)
173
+ */
174
+ contactClick = output<void>();
175
+
176
+ handleContactClick(): void {
177
+ if (this.clickable()) {
178
+ this.contactClick.emit();
179
+ }
180
+ }
181
+ }
182
+
@@ -0,0 +1,2 @@
1
+ export { DsMobileContactListItemComponent } from './ds-mobile-contact-list-item';
2
+