@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,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
Output,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
HostListener,
|
|
6
|
+
Input,
|
|
7
|
+
OnDestroy
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { Haptics, ImpactStyle } from '@capacitor/haptics';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DsMobileLongPressDirective
|
|
13
|
+
*
|
|
14
|
+
* A reusable directive for handling long press interactions on mobile devices.
|
|
15
|
+
* Provides haptic feedback and prevents long press when touching interactive elements.
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Configurable duration and movement threshold
|
|
19
|
+
* - Automatic haptic feedback (with fallback to navigator.vibrate)
|
|
20
|
+
* - Excludes interactive elements (buttons, links, inputs)
|
|
21
|
+
* - Handles touchmove cancellation
|
|
22
|
+
* - Context menu support (right-click on desktop)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```html
|
|
26
|
+
* <!-- Basic usage -->
|
|
27
|
+
* <div dsMobileLongPress (longPress)="handleLongPress()">
|
|
28
|
+
* Long press me
|
|
29
|
+
* </div>
|
|
30
|
+
*
|
|
31
|
+
* <!-- Custom duration and threshold -->
|
|
32
|
+
* <div
|
|
33
|
+
* dsMobileLongPress
|
|
34
|
+
* [longPressDuration]="800"
|
|
35
|
+
* [moveThreshold]="15"
|
|
36
|
+
* [excludeSelectors]="'button, a, .no-longpress'"
|
|
37
|
+
* (longPress)="showContextMenu()">
|
|
38
|
+
* Custom long press
|
|
39
|
+
* </div>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
@Directive({
|
|
43
|
+
selector: '[dsMobileLongPress]',
|
|
44
|
+
standalone: true
|
|
45
|
+
})
|
|
46
|
+
export class DsMobileLongPressDirective implements OnDestroy {
|
|
47
|
+
/**
|
|
48
|
+
* Duration in milliseconds to trigger long press
|
|
49
|
+
* @default 500
|
|
50
|
+
*/
|
|
51
|
+
@Input() longPressDuration = 500;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Maximum movement in pixels before canceling long press
|
|
55
|
+
* @default 10
|
|
56
|
+
*/
|
|
57
|
+
@Input() moveThreshold = 10;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* CSS selectors to exclude from long press detection
|
|
61
|
+
* @default 'button, a, input, select, textarea, [role="button"]'
|
|
62
|
+
*/
|
|
63
|
+
@Input() excludeSelectors = 'button, a, input, select, textarea, [role="button"]';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Haptic feedback style (Light, Medium, Heavy)
|
|
67
|
+
* @default ImpactStyle.Medium
|
|
68
|
+
*/
|
|
69
|
+
@Input() hapticStyle: ImpactStyle = ImpactStyle.Medium;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Enable/disable haptic feedback
|
|
73
|
+
* @default true
|
|
74
|
+
*/
|
|
75
|
+
@Input() enableHaptics = true;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Emits when long press is triggered
|
|
79
|
+
*/
|
|
80
|
+
@Output() longPress = new EventEmitter<void>();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Emits when long press starts (timer begins)
|
|
84
|
+
*/
|
|
85
|
+
@Output() longPressStart = new EventEmitter<void>();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Emits when long press is cancelled
|
|
89
|
+
*/
|
|
90
|
+
@Output() longPressCancel = new EventEmitter<void>();
|
|
91
|
+
|
|
92
|
+
private longPressTimer: any = null;
|
|
93
|
+
private longPressTriggered = false;
|
|
94
|
+
private touchStartX = 0;
|
|
95
|
+
private touchStartY = 0;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Handle touch start for long press detection
|
|
99
|
+
*/
|
|
100
|
+
@HostListener('touchstart', ['$event'])
|
|
101
|
+
handleTouchStart(event: TouchEvent): void {
|
|
102
|
+
// Don't start long press if touching interactive elements
|
|
103
|
+
const target = event.target as HTMLElement;
|
|
104
|
+
if (target.closest(this.excludeSelectors)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.longPressTriggered = false;
|
|
109
|
+
this.touchStartX = event.touches[0].clientX;
|
|
110
|
+
this.touchStartY = event.touches[0].clientY;
|
|
111
|
+
|
|
112
|
+
// Emit start event
|
|
113
|
+
this.longPressStart.emit();
|
|
114
|
+
|
|
115
|
+
// Start long press timer
|
|
116
|
+
this.longPressTimer = setTimeout(async () => {
|
|
117
|
+
this.longPressTriggered = true;
|
|
118
|
+
this.longPress.emit();
|
|
119
|
+
|
|
120
|
+
// Haptic feedback for long press
|
|
121
|
+
if (this.enableHaptics) {
|
|
122
|
+
await this.triggerHaptics();
|
|
123
|
+
}
|
|
124
|
+
}, this.longPressDuration);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Handle touch end to clear long press timer
|
|
129
|
+
*/
|
|
130
|
+
@HostListener('touchend', ['$event'])
|
|
131
|
+
handleTouchEnd(event: TouchEvent): void {
|
|
132
|
+
if (this.longPressTimer) {
|
|
133
|
+
clearTimeout(this.longPressTimer);
|
|
134
|
+
this.longPressTimer = null;
|
|
135
|
+
|
|
136
|
+
if (!this.longPressTriggered) {
|
|
137
|
+
this.longPressCancel.emit();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Prevent normal click if long press was triggered
|
|
142
|
+
if (this.longPressTriggered) {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
this.longPressTriggered = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle touch move to cancel long press if moved too much
|
|
151
|
+
*/
|
|
152
|
+
@HostListener('touchmove', ['$event'])
|
|
153
|
+
handleTouchMove(event: TouchEvent): void {
|
|
154
|
+
if (!this.longPressTimer) return;
|
|
155
|
+
|
|
156
|
+
const touch = event.touches[0];
|
|
157
|
+
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
|
158
|
+
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
|
159
|
+
|
|
160
|
+
// Cancel long press if moved too far
|
|
161
|
+
if (deltaX > this.moveThreshold || deltaY > this.moveThreshold) {
|
|
162
|
+
clearTimeout(this.longPressTimer);
|
|
163
|
+
this.longPressTimer = null;
|
|
164
|
+
this.longPressTriggered = false;
|
|
165
|
+
this.longPressCancel.emit();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle context menu (right-click on desktop) to trigger long press action
|
|
171
|
+
*/
|
|
172
|
+
@HostListener('contextmenu', ['$event'])
|
|
173
|
+
handleContextMenu(event: Event): void {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
this.longPress.emit();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Trigger haptic feedback
|
|
180
|
+
*/
|
|
181
|
+
private async triggerHaptics(): Promise<void> {
|
|
182
|
+
try {
|
|
183
|
+
await Haptics.impact({ style: this.hapticStyle });
|
|
184
|
+
} catch {
|
|
185
|
+
// Fallback to Web Vibration API if Capacitor Haptics is not available
|
|
186
|
+
if ('vibrate' in navigator) {
|
|
187
|
+
// Map haptic styles to vibration durations
|
|
188
|
+
const vibrationMap = {
|
|
189
|
+
[ImpactStyle.Light]: 30,
|
|
190
|
+
[ImpactStyle.Medium]: 50,
|
|
191
|
+
[ImpactStyle.Heavy]: 80
|
|
192
|
+
};
|
|
193
|
+
navigator.vibrate(vibrationMap[this.hapticStyle] || 50);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Cleanup on destroy
|
|
200
|
+
*/
|
|
201
|
+
ngOnDestroy(): void {
|
|
202
|
+
if (this.longPressTimer) {
|
|
203
|
+
clearTimeout(this.longPressTimer);
|
|
204
|
+
this.longPressTimer = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mobile Common Styles
|
|
3
|
+
* Shared CSS classes used across multiple mobile components
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: Import this file via styleUrls in components that use these classes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/* Author Details Container */
|
|
9
|
+
.author-details {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
gap: 2px;
|
|
13
|
+
min-width: 0;
|
|
14
|
+
flex: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Author Name */
|
|
18
|
+
.author-name {
|
|
19
|
+
font-family: 'Brockmann', sans-serif;
|
|
20
|
+
font-size: var(--font-size-sm);
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
line-height: 20px;
|
|
23
|
+
letter-spacing: -0.3px;
|
|
24
|
+
color: var(--color-text-primary, #1a1a1a);
|
|
25
|
+
white-space: nowrap;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
text-overflow: ellipsis;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Author Meta (role, timestamp, etc.) */
|
|
31
|
+
.author-meta {
|
|
32
|
+
font-family: 'Brockmann', sans-serif;
|
|
33
|
+
font-size: var(--font-size-xs);
|
|
34
|
+
font-weight: 400;
|
|
35
|
+
line-height: 1.2;
|
|
36
|
+
letter-spacing: -0.26px;
|
|
37
|
+
color: var(--color-text-tertiary, #737373);
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 6px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.author-meta .separator {
|
|
44
|
+
color: var(--color-text-tertiary, #a0a0a0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Variants for lightbox/overlay contexts (white text on dark bg) */
|
|
48
|
+
.lightbox-context .author-name,
|
|
49
|
+
.overlay-context .author-name {
|
|
50
|
+
color: rgba(255, 255, 255, 0.95);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.lightbox-context .author-meta,
|
|
54
|
+
.overlay-context .author-meta {
|
|
55
|
+
color: rgba(255, 255, 255, 0.7);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.lightbox-context .author-meta .separator,
|
|
59
|
+
.overlay-context .author-meta .separator {
|
|
60
|
+
color: rgba(255, 255, 255, 0.5);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Section Headlines */
|
|
64
|
+
.section-headline {
|
|
65
|
+
font-size: var(--font-size-sm);
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
color: var(--text-color-default-primary);
|
|
68
|
+
padding: 16px 0;
|
|
69
|
+
margin: 0;
|
|
70
|
+
letter-spacing: -0.2px;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 6px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Empty State Text */
|
|
77
|
+
.empty-state-title {
|
|
78
|
+
font-family: 'Brockmann', sans-serif;
|
|
79
|
+
font-size: var(--font-size-base);
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
line-height: 1.3;
|
|
82
|
+
color: var(--text-color-default-primary, #202227);
|
|
83
|
+
margin: 0 0 8px 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.empty-state-description {
|
|
87
|
+
font-family: 'Brockmann', sans-serif;
|
|
88
|
+
font-size: var(--font-size-sm);
|
|
89
|
+
font-weight: 400;
|
|
90
|
+
line-height: 1.4;
|
|
91
|
+
color: var(--text-color-default-secondary, #545B66);
|
|
92
|
+
margin: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
MOBILE PAGE BASE STYLES
|
|
3
|
+
Shared styles for mobile pages
|
|
4
|
+
Import this in your page component's styleUrls
|
|
5
|
+
============================================ */
|
|
6
|
+
|
|
7
|
+
/* ============================================
|
|
8
|
+
ION-CONTENT
|
|
9
|
+
============================================ */
|
|
10
|
+
|
|
11
|
+
ion-content {
|
|
12
|
+
--background: transparent;
|
|
13
|
+
--padding-top: 0;
|
|
14
|
+
--padding-start: 0;
|
|
15
|
+
--padding-end: 0;
|
|
16
|
+
--padding-bottom: 0;
|
|
17
|
+
border-radius: 24px 24px 0 0;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ion-content::part(scroll) {
|
|
22
|
+
-webkit-overflow-scrolling: touch;
|
|
23
|
+
overscroll-behavior-y: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Desktop/Tablet adjustments */
|
|
27
|
+
@media (min-width: 768px) {
|
|
28
|
+
ion-content {
|
|
29
|
+
border-radius: 16px 16px 0 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ============================================
|
|
34
|
+
ION-HEADER
|
|
35
|
+
============================================ */
|
|
36
|
+
|
|
37
|
+
ion-header {
|
|
38
|
+
background: transparent;
|
|
39
|
+
box-shadow: none;
|
|
40
|
+
height: 72px;
|
|
41
|
+
min-height: 72px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ion-header ion-toolbar {
|
|
45
|
+
--background: transparent;
|
|
46
|
+
--border-width: 0;
|
|
47
|
+
--box-shadow: none;
|
|
48
|
+
--padding-top: 0;
|
|
49
|
+
--padding-bottom: 0;
|
|
50
|
+
--padding-start: 0;
|
|
51
|
+
--padding-end: 0;
|
|
52
|
+
--min-height: 72px;
|
|
53
|
+
height: 100%;
|
|
54
|
+
min-height: 72px;
|
|
55
|
+
padding: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ion-header ion-toolbar::part(native) {
|
|
59
|
+
height: 100%;
|
|
60
|
+
min-height: 72px;
|
|
61
|
+
padding: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ion-header ion-toolbar .toolbar-container {
|
|
65
|
+
height: 100%;
|
|
66
|
+
min-height: 72px;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ion-header ion-toolbar .toolbar-container > * {
|
|
72
|
+
height: 100%;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
padding-left: 0;
|
|
76
|
+
padding-right: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Hide header on desktop when using ds-mobile-tabs top bar */
|
|
80
|
+
@media (min-width: 768px) {
|
|
81
|
+
ion-header {
|
|
82
|
+
display: none;
|
|
83
|
+
height: auto;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ============================================
|
|
88
|
+
REFRESHER
|
|
89
|
+
============================================ */
|
|
90
|
+
|
|
91
|
+
ion-refresher {
|
|
92
|
+
z-index: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ion-refresher-content {
|
|
96
|
+
--color: white;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ============================================
|
|
100
|
+
HEADER VARIANTS
|
|
101
|
+
============================================ */
|
|
102
|
+
|
|
103
|
+
.header-home {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
padding: 12px 16px;
|
|
108
|
+
background: var(--color-brand-secondary);
|
|
109
|
+
position: relative;
|
|
110
|
+
height: 100%;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.header-home__title {
|
|
114
|
+
position: absolute;
|
|
115
|
+
left: 50%;
|
|
116
|
+
transform: translateX(-50%) translateY(-100%);
|
|
117
|
+
font-size: var(--font-size-base);
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
color: white;
|
|
120
|
+
opacity: 0;
|
|
121
|
+
transition: transform 0.6s ease, opacity 0.6s ease;
|
|
122
|
+
margin: 0;
|
|
123
|
+
padding: 0;
|
|
124
|
+
--color: white;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.header-home__actions {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.logomark {
|
|
134
|
+
height: 28px;
|
|
135
|
+
width: auto;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Condensed header - hidden by default on mobile */
|
|
140
|
+
ion-header[collapse="condense"] {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Show title in top header when scrolled past condensed header */
|
|
145
|
+
.header-scrolled .header-home__title {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
transform: translateX(-50%) translateY(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@media (min-width: 768px) {
|
|
151
|
+
.header-home {
|
|
152
|
+
padding: 16px 24px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.logomark {
|
|
156
|
+
height: 32px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Hide title on desktop - not needed */
|
|
160
|
+
.header-home__title {
|
|
161
|
+
display: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Hide condensed header on desktop */
|
|
165
|
+
ion-header[collapse="condense"] {
|
|
166
|
+
display: none;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ============================================
|
|
171
|
+
EXPANDABLE HEADER
|
|
172
|
+
============================================ */
|
|
173
|
+
|
|
174
|
+
.header-expandable {
|
|
175
|
+
background: var(--color-brand-secondary);
|
|
176
|
+
padding: 24px 16px;
|
|
177
|
+
color: white;
|
|
178
|
+
position: sticky;
|
|
179
|
+
top: 0;
|
|
180
|
+
z-index: 10;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.header-expandable-inner {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
gap: 16px;
|
|
187
|
+
max-width: 640px;
|
|
188
|
+
margin: 0 auto;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@media (min-width: 768px) {
|
|
192
|
+
.header-expandable {
|
|
193
|
+
padding: 32px var(--content-padding-md);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@media (min-width: 992px) {
|
|
198
|
+
.header-expandable {
|
|
199
|
+
padding-left: var(--content-padding-lg);
|
|
200
|
+
padding-right: var(--content-padding-lg);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@media (min-width: 1440px) {
|
|
205
|
+
.header-expandable {
|
|
206
|
+
padding-left: var(--content-padding-xl);
|
|
207
|
+
padding-right: var(--content-padding-xl);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@media (min-width: 1768px) {
|
|
212
|
+
.header-expandable {
|
|
213
|
+
padding-left: var(--content-padding-2xl);
|
|
214
|
+
padding-right: var(--content-padding-2xl);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@media (min-width: 1920px) {
|
|
219
|
+
.header-expandable {
|
|
220
|
+
padding-left: var(--content-padding-3xl);
|
|
221
|
+
padding-right: var(--content-padding-3xl);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.header-expandable__text {
|
|
226
|
+
margin-bottom: 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.header-expandable__title {
|
|
230
|
+
font-size: var(--font-size-2xl);
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
color: white;
|
|
233
|
+
margin: 0 0 8px 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@media (min-width: 768px) {
|
|
237
|
+
.header-expandable__title {
|
|
238
|
+
font-size: var(--font-size-3xl);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.header-expandable__subtitle {
|
|
243
|
+
font-size: var(--font-size-sm);
|
|
244
|
+
font-weight: 400;
|
|
245
|
+
color: white;
|
|
246
|
+
opacity: 0.85;
|
|
247
|
+
margin: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
@media (min-width: 768px) {
|
|
251
|
+
.header-expandable__subtitle {
|
|
252
|
+
font-size: var(--font-size-base);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ============================================
|
|
257
|
+
CONTENT WRAPPER
|
|
258
|
+
============================================ */
|
|
259
|
+
|
|
260
|
+
.content-wrapper {
|
|
261
|
+
position: relative;
|
|
262
|
+
z-index: 10;
|
|
263
|
+
background: var(--color-background-neutral-primary);
|
|
264
|
+
border-radius: 24px 24px 0 0;
|
|
265
|
+
padding: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@media (min-width: 768px) {
|
|
269
|
+
.content-wrapper {
|
|
270
|
+
border-radius: 16px 16px 0 0;
|
|
271
|
+
width: 100%;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.content-inner {
|
|
276
|
+
padding: 20px 16px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@media (min-width: 768px) {
|
|
280
|
+
.content-inner {
|
|
281
|
+
padding: 32px var(--content-padding-md);
|
|
282
|
+
max-width: calc(var(--content-max-width-md) + (var(--content-padding-md) * 2));
|
|
283
|
+
margin: 0 auto;
|
|
284
|
+
width: 100%;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
@media (min-width: 992px) {
|
|
289
|
+
.content-inner {
|
|
290
|
+
padding: 32px var(--content-padding-lg);
|
|
291
|
+
max-width: calc(var(--content-max-width-md) + (var(--content-padding-md) * 2));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@media (min-width: 1440px) {
|
|
296
|
+
.content-inner {
|
|
297
|
+
padding: 32px var(--content-padding-lg);
|
|
298
|
+
max-width: calc(var(--content-max-width-lg) + (var(--content-padding-lg) * 2));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@media (min-width: 1768px) {
|
|
303
|
+
.content-inner {
|
|
304
|
+
/* Keep xl max-width, only increase padding */
|
|
305
|
+
padding: 32px var(--content-padding-2xl);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@media (min-width: 1920px) {
|
|
310
|
+
.content-inner {
|
|
311
|
+
/* Keep xl max-width, only increase padding */
|
|
312
|
+
padding: 32px var(--content-padding-3xl);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { input, computed, Directive } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content width preset values
|
|
5
|
+
* - 'narrow' - 640px max width (reading content)
|
|
6
|
+
* - 'standard' - 1024px max width (default)
|
|
7
|
+
* - 'wide' - 1440px max width (dashboards)
|
|
8
|
+
* - 'full' - 100% width (no max)
|
|
9
|
+
*/
|
|
10
|
+
export type ContentWidth = 'narrow' | 'standard' | 'wide' | 'full';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MobilePageBase
|
|
14
|
+
*
|
|
15
|
+
* Shared base class for mobile page components (ds-mobile-page-main, ds-mobile-page-details).
|
|
16
|
+
* Provides consistent content width control across all page types.
|
|
17
|
+
*
|
|
18
|
+
* **Padding Strategy:**
|
|
19
|
+
* - All pages use 20px horizontal padding globally
|
|
20
|
+
* - For tappable lists, use negative margins (e.g., margin: 0 -8px) to create full-width sections
|
|
21
|
+
* - This approach simplifies padding management and provides consistency
|
|
22
|
+
*
|
|
23
|
+
* @internal This is a base class and should not be used directly.
|
|
24
|
+
*/
|
|
25
|
+
@Directive()
|
|
26
|
+
export abstract class MobilePageBase {
|
|
27
|
+
/**
|
|
28
|
+
* Maximum content width (desktop only)
|
|
29
|
+
*
|
|
30
|
+
* **Options:**
|
|
31
|
+
* - `'narrow'` (640px) - For reading content, forms
|
|
32
|
+
* - `'standard'` (1024px) - Default for most pages
|
|
33
|
+
* - `'wide'` (1440px) - For dashboards, tables
|
|
34
|
+
* - `'full'` - No max-width constraint
|
|
35
|
+
*
|
|
36
|
+
* **Note:** Only applies on desktop (>= 768px). Mobile is always full width.
|
|
37
|
+
*
|
|
38
|
+
* @default 'standard'
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```html
|
|
42
|
+
* <!-- Narrow reading layout -->
|
|
43
|
+
* <ds-mobile-page-main title="Article" contentWidth="narrow">
|
|
44
|
+
*
|
|
45
|
+
* <!-- Wide dashboard -->
|
|
46
|
+
* <ds-mobile-page-main title="Dashboard" contentWidth="wide">
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
contentWidth = input<ContentWidth>('standard');
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolved max-width value (computed)
|
|
53
|
+
* Maps preset values to pixel values
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
protected maxWidthValue = computed(() => {
|
|
58
|
+
const w = this.contentWidth();
|
|
59
|
+
|
|
60
|
+
const widthMap: Record<ContentWidth, string> = {
|
|
61
|
+
'narrow': '640px',
|
|
62
|
+
'standard': '1024px',
|
|
63
|
+
'wide': '1440px',
|
|
64
|
+
'full': '100%'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return widthMap[w];
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|