@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,499 @@
|
|
|
1
|
+
import { Component, input, output, computed, signal, PLATFORM_ID, inject } from '@angular/core';
|
|
2
|
+
import { CommonModule, isPlatformBrowser } from '@angular/common';
|
|
3
|
+
import { DsMobileLongPressDirective } from '../shared/directives/long-press.directive';
|
|
4
|
+
import { DsIconButtonComponent } from '@propbinder/design-system';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DsMobileListItemComponent
|
|
8
|
+
*
|
|
9
|
+
* A versatile, reusable list item component for mobile applications.
|
|
10
|
+
* Supports both interactive and non-interactive modes with flexible content projection.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Interactive mode with click and long-press support
|
|
14
|
+
* - Pseudo-element background extends 8px beyond bounds (no negative margins needed)
|
|
15
|
+
* - Flexible content slots (leading, main, trailing)
|
|
16
|
+
* - Optional structured inputs for common use cases (title, subtitle)
|
|
17
|
+
* - Accessibility features (focus states, ARIA attributes)
|
|
18
|
+
* - Disabled and loading states
|
|
19
|
+
*
|
|
20
|
+
* This component serves as the foundation for specialized list item types like posts,
|
|
21
|
+
* notifications, messages, contacts, and other list content.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```html
|
|
25
|
+
* <!-- Simple structured usage -->
|
|
26
|
+
* <ds-mobile-list-item
|
|
27
|
+
* title="Document Title"
|
|
28
|
+
* subtitle="Supporting text"
|
|
29
|
+
* [interactive]="true"
|
|
30
|
+
* (itemClick)="handleClick()">
|
|
31
|
+
*
|
|
32
|
+
* <ds-icon content-leading name="document" />
|
|
33
|
+
* </ds-mobile-list-item>
|
|
34
|
+
*
|
|
35
|
+
* <!-- Flexible custom usage -->
|
|
36
|
+
* <ds-mobile-list-item
|
|
37
|
+
* [interactive]="true"
|
|
38
|
+
* (itemClick)="handleClick()"
|
|
39
|
+
* (longPress)="showContextMenu()">
|
|
40
|
+
*
|
|
41
|
+
* <div content-leading>
|
|
42
|
+
* <ds-avatar initials="JD" />
|
|
43
|
+
* </div>
|
|
44
|
+
*
|
|
45
|
+
* <div content-main>
|
|
46
|
+
* <h3>Custom Content</h3>
|
|
47
|
+
* <p>Full control over layout and styling</p>
|
|
48
|
+
* </div>
|
|
49
|
+
*
|
|
50
|
+
* <button content-trailing (click)="handleAction($event)">
|
|
51
|
+
* Action
|
|
52
|
+
* </button>
|
|
53
|
+
* </ds-mobile-list-item>
|
|
54
|
+
*
|
|
55
|
+
* <!-- Non-interactive read-only -->
|
|
56
|
+
* <ds-mobile-list-item
|
|
57
|
+
* title="Read-only Item"
|
|
58
|
+
* subtitle="No interaction">
|
|
59
|
+
* <ds-icon content-leading name="info" />
|
|
60
|
+
* </ds-mobile-list-item>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
@Component({
|
|
64
|
+
selector: 'ds-mobile-list-item',
|
|
65
|
+
standalone: true,
|
|
66
|
+
imports: [CommonModule, DsIconButtonComponent],
|
|
67
|
+
hostDirectives: [
|
|
68
|
+
{
|
|
69
|
+
directive: DsMobileLongPressDirective,
|
|
70
|
+
outputs: ['longPress']
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
host: {
|
|
74
|
+
'[class.interactive]': 'interactive() && !disabled()',
|
|
75
|
+
'[class.disabled]': 'disabled()',
|
|
76
|
+
'[class.loading]': 'loading()',
|
|
77
|
+
'[class.no-divider]': '!showDivider()',
|
|
78
|
+
'[class.variant-feed]': 'variant() === "feed"',
|
|
79
|
+
'[class.variant-detail]': 'variant() === "detail"',
|
|
80
|
+
'[class.variant-compact]': 'variant() === "compact"',
|
|
81
|
+
'[attr.role]': 'interactive() ? "button" : null',
|
|
82
|
+
'[attr.tabindex]': 'interactive() && !disabled() ? "0" : null',
|
|
83
|
+
'[attr.aria-disabled]': 'disabled() ? "true" : null',
|
|
84
|
+
'[style.--leading-size]': 'leadingSize()',
|
|
85
|
+
'[style.--interactive-offset]': 'interactiveOffset()',
|
|
86
|
+
'[style.--divider-spacing]': 'dividerSpacing()',
|
|
87
|
+
'(click)': 'handleClick($event)',
|
|
88
|
+
'(keydown.enter)': 'handleKeyDown($event)',
|
|
89
|
+
'(keydown.space)': 'handleKeyDown($event)',
|
|
90
|
+
'(longPress)': 'handleLongPress()'
|
|
91
|
+
},
|
|
92
|
+
styles: [`
|
|
93
|
+
:host {
|
|
94
|
+
display: block;
|
|
95
|
+
position: relative;
|
|
96
|
+
padding: var(--item-padding-top, 12px) 0 var(--item-padding-bottom, 12px) 0;
|
|
97
|
+
box-sizing: border-box;
|
|
98
|
+
/* CSS variables defined at host level for use by children and pseudo-elements */
|
|
99
|
+
--leading-size: 32px;
|
|
100
|
+
--content-gap: 12px;
|
|
101
|
+
--interactive-offset: 8px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Divider line on host */
|
|
105
|
+
:host::after {
|
|
106
|
+
content: '';
|
|
107
|
+
position: absolute;
|
|
108
|
+
bottom: 0;
|
|
109
|
+
left: calc(var(--leading-size) + var(--content-gap));
|
|
110
|
+
right: 0;
|
|
111
|
+
height: 1px;
|
|
112
|
+
background: var(--border-color-default, #e5e5e5);
|
|
113
|
+
z-index: 1;
|
|
114
|
+
display: var(--divider-display, block);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.list-item-inner {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: row;
|
|
120
|
+
align-items: flex-start;
|
|
121
|
+
gap: var(--content-gap);
|
|
122
|
+
position: relative;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Pseudo-element for interactive background */
|
|
126
|
+
:host(.interactive) .list-item-inner::before {
|
|
127
|
+
content: '';
|
|
128
|
+
position: absolute;
|
|
129
|
+
top: calc(-1 * var(--interactive-offset));
|
|
130
|
+
left: calc(-1 * var(--interactive-offset));
|
|
131
|
+
right: calc(-1 * var(--interactive-offset));
|
|
132
|
+
bottom: calc(-1 * var(--interactive-offset));
|
|
133
|
+
background: var(--color-background-primary, #ffffff);
|
|
134
|
+
border-radius: 16px;
|
|
135
|
+
z-index: -1;
|
|
136
|
+
pointer-events: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Interactive states */
|
|
140
|
+
:host(.interactive) {
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Hover state (desktop only) */
|
|
145
|
+
@media (hover: hover) and (pointer: fine) {
|
|
146
|
+
:host(.interactive):hover .list-item-inner::before {
|
|
147
|
+
background: var(--color-background-neutral-primary-hover, #f5f5f5);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Active state */
|
|
152
|
+
:host(.interactive):active .list-item-inner::before {
|
|
153
|
+
background: var(--color-background-neutral-primary-hover, #f5f5f5);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Focus visible for keyboard navigation */
|
|
157
|
+
:host(.interactive):focus-visible {
|
|
158
|
+
outline: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
:host(.interactive):focus-visible .list-item-inner::before {
|
|
162
|
+
outline: 2px solid var(--color-brand-primary, #5d5fef);
|
|
163
|
+
outline-offset: 2px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Disabled state */
|
|
167
|
+
:host(.disabled) {
|
|
168
|
+
opacity: 0.5;
|
|
169
|
+
pointer-events: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Loading state */
|
|
173
|
+
:host(.loading) {
|
|
174
|
+
pointer-events: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Variants */
|
|
178
|
+
:host(.variant-detail) .list-item-inner {
|
|
179
|
+
padding: 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
:host(.variant-compact) .list-item-inner {
|
|
183
|
+
gap: 8px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Content slots */
|
|
187
|
+
.content-leading {
|
|
188
|
+
flex-shrink: 0;
|
|
189
|
+
width: var(--leading-size);
|
|
190
|
+
height: var(--leading-size);
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: flex-start;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
position: relative;
|
|
195
|
+
z-index: 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.content-main {
|
|
199
|
+
flex: 1;
|
|
200
|
+
min-width: 0;
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
gap: 8px;
|
|
204
|
+
position: relative;
|
|
205
|
+
z-index: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.content-trailing {
|
|
209
|
+
flex-shrink: 0;
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: flex-start;
|
|
212
|
+
position: relative;
|
|
213
|
+
z-index: 1;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Structured content styles */
|
|
217
|
+
.structured-title {
|
|
218
|
+
font-family: 'Brockmann', sans-serif;
|
|
219
|
+
font-size: var(--font-size-sm, 14px);
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
line-height: 20px;
|
|
222
|
+
letter-spacing: -0.3px;
|
|
223
|
+
color: var(--text-color-default-primary, #202227);
|
|
224
|
+
margin: 0;
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
text-overflow: ellipsis;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.structured-subtitle {
|
|
231
|
+
font-family: 'Brockmann', sans-serif;
|
|
232
|
+
font-size: var(--font-size-sm, 14px);
|
|
233
|
+
font-weight: 400;
|
|
234
|
+
line-height: 20px;
|
|
235
|
+
letter-spacing: -0.3px;
|
|
236
|
+
color: var(--text-color-default-secondary, #545B66);
|
|
237
|
+
margin: 0;
|
|
238
|
+
white-space: nowrap;
|
|
239
|
+
overflow: hidden;
|
|
240
|
+
text-overflow: ellipsis;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Desktop more actions button - using ds-icon-button */
|
|
244
|
+
.desktop-more-button::ng-deep button {
|
|
245
|
+
border-radius: 50% !important;
|
|
246
|
+
}
|
|
247
|
+
`],
|
|
248
|
+
template: `
|
|
249
|
+
<div class="list-item-inner">
|
|
250
|
+
<div class="content-leading">
|
|
251
|
+
<ng-content select="[content-leading]" />
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<div class="content-main">
|
|
255
|
+
@if (title()) {
|
|
256
|
+
<h3 class="structured-title">{{ title() }}</h3>
|
|
257
|
+
}
|
|
258
|
+
@if (subtitle()) {
|
|
259
|
+
<p class="structured-subtitle">{{ subtitle() }}</p>
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
<ng-content select="[content-main]" />
|
|
263
|
+
<ng-content />
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="content-trailing">
|
|
267
|
+
@if (interactive() && enableLongPress() && showDesktopMoreButton() && isDesktop()) {
|
|
268
|
+
<ds-icon-button
|
|
269
|
+
class="desktop-more-button"
|
|
270
|
+
icon="remixMoreFill"
|
|
271
|
+
variant="secondary"
|
|
272
|
+
size="sm"
|
|
273
|
+
(clicked)="handleMoreButtonClick($event)"
|
|
274
|
+
aria-label="More options">
|
|
275
|
+
</ds-icon-button>
|
|
276
|
+
}
|
|
277
|
+
<ng-content select="[content-trailing]" />
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
`
|
|
281
|
+
})
|
|
282
|
+
export class DsMobileListItemComponent {
|
|
283
|
+
private platformId = inject(PLATFORM_ID);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Detect if viewport is desktop size
|
|
287
|
+
* Use viewport width for breakpoint detection (show button on tablet and above)
|
|
288
|
+
*/
|
|
289
|
+
isDesktop = signal<boolean>(false);
|
|
290
|
+
|
|
291
|
+
constructor() {
|
|
292
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
293
|
+
// Show button on tablet breakpoint and above (768px+)
|
|
294
|
+
const isDesktopViewport = window.innerWidth >= 768;
|
|
295
|
+
|
|
296
|
+
console.log('[ListItem] Desktop detection:', {
|
|
297
|
+
innerWidth: window.innerWidth,
|
|
298
|
+
isDesktopViewport
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
this.isDesktop.set(isDesktopViewport);
|
|
302
|
+
|
|
303
|
+
// Listen for window resize to update detection
|
|
304
|
+
window.addEventListener('resize', () => {
|
|
305
|
+
const newIsDesktop = window.innerWidth >= 768;
|
|
306
|
+
if (newIsDesktop !== this.isDesktop()) {
|
|
307
|
+
console.log('[ListItem] Viewport changed, updating desktop detection:', newIsDesktop);
|
|
308
|
+
this.isDesktop.set(newIsDesktop);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* CSS size value for the leading content area (e.g., '32px', '40px', '48px')
|
|
316
|
+
* Defaults to '32px' for standard list item avatars/icons
|
|
317
|
+
*/
|
|
318
|
+
leadingSize = input<string>('32px');
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Display variant
|
|
322
|
+
* - 'feed' - Standard feed display (default)
|
|
323
|
+
* - 'detail' - Full detail view
|
|
324
|
+
* - 'compact' - Compact display for nested/related items
|
|
325
|
+
*/
|
|
326
|
+
variant = input<'feed' | 'detail' | 'compact'>('feed');
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Whether the list item is interactive (clickable and long-pressable)
|
|
330
|
+
* When true, adds interactive background, cursor pointer, and touch handlers
|
|
331
|
+
*/
|
|
332
|
+
interactive = input<boolean>(false);
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Whether the list item is disabled
|
|
336
|
+
* Disables all interactions and reduces opacity
|
|
337
|
+
*/
|
|
338
|
+
disabled = input<boolean>(false);
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Whether the list item is in a loading state
|
|
342
|
+
* Disables interactions but maintains full opacity
|
|
343
|
+
*/
|
|
344
|
+
loading = input<boolean>(false);
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Enable long-press interaction when interactive is true
|
|
348
|
+
* Set to false to disable long-press but keep click
|
|
349
|
+
*/
|
|
350
|
+
enableLongPress = input<boolean>(true);
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Show "more actions" button on desktop for items with long-press enabled
|
|
354
|
+
* Only visible on desktop (hover: hover) and when enableLongPress is true
|
|
355
|
+
* Clicking this button triggers the same handler as long-press on mobile
|
|
356
|
+
* @default true
|
|
357
|
+
*/
|
|
358
|
+
showDesktopMoreButton = input<boolean>(true);
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Offset distance for the interactive background pseudo-element
|
|
362
|
+
* Extends the background beyond the content bounds
|
|
363
|
+
* @default '8px'
|
|
364
|
+
*/
|
|
365
|
+
interactiveOffset = input<string>('8px');
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Optional structured title text
|
|
369
|
+
* Provides a simple way to add title without custom markup
|
|
370
|
+
*/
|
|
371
|
+
title = input<string>();
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Optional structured subtitle text
|
|
375
|
+
* Provides a simple way to add subtitle without custom markup
|
|
376
|
+
*/
|
|
377
|
+
subtitle = input<string>();
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Whether to show the divider line below the list item
|
|
381
|
+
* Automatically hidden on last-child and detail variant
|
|
382
|
+
* @default true
|
|
383
|
+
*/
|
|
384
|
+
showDivider = input<boolean>(true);
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Spacing around the divider (top and bottom padding)
|
|
388
|
+
* @default '4px'
|
|
389
|
+
*/
|
|
390
|
+
dividerSpacing = input<string>('4px');
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Emits when the list item is clicked (if interactive and not disabled)
|
|
394
|
+
*/
|
|
395
|
+
itemClick = output<void>();
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Emits when the desktop more actions button is clicked
|
|
399
|
+
* This is separate from longPress to give more control to parent components
|
|
400
|
+
* Typically, you can use (longPress) for both mobile and desktop actions
|
|
401
|
+
*/
|
|
402
|
+
moreButtonClick = output<Event>();
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Track if long press was triggered to prevent click
|
|
406
|
+
*/
|
|
407
|
+
private longPressTriggered = false;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Check if leading content slot has content
|
|
411
|
+
* Always true to maintain consistent layout
|
|
412
|
+
*/
|
|
413
|
+
hasLeadingContent = computed(() => true);
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if trailing content slot has content
|
|
417
|
+
* Always true to maintain consistent layout
|
|
418
|
+
*/
|
|
419
|
+
hasTrailingContent = computed(() => true);
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Handle click events
|
|
423
|
+
*/
|
|
424
|
+
handleClick(event: Event): void {
|
|
425
|
+
console.log('[ListItem] Click event fired', {
|
|
426
|
+
interactive: this.interactive(),
|
|
427
|
+
disabled: this.disabled(),
|
|
428
|
+
loading: this.loading(),
|
|
429
|
+
longPressTriggered: this.longPressTriggered,
|
|
430
|
+
target: event.target
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (!this.interactive() || this.disabled() || this.loading()) {
|
|
434
|
+
console.log('[ListItem] Click ignored - not interactive or disabled/loading');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Don't emit click if it came from an interactive child element
|
|
439
|
+
// (but not the host element itself)
|
|
440
|
+
const target = event.target as HTMLElement;
|
|
441
|
+
const closestInteractive = target.closest('button, a, input, select, textarea, [role="button"]');
|
|
442
|
+
|
|
443
|
+
// Check if the interactive element is a child, not the host itself
|
|
444
|
+
if (closestInteractive && closestInteractive !== event.currentTarget) {
|
|
445
|
+
console.log('[ListItem] Click ignored - came from interactive child:', closestInteractive);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!this.longPressTriggered) {
|
|
450
|
+
console.log('[ListItem] Emitting itemClick');
|
|
451
|
+
this.itemClick.emit();
|
|
452
|
+
} else {
|
|
453
|
+
console.log('[ListItem] Click ignored - long press was triggered');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.longPressTriggered = false;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Handle keyboard events (Enter/Space)
|
|
461
|
+
*/
|
|
462
|
+
handleKeyDown(event: KeyboardEvent): void {
|
|
463
|
+
if (!this.interactive() || this.disabled() || this.loading()) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
event.preventDefault();
|
|
468
|
+
this.itemClick.emit();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Handle long press events from the directive
|
|
473
|
+
* Set the flag to prevent the subsequent click event
|
|
474
|
+
*/
|
|
475
|
+
handleLongPress(): void {
|
|
476
|
+
this.longPressTriggered = true;
|
|
477
|
+
// Reset the flag after a short delay to allow for the next interaction
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
this.longPressTriggered = false;
|
|
480
|
+
}, 100);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Handle desktop more button click
|
|
485
|
+
* Stops propagation to prevent triggering itemClick
|
|
486
|
+
* Emits moreButtonClick for parent components to handle
|
|
487
|
+
*/
|
|
488
|
+
handleMoreButtonClick(event: Event): void {
|
|
489
|
+
console.log('[ListItem] Desktop more button clicked');
|
|
490
|
+
|
|
491
|
+
// Stop propagation to prevent triggering itemClick
|
|
492
|
+
event.stopPropagation();
|
|
493
|
+
event.preventDefault();
|
|
494
|
+
|
|
495
|
+
// Emit the more button click event
|
|
496
|
+
this.moreButtonClick.emit(event);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Component, input, computed } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DsMobileListItemStaticComponent
|
|
6
|
+
*
|
|
7
|
+
* A read-only version of the interactive list item component.
|
|
8
|
+
* Used for displaying static information without interaction.
|
|
9
|
+
*
|
|
10
|
+
* This component has the same structure as the interactive list item but without:
|
|
11
|
+
* - Padding
|
|
12
|
+
* - Rounded corners
|
|
13
|
+
* - Hover states
|
|
14
|
+
* - Click interactions
|
|
15
|
+
* - Background fill (transparent)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```html
|
|
19
|
+
* <ds-mobile-list-item-static
|
|
20
|
+
* [leadingSize]="'40px'">
|
|
21
|
+
*
|
|
22
|
+
* <div content-leading>
|
|
23
|
+
* <ds-avatar initials="JD" />
|
|
24
|
+
* </div>
|
|
25
|
+
*
|
|
26
|
+
* <div content-main>
|
|
27
|
+
* <h3>Main Content</h3>
|
|
28
|
+
* <p>Supporting text goes here...</p>
|
|
29
|
+
* </div>
|
|
30
|
+
*
|
|
31
|
+
* <div content-trailing>
|
|
32
|
+
* <span>Info</span>
|
|
33
|
+
* </div>
|
|
34
|
+
* </ds-mobile-list-item-static>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
@Component({
|
|
38
|
+
selector: 'ds-mobile-list-item-static',
|
|
39
|
+
standalone: true,
|
|
40
|
+
imports: [CommonModule],
|
|
41
|
+
host: {
|
|
42
|
+
'[style.--leading-size]': 'leadingSize()'
|
|
43
|
+
},
|
|
44
|
+
styles: [`
|
|
45
|
+
:host {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: row;
|
|
48
|
+
align-items: flex-start;
|
|
49
|
+
background: transparent;
|
|
50
|
+
padding: 0;
|
|
51
|
+
gap: 12px;
|
|
52
|
+
position: relative;
|
|
53
|
+
--leading-size: 32px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
:host::after {
|
|
57
|
+
content: '';
|
|
58
|
+
position: absolute;
|
|
59
|
+
bottom: -10px;
|
|
60
|
+
left: calc(var(--leading-size) + 12px);
|
|
61
|
+
right: 0;
|
|
62
|
+
height: 1px;
|
|
63
|
+
background: var(--border-color-default);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:host:last-child::after {
|
|
67
|
+
display: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.content-leading {
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
width: var(--leading-size);
|
|
73
|
+
min-height: var(--leading-size);
|
|
74
|
+
height: auto;
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.content-main {
|
|
81
|
+
flex: 1;
|
|
82
|
+
min-width: 0;
|
|
83
|
+
min-height: var(--leading-size);
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
align-items: flex-start;
|
|
88
|
+
gap: 8px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.content-trailing {
|
|
92
|
+
flex-shrink: 0;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: flex-start;
|
|
95
|
+
}
|
|
96
|
+
`],
|
|
97
|
+
template: `
|
|
98
|
+
@if (hasLeadingContent()) {
|
|
99
|
+
<div class="content-leading">
|
|
100
|
+
<ng-content select="[content-leading]" />
|
|
101
|
+
</div>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
<div class="content-main">
|
|
105
|
+
<ng-content select="[content-main]" />
|
|
106
|
+
<ng-content />
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
@if (hasTrailingContent()) {
|
|
110
|
+
<div class="content-trailing">
|
|
111
|
+
<ng-content select="[content-trailing]" />
|
|
112
|
+
</div>
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
})
|
|
116
|
+
export class DsMobileListItemStaticComponent {
|
|
117
|
+
/**
|
|
118
|
+
* CSS size value for the leading content area (e.g., '32px', '40px', '48px')
|
|
119
|
+
* Defaults to '32px' for standard list item avatars
|
|
120
|
+
*/
|
|
121
|
+
leadingSize = input<string>('32px');
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if leading content slot has content
|
|
125
|
+
*/
|
|
126
|
+
hasLeadingContent = computed(() => true); // Always render slot container for consistency
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if trailing content slot has content
|
|
130
|
+
*/
|
|
131
|
+
hasTrailingContent = computed(() => true); // Always render slot container for consistency
|
|
132
|
+
}
|
|
133
|
+
|