@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.
- package/ng-package.json +7 -0
- package/package.json +12 -39
- package/src/animations/page-transitions.ts +86 -0
- package/src/assets/fonts/Brockmann-Bold.otf +0 -0
- package/src/assets/fonts/Brockmann-BoldItalic.otf +0 -0
- package/src/assets/fonts/Brockmann-Medium.otf +0 -0
- package/src/assets/fonts/Brockmann-MediumItalic.otf +0 -0
- package/src/assets/fonts/Brockmann-Regular.otf +0 -0
- package/src/assets/fonts/Brockmann-RegularItalic.otf +0 -0
- package/src/assets/fonts/Brockmann-SemiBold.otf +0 -0
- package/src/assets/fonts/Brockmann-SemiBoldItalic.otf +0 -0
- package/src/assets/fonts/Brockmann_desktop_license.pdf +0 -0
- package/src/assets/fonts/brockmann-medium-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-regular-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-semibold-webfont.woff2 +0 -0
- package/src/components/action-list-item/ds-mobile-action-list-item.ts +83 -0
- package/src/components/action-list-item/index.ts +2 -0
- package/src/components/app-layout/ds-mobile-app-layout.css +343 -0
- package/src/components/app-layout/ds-mobile-app-layout.ts +271 -0
- package/src/components/app-layout/index.ts +2 -0
- package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +130 -0
- package/src/components/avatar-with-badge/index.ts +2 -0
- package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +273 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +110 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +167 -0
- package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +656 -0
- package/src/components/bottom-sheet/index.ts +3 -0
- package/src/components/comment/ds-mobile-comment.ts +516 -0
- package/src/components/comment/index.ts +2 -0
- package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
- package/src/components/contact-list-item/index.ts +2 -0
- package/src/components/content/ds-mobile-content.ts +158 -0
- package/src/components/content/index.ts +2 -0
- package/src/components/ds-mobile-tabs.css +372 -0
- package/src/components/ds-mobile-tabs.ts +217 -0
- package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
- package/src/components/file-attachment/index.ts +2 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +98 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +514 -0
- package/src/components/handbook-detail-modal/index.ts +3 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +130 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +444 -0
- package/src/components/handbook-folder/index.ts +4 -0
- package/src/components/header-content/ds-mobile-header-content.ts +211 -0
- package/src/components/header-content/index.ts +2 -0
- package/src/components/index.ts +45 -0
- package/src/components/inline-photo/ds-mobile-inline-photo.ts +269 -0
- package/src/components/inline-photo/index.ts +1 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.css +60 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +280 -0
- package/src/components/interactive-list-item-inquiry/index.ts +2 -0
- package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +197 -0
- package/src/components/interactive-list-item-message/index.ts +2 -0
- package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.css +70 -0
- package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +594 -0
- package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
- package/src/components/interactive-list-item-post/index.ts +13 -0
- package/src/components/lightbox/ds-mobile-lightbox-footer.ts +331 -0
- package/src/components/lightbox/ds-mobile-lightbox-header.ts +173 -0
- package/src/components/lightbox/ds-mobile-lightbox-image.ts +464 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.css +375 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
- package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
- package/src/components/lightbox/ds-mobile-lightbox.service.ts +293 -0
- package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
- package/src/components/lightbox/index.ts +22 -0
- package/src/components/list-item/ds-mobile-list-item.ts +499 -0
- package/src/components/list-item/index.ts +2 -0
- package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
- package/src/components/list-item-static/index.ts +2 -0
- package/src/components/logo/ds-logo.ts +85 -0
- package/src/components/logo/index.ts +2 -0
- package/src/components/modal/ds-mobile-modal.css +163 -0
- package/src/components/modal/ds-mobile-modal.service.ts +329 -0
- package/src/components/modal/index.ts +8 -0
- package/src/components/page-details/ds-mobile-page-details.css +285 -0
- package/src/components/page-details/ds-mobile-page-details.ts +128 -0
- package/src/components/page-details/index.ts +2 -0
- package/src/components/page-main/ds-mobile-page-main.css +346 -0
- package/src/components/page-main/ds-mobile-page-main.ts +331 -0
- package/src/components/page-main/index.ts +2 -0
- package/src/components/post-card/ds-mobile-post-card.ts +685 -0
- package/src/components/post-card/ds-mobile-post-pdf-attachment.ts +124 -0
- package/src/components/post-card/index.ts +11 -0
- package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
- package/src/components/post-composer/index.ts +2 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +104 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +1273 -0
- package/src/components/post-detail-modal/index.ts +9 -0
- package/src/components/shared/directives/index.ts +2 -0
- package/src/components/shared/directives/long-press.directive.ts +208 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/shared/mobile-common.css +94 -0
- package/src/components/shared/mobile-page-base.css +315 -0
- package/src/components/shared/mobile-page-base.ts +70 -0
- package/src/components/swiper/ds-mobile-swiper.ts +123 -0
- package/src/components/swiper/index.ts +2 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.ts +132 -0
- package/src/components/tab-bar/index.ts +2 -0
- package/src/components/tabs/ds-mobile-tabs.css +405 -0
- package/src/components/tabs/ds-mobile-tabs.ts +204 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/pages/community.page.ts +768 -0
- package/src/pages/handbook.page.ts +298 -0
- package/src/pages/home.page.ts +192 -0
- package/src/pages/index.ts +9 -0
- package/src/pages/inquiries.example.ts +212 -0
- package/src/pages/inquiry-detail.example.css +434 -0
- package/src/pages/inquiry-detail.example.ts +416 -0
- package/src/pages/mobile-tabs-example.component.ts +146 -0
- package/src/pages/post-create.page.ts +311 -0
- package/src/pages/post-detail.page.ts +295 -0
- package/src/pages/whitelabel-demo.page.ts +548 -0
- package/src/public-api.ts +5 -0
- package/src/services/user.service.ts +35 -0
- package/src/services/whitelabel.service.ts +171 -0
- package/src/styles/ionic.css +673 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +13 -0
- package/fesm2022/propbinder-mobile-design.mjs +0 -8294
- package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
- 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,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
|
+
|