@propbinder/mobile-design 0.2.48 → 0.2.50

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 (221) hide show
  1. package/ng-package.json +24 -0
  2. package/package.json +3 -39
  3. package/src/animations/page-transitions.ts +165 -0
  4. package/src/assets/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
  5. package/src/assets/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
  6. package/src/assets/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
  7. package/src/components/action-list-item/ds-mobile-action-list-item.ts +102 -0
  8. package/src/components/action-list-item/index.ts +2 -0
  9. package/src/components/app-icon/ds-app-icon.ts +133 -0
  10. package/src/components/app-icon/index.ts +2 -0
  11. package/src/components/attachment-preview/ds-mobile-attachment-preview.css +139 -0
  12. package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +164 -0
  13. package/src/components/attachment-preview/index.ts +1 -0
  14. package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +142 -0
  15. package/src/components/avatar-with-badge/index.ts +2 -0
  16. package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +71 -0
  17. package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +121 -0
  18. package/src/components/booking-modal/ds-mobile-booking-modal.ts +598 -0
  19. package/src/components/booking-modal/ds-mobile-booking-summary.ts +161 -0
  20. package/src/components/booking-modal/index.ts +4 -0
  21. package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +266 -0
  22. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +146 -0
  23. package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +156 -0
  24. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +101 -0
  25. package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +169 -0
  26. package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +211 -0
  27. package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +578 -0
  28. package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +614 -0
  29. package/src/components/bottom-sheet/index.ts +8 -0
  30. package/src/components/bottom-sheet/modal-shadow-fix.ts +42 -0
  31. package/src/components/card-inline/ds-mobile-card-inline.ts +301 -0
  32. package/src/components/card-inline/index.ts +2 -0
  33. package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +118 -0
  34. package/src/components/card-inline-banner/index.ts +1 -0
  35. package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +120 -0
  36. package/src/components/card-inline-contact/index.ts +1 -0
  37. package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +141 -0
  38. package/src/components/card-inline-file/index.ts +1 -0
  39. package/src/components/chat-modal/ds-mobile-chat-modal.css +159 -0
  40. package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +105 -0
  41. package/src/components/chat-modal/ds-mobile-chat-modal.ts +918 -0
  42. package/src/components/chat-modal/index.ts +8 -0
  43. package/src/components/comment/ds-mobile-comment.ts +568 -0
  44. package/src/components/comment/index.ts +2 -0
  45. package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
  46. package/src/components/contact-list-item/index.ts +2 -0
  47. package/src/components/content/ds-mobile-content.ts +139 -0
  48. package/src/components/content/index.ts +2 -0
  49. package/src/components/dropdown/ds-mobile-dropdown.css +199 -0
  50. package/src/components/dropdown/ds-mobile-dropdown.ts +340 -0
  51. package/src/components/dropdown/index.ts +2 -0
  52. package/src/components/ds-mobile-tabs.css +407 -0
  53. package/src/components/ds-mobile-tabs.ts +216 -0
  54. package/src/components/empty-state/ds-mobile-empty-state.ts +120 -0
  55. package/src/components/empty-state/index.ts +2 -0
  56. package/src/components/fab/ds-mobile-fab.ts +315 -0
  57. package/src/components/fab/index.ts +1 -0
  58. package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +121 -0
  59. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +189 -0
  60. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +135 -0
  61. package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +656 -0
  62. package/src/components/facility-creation-modal/index.ts +9 -0
  63. package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +105 -0
  64. package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +188 -0
  65. package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +460 -0
  66. package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +134 -0
  67. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +69 -0
  68. package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +379 -0
  69. package/src/components/facility-detail-modal/index.ts +2 -0
  70. package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
  71. package/src/components/file-attachment/index.ts +2 -0
  72. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +214 -0
  73. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +84 -0
  74. package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +424 -0
  75. package/src/components/handbook-detail-modal/index.ts +3 -0
  76. package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +175 -0
  77. package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +533 -0
  78. package/src/components/handbook-folder/index.ts +4 -0
  79. package/src/components/header-content/ds-mobile-header-content.ts +222 -0
  80. package/src/components/header-content/index.ts +2 -0
  81. package/src/components/illustration/ds-mobile-illustration.ts +124 -0
  82. package/src/components/illustration/index.ts +2 -0
  83. package/src/components/index.ts +124 -0
  84. package/src/components/inline-photo/ds-mobile-inline-photo.ts +361 -0
  85. package/src/components/inline-photo/index.ts +1 -0
  86. package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +132 -0
  87. package/src/components/inline-tabs/index.ts +2 -0
  88. package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +350 -0
  89. package/src/components/interactive-list-item-booking/index.ts +1 -0
  90. package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +321 -0
  91. package/src/components/interactive-list-item-inquiry/index.ts +2 -0
  92. package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +237 -0
  93. package/src/components/interactive-list-item-message/index.ts +2 -0
  94. package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +549 -0
  95. package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
  96. package/src/components/interactive-list-item-post/index.ts +13 -0
  97. package/src/components/lightbox/ds-mobile-lightbox-footer.ts +315 -0
  98. package/src/components/lightbox/ds-mobile-lightbox-header.ts +202 -0
  99. package/src/components/lightbox/ds-mobile-lightbox-image.ts +484 -0
  100. package/src/components/lightbox/ds-mobile-lightbox-pdf.css +377 -0
  101. package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
  102. package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
  103. package/src/components/lightbox/ds-mobile-lightbox.service.ts +296 -0
  104. package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
  105. package/src/components/lightbox/index.ts +22 -0
  106. package/src/components/list-item/ds-mobile-list-item.ts +603 -0
  107. package/src/components/list-item/index.ts +2 -0
  108. package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
  109. package/src/components/list-item-static/index.ts +2 -0
  110. package/src/components/loader-overlay/ds-mobile-loader-overlay.css +49 -0
  111. package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +77 -0
  112. package/src/components/loader-overlay/index.ts +1 -0
  113. package/src/components/logo/ds-logo.ts +95 -0
  114. package/src/components/logo/index.ts +2 -0
  115. package/src/components/message-bubble/ds-mobile-message-bubble.ts +633 -0
  116. package/src/components/message-bubble/index.ts +7 -0
  117. package/src/components/message-composer/ds-mobile-message-composer.ts +1146 -0
  118. package/src/components/message-composer/index.ts +7 -0
  119. package/src/components/modal/ds-mobile-modal.css +163 -0
  120. package/src/components/modal/ds-mobile-modal.service.ts +329 -0
  121. package/src/components/modal/index.ts +8 -0
  122. package/src/components/modal-base/ds-mobile-modal-base.css +378 -0
  123. package/src/components/modal-base/ds-mobile-modal-base.ts +261 -0
  124. package/src/components/modal-base/index.ts +2 -0
  125. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +112 -0
  126. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +93 -0
  127. package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +442 -0
  128. package/src/components/new-inquiry-modal/index.ts +4 -0
  129. package/src/components/offline-banner/ds-mobile-offline-banner.ts +135 -0
  130. package/src/components/offline-banner/index.ts +1 -0
  131. package/src/components/page-details/ds-mobile-page-details.css +83 -0
  132. package/src/components/page-details/ds-mobile-page-details.ts +282 -0
  133. package/src/components/page-details/index.ts +2 -0
  134. package/src/components/page-main/ds-mobile-page-main.css +68 -0
  135. package/src/components/page-main/ds-mobile-page-main.ts +421 -0
  136. package/src/components/page-main/index.ts +2 -0
  137. package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
  138. package/src/components/post-composer/index.ts +2 -0
  139. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +390 -0
  140. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +108 -0
  141. package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +722 -0
  142. package/src/components/post-detail-modal/index.ts +9 -0
  143. package/src/components/property-banner/ds-mobile-property-banner.ts +95 -0
  144. package/src/components/property-banner/index.ts +2 -0
  145. package/src/components/section/ds-mobile-section.ts +263 -0
  146. package/src/components/section/index.ts +2 -0
  147. package/src/components/shared/directives/index.ts +2 -0
  148. package/src/components/shared/directives/long-press.directive.ts +212 -0
  149. package/src/components/shared/index.ts +3 -0
  150. package/src/components/shared/mobile-modal-base.ts +457 -0
  151. package/src/components/shared/mobile-page-base.ts +204 -0
  152. package/src/components/swiper/ds-mobile-swiper-with-nav.ts +160 -0
  153. package/src/components/swiper/ds-mobile-swiper.ts +327 -0
  154. package/src/components/swiper/index.ts +3 -0
  155. package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +129 -0
  156. package/src/components/system-message-banner/index.ts +2 -0
  157. package/src/components/tab-bar/ds-mobile-tab-bar.css +533 -0
  158. package/src/components/tab-bar/ds-mobile-tab-bar.ts +735 -0
  159. package/src/components/tab-bar/index.ts +2 -0
  160. package/src/components/tabs/ds-mobile-tabs.css +25 -0
  161. package/src/components/tabs/ds-mobile-tabs.ts +89 -0
  162. package/src/components/tabs/index.ts +2 -0
  163. package/src/components/text-input/ds-text-input.ts +287 -0
  164. package/src/components/text-input/index.ts +2 -0
  165. package/src/examples/booking.page.ts +434 -0
  166. package/src/examples/community.page.ts +776 -0
  167. package/src/examples/handbook.page.ts +324 -0
  168. package/src/examples/home.page.ts +347 -0
  169. package/src/examples/index.ts +12 -0
  170. package/src/examples/inquiries.example.ts +273 -0
  171. package/src/examples/inquiry-detail.example.css +189 -0
  172. package/src/examples/inquiry-detail.example.ts +415 -0
  173. package/src/examples/mobile-tabs-example.component.ts +208 -0
  174. package/src/examples/post-create.page.ts +311 -0
  175. package/src/examples/post-detail.page.ts +296 -0
  176. package/src/examples/sign-in.page.ts +291 -0
  177. package/src/examples/whitelabel-demo-modal.component.ts +1094 -0
  178. package/src/examples/whitelabel-demo-modal.service.ts +77 -0
  179. package/src/models/index.ts +7 -0
  180. package/src/models/post.model.ts +41 -0
  181. package/src/pages/community.page.ts +769 -0
  182. package/src/pages/handbook.page.ts +388 -0
  183. package/src/pages/home.page.ts +303 -0
  184. package/src/pages/index.ts +11 -0
  185. package/src/pages/inquiries.example.ts +273 -0
  186. package/src/pages/inquiry-detail.example.css +189 -0
  187. package/src/pages/inquiry-detail.example.ts +415 -0
  188. package/src/pages/mobile-tabs-example.component.ts +179 -0
  189. package/src/pages/post-create.page.ts +311 -0
  190. package/src/pages/post-detail.page.ts +296 -0
  191. package/src/pages/sign-in.page.ts +291 -0
  192. package/src/pages/whitelabel-demo-modal.component.ts +1094 -0
  193. package/src/pages/whitelabel-demo-modal.service.ts +77 -0
  194. package/src/public-api.ts +6 -0
  195. package/src/services/base-modal.service.ts +101 -0
  196. package/src/services/index.ts +11 -0
  197. package/src/services/posts.service.ts +542 -0
  198. package/src/services/tracking-permission.service.ts +88 -0
  199. package/src/services/user.service.ts +60 -0
  200. package/src/services/whitelabel.service.ts +675 -0
  201. package/{styles → src/styles}/ionic.css +25 -0
  202. package/tsconfig.lib.json +17 -0
  203. package/tsconfig.lib.prod.json +9 -0
  204. package/tsconfig.spec.json +13 -0
  205. package/fesm2022/propbinder-mobile-design.mjs +0 -26168
  206. package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
  207. package/index.d.ts +0 -8169
  208. /package/{assets → src/assets}/fonts/Brockmann-Bold.otf +0 -0
  209. /package/{assets → src/assets}/fonts/Brockmann-BoldItalic.otf +0 -0
  210. /package/{assets → src/assets}/fonts/Brockmann-Medium.otf +0 -0
  211. /package/{assets → src/assets}/fonts/Brockmann-MediumItalic.otf +0 -0
  212. /package/{assets → src/assets}/fonts/Brockmann-Regular.otf +0 -0
  213. /package/{assets → src/assets}/fonts/Brockmann-RegularItalic.otf +0 -0
  214. /package/{assets → src/assets}/fonts/Brockmann-SemiBold.otf +0 -0
  215. /package/{assets → src/assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
  216. /package/{assets → src/assets}/fonts/Brockmann_desktop_license.pdf +0 -0
  217. /package/{assets → src/assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
  218. /package/{assets → src/assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
  219. /package/{assets → src/assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
  220. /package/{styles → src/components/shared}/mobile-common.css +0 -0
  221. /package/{styles → src/components/shared}/mobile-page-base.css +0 -0
@@ -0,0 +1,460 @@
1
+ import { Component, signal, computed, input, CUSTOM_ELEMENTS_SCHEMA, OnInit, PLATFORM_ID, inject } from '@angular/core';
2
+ import { CommonModule, isPlatformBrowser } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { ModalController, IonPicker, IonPickerColumn, IonPickerColumnOption } from '@ionic/angular/standalone';
5
+ import { DsMobileDropdownComponent } from '../../dropdown';
6
+ import { DsInputTimeComponent, DsLabelComponent } from '@propbinder/design-system';
7
+ import { DsMobileBottomSheetWrapperComponent } from '../../bottom-sheet/ds-mobile-bottom-sheet-wrapper';
8
+ import { DsMobileBottomSheetHeaderComponent } from '../../bottom-sheet/ds-mobile-bottom-sheet-header';
9
+
10
+ export interface WhenCanBookData {
11
+ days: string[];
12
+ timeRange: { start: string; end: string };
13
+ duration: string;
14
+ }
15
+
16
+ /**
17
+ * DsMobileWhenCanBookSheetComponent
18
+ *
19
+ * Bottom sheet for selecting when a facility can be booked (days, time range, duration).
20
+ * "Vælg selv" opens a drum-roll IonPopover on mobile, or inline number inputs on desktop.
21
+ */
22
+ @Component({
23
+ selector: 'ds-mobile-when-can-book-sheet',
24
+ standalone: true,
25
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
26
+ imports: [
27
+ CommonModule,
28
+ FormsModule,
29
+ DsInputTimeComponent,
30
+ DsLabelComponent,
31
+ DsMobileBottomSheetWrapperComponent,
32
+ DsMobileBottomSheetHeaderComponent,
33
+ DsMobileDropdownComponent,
34
+ IonPicker,
35
+ IonPickerColumn,
36
+ IonPickerColumnOption,
37
+ ],
38
+ template: `
39
+ <ds-mobile-bottom-sheet-wrapper>
40
+ <!-- Header with back and done buttons -->
41
+ <ds-mobile-bottom-sheet-header
42
+ title="Kan bookes"
43
+ leftButtonLabel="Annuller"
44
+ rightButtonLabel="Gem"
45
+ [rightButtonDisabled]="!isValid()"
46
+ (leftButtonClick)="dismiss()"
47
+ (rightButtonClick)="confirmSelection()">
48
+ </ds-mobile-bottom-sheet-header>
49
+
50
+ <div class="form-content">
51
+ <!-- Day chips -->
52
+ <div class="section">
53
+ <ds-label size="md" className="form-section-label">Kan bookes</ds-label>
54
+ <div class="day-chips">
55
+ @for (day of days; track day) {
56
+ <button
57
+ type="button"
58
+ class="day-chip"
59
+ [class.active]="selectedDays().has(day)"
60
+ (click)="toggleDay(day)">
61
+ {{ day }}
62
+ </button>
63
+ }
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Time range -->
68
+ <div class="section">
69
+ <ds-label size="md" className="form-section-label">I tidsrummet</ds-label>
70
+ <div class="time-inputs">
71
+ <ds-input-time size="lg" [(ngModel)]="startTime"></ds-input-time>
72
+ <span class="separator">til</span>
73
+ <ds-input-time size="lg" [(ngModel)]="endTime"></ds-input-time>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Duration chips -->
78
+ <div class="section">
79
+ <ds-label size="md" className="form-section-label">Varighed</ds-label>
80
+ <div class="duration-chips">
81
+ @for (duration of durations; track duration.value) {
82
+ <button
83
+ type="button"
84
+ class="duration-chip"
85
+ [id]="duration.value === 'Vælg selv' ? 'vaelg-selv-chip' : null"
86
+ [class.active]="selectedDuration() === duration.value"
87
+ (click)="selectDuration(duration.value)">
88
+ @if (duration.value === 'Vælg selv' && selectedDuration() === 'Vælg selv') {
89
+ {{ customDurationLabel() || 'Vælg selv' }}
90
+ } @else {
91
+ {{ duration.label }}
92
+ }
93
+ </button>
94
+ }
95
+ </div>
96
+
97
+ <!-- Desktop: inline inputs shown when "Vælg selv" is active -->
98
+ @if (showInlineInputs()) {
99
+ <div class="custom-duration-inputs">
100
+ <input
101
+ type="number"
102
+ class="duration-number-input"
103
+ min="0"
104
+ [ngModel]="customHours()"
105
+ (ngModelChange)="customHours.set(+$event)"
106
+ (blur)="normalize()"
107
+ />
108
+ <span class="duration-unit">timer</span>
109
+ <input
110
+ type="number"
111
+ class="duration-number-input"
112
+ min="0"
113
+ max="59"
114
+ [ngModel]="customMinutes()"
115
+ (ngModelChange)="customMinutes.set(+$event)"
116
+ (blur)="normalize()"
117
+ />
118
+ <span class="duration-unit">min</span>
119
+ </div>
120
+ }
121
+
122
+ <!-- Mobile: drum-roll popover anchored to the "Vælg selv" chip -->
123
+ @if (!isDesktop()) {
124
+ <ds-mobile-dropdown
125
+ trigger="vaelg-selv-chip"
126
+ [isOpen]="pickerOpen()"
127
+ maxWidth="240px"
128
+ position="above"
129
+ (closed)="onPickerDismiss()">
130
+ <ng-template #customContent>
131
+ <ion-picker style="--background: transparent; --fade-background-rgb: transparent; --highlight-background: rgba(0, 0, 0, 0.05); --highlight-border-radius: 9999px;">
132
+ <ion-picker-column
133
+ [value]="customHours()"
134
+ style="--padding-start: 4px; --padding-end: 4px;"
135
+ (ionChange)="customHours.set(+($event.detail.value ?? customHours()))">
136
+ <div slot="suffix" class="picker-suffix">t</div>
137
+ @for (h of hours; track h) {
138
+ <ion-picker-column-option [value]="h">{{ h }}</ion-picker-column-option>
139
+ }
140
+ </ion-picker-column>
141
+ <ion-picker-column
142
+ [value]="customMinutes()"
143
+ style="--padding-start: 4px; --padding-end: 4px;"
144
+ (ionChange)="customMinutes.set(+($event.detail.value ?? customMinutes()))">
145
+ <div slot="suffix" class="picker-suffix">min</div>
146
+ @for (m of minuteOptions; track m) {
147
+ <ion-picker-column-option [value]="m">{{ m }}</ion-picker-column-option>
148
+ }
149
+ </ion-picker-column>
150
+ </ion-picker>
151
+ </ng-template>
152
+ </ds-mobile-dropdown>
153
+ }
154
+ </div>
155
+ </div>
156
+ </ds-mobile-bottom-sheet-wrapper>
157
+ `,
158
+ styles: [`
159
+ .form-content {
160
+ padding: 16px;
161
+ }
162
+
163
+ .section {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 8px;
167
+ margin-bottom: 24px;
168
+ }
169
+
170
+ .section:last-child {
171
+ margin-bottom: 0;
172
+ }
173
+
174
+ /* Label spacing */
175
+ ::ng-deep ds-label.form-section-label {
176
+ display: block;
177
+ margin-bottom: 12px;
178
+ }
179
+
180
+ ::ng-deep .form-section-label {
181
+ font-weight: 500;
182
+ }
183
+
184
+ /* Day chips */
185
+ .day-chips {
186
+ display: flex;
187
+ gap: 8px;
188
+ flex-wrap: wrap;
189
+ width: 100%;
190
+ }
191
+
192
+ .day-chip {
193
+ flex: none;
194
+ width: 64px;
195
+ padding: 10px 16px;
196
+ border: 1px solid var(--border-color-default, #d1d5db);
197
+ border-radius: 8px;
198
+ background: transparent;
199
+ color: var(--color-text-primary);
200
+ font-size: 14px;
201
+ font-weight: 500;
202
+ cursor: pointer;
203
+ transition: all 0.2s ease;
204
+ outline: none;
205
+ }
206
+
207
+ .day-chip:hover {
208
+ background: var(--color-bg-secondary);
209
+ }
210
+
211
+ .day-chip.active {
212
+ background: var(--color-accent);
213
+ color: white;
214
+ border-color: var(--color-accent);
215
+ }
216
+
217
+ /* Time inputs */
218
+ .time-inputs {
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 12px;
222
+ }
223
+
224
+ .separator {
225
+ color: var(--color-text-secondary);
226
+ font-size: 14px;
227
+ }
228
+
229
+ /* Duration chips */
230
+ .duration-chips {
231
+ display: flex;
232
+ gap: 8px;
233
+ flex-wrap: wrap;
234
+ }
235
+
236
+ .duration-chip {
237
+ padding: 10px 16px;
238
+ border: 1px solid var(--border-color-default, #d1d5db);
239
+ border-radius: 8px;
240
+ background: transparent;
241
+ color: var(--color-text-primary);
242
+ font-size: 14px;
243
+ font-weight: 500;
244
+ cursor: pointer;
245
+ transition: all 0.2s ease;
246
+ outline: none;
247
+ }
248
+
249
+ .duration-chip:hover {
250
+ background: var(--color-bg-secondary);
251
+ }
252
+
253
+ .duration-chip.active {
254
+ background: var(--color-accent);
255
+ color: white;
256
+ border-color: var(--color-accent);
257
+ }
258
+
259
+ /* Desktop inline custom duration inputs */
260
+ .custom-duration-inputs {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 8px;
264
+ margin-top: 12px;
265
+ animation: fadeSlideIn 0.15s ease;
266
+ }
267
+
268
+ @keyframes fadeSlideIn {
269
+ from { opacity: 0; transform: translateY(-4px); }
270
+ to { opacity: 1; transform: translateY(0); }
271
+ }
272
+
273
+ .duration-number-input {
274
+ width: 64px;
275
+ padding: 8px 10px;
276
+ border: 1px solid var(--border-color-default, #d1d5db);
277
+ border-radius: 8px;
278
+ background: transparent;
279
+ color: var(--color-text-primary);
280
+ font-size: 14px;
281
+ font-weight: 500;
282
+ text-align: center;
283
+ outline: none;
284
+ transition: border-color 0.2s ease;
285
+ }
286
+
287
+ .duration-number-input:focus {
288
+ border-color: var(--color-accent);
289
+ }
290
+
291
+ /* Remove browser spin buttons */
292
+ .duration-number-input::-webkit-inner-spin-button,
293
+ .duration-number-input::-webkit-outer-spin-button {
294
+ -webkit-appearance: none;
295
+ margin: 0;
296
+ }
297
+ .duration-number-input[type=number] {
298
+ -moz-appearance: textfield;
299
+ }
300
+
301
+ .duration-unit {
302
+ color: var(--color-text-secondary);
303
+ font-size: 14px;
304
+ }
305
+
306
+ .picker-suffix {
307
+ margin-left: -24px;
308
+ font-size: 13px;
309
+ color: var(--color-text-secondary);
310
+ }
311
+
312
+ `]
313
+ })
314
+ export class DsMobileWhenCanBookSheetComponent implements OnInit {
315
+ private platformId = inject(PLATFORM_ID);
316
+
317
+ // Platform
318
+ isDesktop = signal<boolean>(false);
319
+
320
+ // State
321
+ selectedDays = signal<Set<string>>(new Set(['Fre', 'Lør', 'Søn']));
322
+ startTime = signal('09:00');
323
+ endTime = signal('17:30');
324
+ selectedDuration = signal('1 time');
325
+
326
+ /**
327
+ * Maximum number of days available in the "Vælg selv" picker/inputs.
328
+ * Defaults to 30 (one full month). Override via componentProps when opening the sheet.
329
+ */
330
+ maxDays = input<number>(30);
331
+
332
+ // Custom duration state
333
+ pickerOpen = signal<boolean>(false);
334
+ customDays = signal<number>(0);
335
+ customHours = signal<number>(1);
336
+ customMinutes = signal<number>(0);
337
+
338
+ customDurationLabel = computed(() => {
339
+ const d = this.customDays();
340
+ const h = this.customHours();
341
+ const m = this.customMinutes();
342
+ const parts: string[] = [];
343
+ if (d > 0) parts.push(`${d} dage`);
344
+ if (h > 0) parts.push(`${h}t`);
345
+ if (m > 0) parts.push(`${m}min`);
346
+ return parts.join(' ');
347
+ });
348
+
349
+ showInlineInputs = computed(() =>
350
+ this.selectedDuration() === 'Vælg selv' && this.isDesktop()
351
+ );
352
+
353
+ // Options
354
+ days = ['Man', 'Tir', 'Ons', 'Tor', 'Fre', 'Lør', 'Søn'];
355
+ durations = [
356
+ { value: '30 min', label: '30 min' },
357
+ { value: '1 time', label: '1 time' },
358
+ { value: '2 timer', label: '2 timer' },
359
+ { value: 'Hele dagen', label: 'Hele dagen' },
360
+ { value: 'Vælg selv', label: 'Vælg selv' }
361
+ ];
362
+ daysOptions = computed(() => Array.from({ length: this.maxDays() + 1 }, (_, i) => i));
363
+ hours = Array.from({ length: 24 }, (_, i) => i);
364
+ minuteOptions = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
365
+
366
+ // Validation
367
+ isValid = computed(() => {
368
+ if (this.selectedDuration() === 'Vælg selv' && this.customDurationLabel() === '') {
369
+ return false;
370
+ }
371
+ return this.selectedDays().size > 0 &&
372
+ this.startTime() &&
373
+ this.endTime();
374
+ });
375
+
376
+ constructor(private modalController: ModalController) {}
377
+
378
+ ngOnInit(): void {
379
+ if (isPlatformBrowser(this.platformId)) {
380
+ this.isDesktop.set(window.innerWidth >= 768);
381
+ window.addEventListener('resize', () => {
382
+ this.isDesktop.set(window.innerWidth >= 768);
383
+ });
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Handle duration chip selection.
389
+ * On mobile, opening "Vælg selv" triggers the IonPopover via its trigger id.
390
+ * On desktop, it shows the inline inputs.
391
+ */
392
+ selectDuration(value: string): void {
393
+ this.selectedDuration.set(value);
394
+ if (value === 'Vælg selv' && !this.isDesktop()) {
395
+ this.pickerOpen.set(true);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Normalizes days/hours/minutes so values never overflow their units.
401
+ * e.g. 0d 48t 0min → 2d 0t 0min; 0d 0t 75min → 0d 1t 15min.
402
+ * Caps days at maxDays().
403
+ */
404
+ normalize(): void {
405
+ const totalMinutes = this.customDays() * 24 * 60
406
+ + this.customHours() * 60
407
+ + this.customMinutes();
408
+
409
+ const maxTotalMinutes = this.maxDays() * 24 * 60 + 23 * 60 + 59;
410
+ const clamped = Math.max(0, Math.min(totalMinutes, maxTotalMinutes));
411
+
412
+ const d = Math.floor(clamped / (24 * 60));
413
+ const h = Math.floor((clamped % (24 * 60)) / 60);
414
+ const m = clamped % 60;
415
+
416
+ this.customDays.set(d);
417
+ this.customHours.set(h);
418
+ this.customMinutes.set(m);
419
+ }
420
+
421
+ /**
422
+ * Called when the mobile picker popover is dismissed — normalize and commit.
423
+ */
424
+ onPickerDismiss(): void {
425
+ this.normalize();
426
+ this.pickerOpen.set(false);
427
+ }
428
+
429
+ /**
430
+ * Toggle day selection
431
+ */
432
+ toggleDay(day: string): void {
433
+ const current = new Set(this.selectedDays());
434
+ current.has(day) ? current.delete(day) : current.add(day);
435
+ this.selectedDays.set(current);
436
+ }
437
+
438
+ /**
439
+ * Confirm selection and dismiss with data
440
+ */
441
+ confirmSelection(): void {
442
+ const duration = this.selectedDuration() === 'Vælg selv'
443
+ ? this.customDurationLabel()
444
+ : this.selectedDuration();
445
+
446
+ const data: WhenCanBookData = {
447
+ days: Array.from(this.selectedDays()),
448
+ timeRange: { start: this.startTime(), end: this.endTime() },
449
+ duration
450
+ };
451
+ this.modalController.dismiss({ value: data }, 'select');
452
+ }
453
+
454
+ /**
455
+ * Dismiss without saving
456
+ */
457
+ dismiss(): void {
458
+ this.modalController.dismiss(null, 'cancel');
459
+ }
460
+ }
@@ -0,0 +1,134 @@
1
+ import { Component, signal, computed, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { ModalController } from '@ionic/angular/standalone';
4
+ import { DsCheckboxComponent } from '@propbinder/design-system';
5
+ import { DsMobileActionListItemComponent } from '../../action-list-item/ds-mobile-action-list-item';
6
+ import { DsMobileBottomSheetWrapperComponent } from '../../bottom-sheet/ds-mobile-bottom-sheet-wrapper';
7
+ import { DsMobileBottomSheetHeaderComponent } from '../../bottom-sheet/ds-mobile-bottom-sheet-header';
8
+
9
+ export interface WhoCanBookOption {
10
+ value: string;
11
+ label: string;
12
+ isMaster?: boolean;
13
+ }
14
+
15
+ /**
16
+ * DsMobileWhoCanBookSheetComponent
17
+ *
18
+ * Bottom sheet for selecting who can book a facility (multi-select with checkboxes).
19
+ */
20
+ @Component({
21
+ selector: 'ds-mobile-who-can-book-sheet',
22
+ standalone: true,
23
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
24
+ imports: [
25
+ CommonModule,
26
+ DsCheckboxComponent,
27
+ DsMobileActionListItemComponent,
28
+ DsMobileBottomSheetWrapperComponent,
29
+ DsMobileBottomSheetHeaderComponent
30
+ ],
31
+ template: `
32
+ <ds-mobile-bottom-sheet-wrapper>
33
+ <!-- Header with back and done buttons -->
34
+ <ds-mobile-bottom-sheet-header
35
+ title="Hvem kan booke"
36
+ leftButtonLabel="Tilbage"
37
+ rightButtonLabel="Færdig"
38
+ [rightButtonDisabled]="!hasSelectionChanged()"
39
+ (leftButtonClick)="dismiss()"
40
+ (rightButtonClick)="confirmSelection()">
41
+ </ds-mobile-bottom-sheet-header>
42
+
43
+ <!-- Options list -->
44
+ <div class="options-list">
45
+ @for (option of options; track option.value; let isLast = $last) {
46
+ <ds-mobile-action-list-item
47
+ [title]="option.label"
48
+ [showDivider]="!isLast"
49
+ (itemClick)="toggleOption(option.value)">
50
+ <div content-trailing>
51
+ <ds-checkbox
52
+ [checked]="selectedOptions().has(option.value)"
53
+ [showLabel]="false"
54
+ size="md"
55
+ (click)="$event.stopPropagation()"
56
+ (checkedChange)="toggleOption(option.value)">
57
+ </ds-checkbox>
58
+ </div>
59
+ </ds-mobile-action-list-item>
60
+ }
61
+ </div>
62
+ </ds-mobile-bottom-sheet-wrapper>
63
+ `,
64
+ styles: [`
65
+ .options-list {
66
+ display: flex;
67
+ flex-direction: column;
68
+ padding: 0 16px;
69
+ }
70
+ `]
71
+ })
72
+ export class DsMobileWhoCanBookSheetComponent {
73
+ options: WhoCanBookOption[] = [
74
+ { value: 'alle', label: 'Alle', isMaster: true },
75
+ { value: 'faelleskab-a', label: 'Fælleskab A' },
76
+ { value: 'faelleskab-b', label: 'Fælleskab B' },
77
+ { value: 'faelleskab-c', label: 'Fælleskab C' }
78
+ ];
79
+
80
+ // Signal-based state for selected options - all selected by default
81
+ selectedOptions = signal<Set<string>>(new Set(['alle', 'faelleskab-a', 'faelleskab-b', 'faelleskab-c']));
82
+
83
+ // Computed signal to check if any selection has been made
84
+ hasSelectionChanged = computed(() => this.selectedOptions().size > 0);
85
+
86
+ constructor(private modalController: ModalController) {}
87
+
88
+ /**
89
+ * Toggle an option on/off
90
+ * Special logic for 'alle': when toggled on, selects all options; when toggled off, deselects all
91
+ */
92
+ toggleOption(value: string): void {
93
+ const current = new Set(this.selectedOptions());
94
+
95
+ if (value === 'alle') {
96
+ if (current.has('alle')) {
97
+ // Deselect all
98
+ current.clear();
99
+ } else {
100
+ // Select all
101
+ this.options.forEach(opt => current.add(opt.value));
102
+ }
103
+ } else {
104
+ if (current.has(value)) {
105
+ current.delete(value);
106
+ current.delete('alle'); // Uncheck "Alle" if any individual item is unchecked
107
+ } else {
108
+ current.add(value);
109
+ // Check if all non-master items are selected
110
+ const allSelected = this.options
111
+ .filter(o => !o.isMaster)
112
+ .every(o => current.has(o.value));
113
+ if (allSelected) current.add('alle');
114
+ }
115
+ }
116
+
117
+ this.selectedOptions.set(current);
118
+ }
119
+
120
+ /**
121
+ * Confirm selection and dismiss with selected options
122
+ */
123
+ confirmSelection(): void {
124
+ const selected = Array.from(this.selectedOptions());
125
+ this.modalController.dismiss({ value: selected }, 'select');
126
+ }
127
+
128
+ /**
129
+ * Dismiss without saving
130
+ */
131
+ dismiss(): void {
132
+ this.modalController.dismiss(null, 'cancel');
133
+ }
134
+ }
@@ -0,0 +1,69 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { ModalController } from '@ionic/angular/standalone';
3
+ import { BaseModalService } from '../../services/base-modal.service';
4
+ import {
5
+ DsMobileFacilityDetailModalComponent,
6
+ FacilityDetailData,
7
+ } from './ds-mobile-facility-detail-modal';
8
+
9
+ /**
10
+ * DsMobileFacilityDetailModalService
11
+ *
12
+ * Service for displaying facility details in a full-screen modal.
13
+ * Built on Ionic's modal system with native gestures and animations.
14
+ * Follows the same pattern as DsMobilePostDetailModalService for consistent behavior.
15
+ *
16
+ * Features:
17
+ * - Full facility information display
18
+ * - Hero image with swiper
19
+ * - Rich text description support
20
+ * - Requirements and booking type display
21
+ * - Restrictions list
22
+ * - Fixed bottom booking button
23
+ * - Native modal animations
24
+ * - Safe area support
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * constructor(private facilityModal: DsMobileFacilityDetailModalService) {}
29
+ *
30
+ * async openFacility() {
31
+ * await this.facilityModal.open({
32
+ * id: 'facility-1',
33
+ * facilityTitle: 'Festlokale på taget',
34
+ * heroImage: '/Assets/Dummy-photos/balcony-view.jpg',
35
+ * fullDescription: '<p>The rooftop terrace is designed...</p>',
36
+ * requirements: ['Kræver nøglekort'],
37
+ * bookingType: 'Instant booking',
38
+ * expectations: '<p>The terrace is furnished...</p>',
39
+ * restrictions: ['No smoking or vaping...']
40
+ * });
41
+ * }
42
+ * ```
43
+ */
44
+ @Injectable({
45
+ providedIn: 'root',
46
+ })
47
+ export class DsMobileFacilityDetailModalService extends BaseModalService {
48
+ constructor(modalController: ModalController) {
49
+ super(modalController);
50
+ }
51
+
52
+ /**
53
+ * Open the facility detail modal
54
+ *
55
+ * @param facilityData Facility data to display
56
+ * @returns Promise that resolves when the modal is presented
57
+ */
58
+ async open(facilityData: FacilityDetailData): Promise<void> {
59
+ const modal = await this.createModal(
60
+ DsMobileFacilityDetailModalComponent,
61
+ { facilityData },
62
+ {
63
+ keyboardClose: true, // Allow keyboard close behavior
64
+ }
65
+ );
66
+
67
+ await modal.present();
68
+ }
69
+ }