@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,9 @@
1
+ /**
2
+ * Mobile Post Detail Modal Module
3
+ *
4
+ * Service and component for displaying posts in a modal
5
+ */
6
+
7
+ export * from './ds-mobile-post-detail-modal';
8
+ export * from './ds-mobile-post-detail-modal.service';
9
+
@@ -0,0 +1,2 @@
1
+ export * from './long-press.directive';
2
+
@@ -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,3 @@
1
+ export * from './mobile-page-base';
2
+ export * from './directives';
3
+
@@ -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
+