ud-components 0.5.15 → 0.5.18

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.
@@ -1,9 +1,9 @@
1
1
  import * as i1$2 from '@angular/common';
2
2
  import { CommonModule, formatDate, NgClass, AsyncPipe, NgStyle, DatePipe, NgTemplateOutlet } from '@angular/common';
3
3
  import * as i0 from '@angular/core';
4
- import { ViewChild, Input, CUSTOM_ELEMENTS_SCHEMA, Component, Inject, Pipe, inject, EventEmitter, forwardRef, Output, HostBinding, Injectable, DestroyRef, TemplateRef, ContentChild, ViewContainerRef, Directive, ContentChildren, Optional, input, effect, ViewChildren, computed } from '@angular/core';
4
+ import { ViewChild, Input, CUSTOM_ELEMENTS_SCHEMA, Component, Inject, Pipe, inject, EventEmitter, forwardRef, Output, HostBinding, Injectable, DestroyRef, TemplateRef, ContentChild, ViewContainerRef, Directive, ContentChildren, Optional, input, effect, ViewChildren, output, signal, ElementRef, computed, HostListener } from '@angular/core';
5
5
  import * as i1 from '@angular/forms';
6
- import { FormsModule, ControlContainer, ReactiveFormsModule, NG_VALUE_ACCESSOR, FormGroup, FormControl, FormGroupDirective } from '@angular/forms';
6
+ import { FormsModule, ControlContainer, ReactiveFormsModule, NG_VALUE_ACCESSOR, FormGroup, FormControl, FormGroupDirective, Validators } from '@angular/forms';
7
7
  import * as i1$1 from '@angular/material/snack-bar';
8
8
  import { MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar';
9
9
  import { interval, isObservable, of, Subject, startWith, takeUntil, map, debounceTime, distinctUntilChanged, switchMap, filter } from 'rxjs';
@@ -38,7 +38,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
38
38
  import * as i2$6 from 'ngx-file-drop';
39
39
  import { NgxFileDropModule } from 'ngx-file-drop';
40
40
  import * as i1$4 from '@angular/material/dialog';
41
- import { MAT_DIALOG_DATA, MatDialogContent, MatDialogConfig } from '@angular/material/dialog';
41
+ import { MAT_DIALOG_DATA, MatDialogContent, MatDialogConfig, MatDialog } from '@angular/material/dialog';
42
42
  import * as i2$3 from '@angular/material/autocomplete';
43
43
  import { MatAutocompleteModule } from '@angular/material/autocomplete';
44
44
  import * as i2$4 from '@angular/cdk/text-field';
@@ -49,6 +49,7 @@ import * as i1$5 from '@angular/platform-browser';
49
49
  import * as i1$6 from '@angular/material/stepper';
50
50
  import { MatStepperModule } from '@angular/material/stepper';
51
51
  import { MatFormField as MatFormField$1 } from '@angular/material/form-field';
52
+ import { ENTER, COMMA } from '@angular/cdk/keycodes';
52
53
  import { signalStoreFeature, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
53
54
 
54
55
  class CarouselComponent {
@@ -56,8 +57,46 @@ class CarouselComponent {
56
57
  height = 300;
57
58
  direction = 'horizontal';
58
59
  autoplay = true;
60
+ objectFit = 'cover';
59
61
  swiperRef;
62
+ lightboxBackdrop;
60
63
  activeIndex = 0;
64
+ lightboxIndex = null;
65
+ lightboxShouldFocus = false;
66
+ get lightboxOpen() {
67
+ return this.lightboxIndex !== null;
68
+ }
69
+ openLightbox(index) {
70
+ this.lightboxIndex = index;
71
+ this.lightboxShouldFocus = true;
72
+ }
73
+ ngAfterViewChecked() {
74
+ if (this.lightboxShouldFocus && this.lightboxBackdrop) {
75
+ this.lightboxShouldFocus = false;
76
+ setTimeout(() => this.lightboxBackdrop?.nativeElement.focus(), 0);
77
+ }
78
+ }
79
+ closeLightbox() {
80
+ this.lightboxIndex = null;
81
+ }
82
+ lightboxPrev() {
83
+ if (this.lightboxIndex === null)
84
+ return;
85
+ this.lightboxIndex = (this.lightboxIndex - 1 + this.pictures.length) % this.pictures.length;
86
+ }
87
+ lightboxNext() {
88
+ if (this.lightboxIndex === null)
89
+ return;
90
+ this.lightboxIndex = (this.lightboxIndex + 1) % this.pictures.length;
91
+ }
92
+ onLightboxKeydown(event) {
93
+ if (event.key === 'Escape')
94
+ this.closeLightbox();
95
+ if (event.key === 'ArrowLeft')
96
+ this.lightboxPrev();
97
+ if (event.key === 'ArrowRight')
98
+ this.lightboxNext();
99
+ }
61
100
  ngAfterViewInit() {
62
101
  const el = this.swiperRef.nativeElement;
63
102
  Object.assign(el, { autoplay: this.autoplay });
@@ -89,11 +128,11 @@ class CarouselComponent {
89
128
  this.swiperRef.nativeElement.swiper.slideTo(index);
90
129
  }
91
130
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
92
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: CarouselComponent, isStandalone: true, selector: "ud-carousel", inputs: { pictures: "pictures", height: "height", direction: "direction", autoplay: "autoplay" }, viewQueries: [{ propertyName: "swiperRef", first: true, predicate: ["swiperRef"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ud-carousel\">\n <swiper-container\n #swiperRef\n init=\"false\"\n [slidesPerView]=\"1\"\n [pagination]=\"false\"\n [direction]=\"direction\"\n [style.height.px]=\"height\">\n @for (picture of pictures; track $index) {\n <swiper-slide>\n <img [src]=\"picture\" alt=\"Slide {{ $index + 1 }}\" />\n </swiper-slide>\n }\n </swiper-container>\n\n <button class=\"ud-nav ud-nav-prev\" (click)=\"prev()\" aria-label=\"Previous slide\">\n <span class=\"material-icons-outlined\">chevron_left</span>\n </button>\n <button class=\"ud-nav ud-nav-next\" (click)=\"next()\" aria-label=\"Next slide\">\n <span class=\"material-icons-outlined\">chevron_right</span>\n </button>\n\n <div class=\"ud-pagination\">\n @for (picture of pictures; track $index) {\n <button\n class=\"ud-dot\"\n [class.active]=\"activeIndex === $index\"\n (click)=\"goTo($index)\"\n [attr.aria-label]=\"'Go to slide ' + ($index + 1)\">\n </button>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%}.ud-carousel{position:relative;width:100%;border-radius:12px;overflow:hidden}.ud-carousel:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:80px;background:linear-gradient(to top,rgba(15,20,30,.6) 0%,transparent 100%);border-radius:0 0 12px 12px;pointer-events:none;z-index:5}swiper-container{width:100%;border-radius:12px}swiper-slide{display:flex;justify-content:center;align-items:center;background:#1b2535;overflow:hidden}swiper-slide img{display:block;width:100%;height:100%;object-fit:cover}.ud-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;border:none;background:#0f141e73;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);color:#fff;cursor:pointer;opacity:.8;transition:background .2s ease,opacity .2s ease,transform .2s ease}.ud-nav span{font-size:22px;line-height:1;display:flex}.ud-nav:hover{background:#0f141ed1;opacity:1;transform:translateY(-50%) scale(1.08)}.ud-nav.ud-nav-prev{left:12px}.ud-nav.ud-nav-next{right:12px}.ud-pagination{position:absolute;bottom:14px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;z-index:10}.ud-dot{display:block;height:8px;width:8px;border-radius:100px;border:none;padding:0;background:#ffffff73;cursor:pointer;transition:width .3s cubic-bezier(.34,1.56,.64,1),background .25s ease}.ud-dot.active{width:28px;background:#fff}.ud-dot:hover:not(.active){background:#ffffffbf}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
131
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: CarouselComponent, isStandalone: true, selector: "ud-carousel", inputs: { pictures: "pictures", height: "height", direction: "direction", autoplay: "autoplay", objectFit: "objectFit" }, viewQueries: [{ propertyName: "swiperRef", first: true, predicate: ["swiperRef"], descendants: true }, { propertyName: "lightboxBackdrop", first: true, predicate: ["lightboxBackdrop"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"ud-carousel\">\n <swiper-container\n #swiperRef\n init=\"false\"\n [slidesPerView]=\"1\"\n [pagination]=\"false\"\n [direction]=\"direction\"\n [style.height.px]=\"height\">\n @for (picture of pictures; track $index) {\n <swiper-slide>\n <img\n [src]=\"picture\"\n alt=\"Slide {{ $index + 1 }}\"\n [style.object-fit]=\"objectFit\"\n (click)=\"openLightbox($index)\"\n class=\"ud-slide-img\" />\n </swiper-slide>\n }\n </swiper-container>\n\n <button class=\"ud-nav ud-nav-prev\" (click)=\"prev()\" aria-label=\"Previous slide\">\n <span class=\"material-icons-outlined\">chevron_left</span>\n </button>\n <button class=\"ud-nav ud-nav-next\" (click)=\"next()\" aria-label=\"Next slide\">\n <span class=\"material-icons-outlined\">chevron_right</span>\n </button>\n\n <div class=\"ud-pagination\">\n @for (picture of pictures; track $index) {\n <button\n class=\"ud-dot\"\n [class.active]=\"activeIndex === $index\"\n (click)=\"goTo($index)\"\n [attr.aria-label]=\"'Go to slide ' + ($index + 1)\">\n </button>\n }\n </div>\n</div>\n\n@if (lightboxOpen) {\n <div\n #lightboxBackdrop\n class=\"ud-lightbox-backdrop\"\n (click)=\"closeLightbox()\"\n (keydown)=\"onLightboxKeydown($event)\"\n tabindex=\"0\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Image fullscreen view\">\n\n <button class=\"ud-lightbox-close\" (click)=\"closeLightbox()\" aria-label=\"Close\">\n <span class=\"material-icons-outlined\">close</span>\n </button>\n\n <button\n class=\"ud-lightbox-nav ud-lightbox-prev\"\n (click)=\"$event.stopPropagation(); lightboxPrev()\"\n aria-label=\"Previous image\">\n <span class=\"material-icons-outlined\">chevron_left</span>\n </button>\n\n <div class=\"ud-lightbox-img-wrap\" (click)=\"$event.stopPropagation()\">\n <img\n [src]=\"pictures[lightboxIndex!]\"\n alt=\"Fullscreen image {{ lightboxIndex! + 1 }}\"\n class=\"ud-lightbox-img\" />\n </div>\n\n <button\n class=\"ud-lightbox-nav ud-lightbox-next\"\n (click)=\"$event.stopPropagation(); lightboxNext()\"\n aria-label=\"Next image\">\n <span class=\"material-icons-outlined\">chevron_right</span>\n </button>\n\n <div class=\"ud-lightbox-counter\">\n {{ lightboxIndex! + 1 }} / {{ pictures.length }}\n </div>\n </div>\n}\n", styles: [":host{display:block;width:100%}.ud-carousel{position:relative;width:100%;border-radius:12px;overflow:hidden}.ud-carousel:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:80px;background:linear-gradient(to top,rgba(15,20,30,.6) 0%,transparent 100%);border-radius:0 0 12px 12px;pointer-events:none;z-index:5}swiper-container{width:100%;border-radius:12px}swiper-slide{display:flex;justify-content:center;align-items:center;background:#1b2535;overflow:hidden}swiper-slide .ud-slide-img{display:block;width:100%;height:100%;cursor:zoom-in}.ud-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;border:none;background:#0f141e73;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);color:#fff;cursor:pointer;opacity:.8;transition:background .2s ease,opacity .2s ease,transform .2s ease}.ud-nav span{font-size:22px;line-height:1;display:flex}.ud-nav:hover{background:#0f141ed1;opacity:1;transform:translateY(-50%) scale(1.08)}.ud-nav.ud-nav-prev{left:12px}.ud-nav.ud-nav-next{right:12px}.ud-lightbox-backdrop{position:fixed;inset:0;z-index:1000;background:#000000e0;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;animation:ud-lightbox-in .2s ease;outline:none}@keyframes ud-lightbox-in{0%{opacity:0}to{opacity:1}}.ud-lightbox-img-wrap{max-width:calc(100vw - 120px);max-height:calc(100vh - 80px);display:flex;align-items:center;justify-content:center}.ud-lightbox-img{max-width:100%;max-height:calc(100vh - 80px);border-radius:10px;box-shadow:0 24px 64px #0009;object-fit:contain;display:block;animation:ud-lightbox-scale-in .22s cubic-bezier(.34,1.56,.64,1)}@keyframes ud-lightbox-scale-in{0%{transform:scale(.88);opacity:0}to{transform:scale(1);opacity:1}}.ud-lightbox-close{position:fixed;top:20px;right:20px;z-index:1001;width:44px;height:44px;border-radius:50%;border:none;background:#ffffff1f;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease,transform .2s ease}.ud-lightbox-close span{font-size:22px}.ud-lightbox-close:hover{background:#ffffff38;transform:scale(1.1)}.ud-lightbox-nav{position:fixed;top:50%;transform:translateY(-50%);z-index:1001;width:52px;height:52px;border-radius:50%;border:none;background:#ffffff1f;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease,transform .2s ease}.ud-lightbox-nav span{font-size:28px}.ud-lightbox-nav:hover{background:#ffffff38}.ud-lightbox-nav.ud-lightbox-prev{left:16px}.ud-lightbox-nav.ud-lightbox-prev:hover{transform:translateY(-50%) scale(1.08)}.ud-lightbox-nav.ud-lightbox-next{right:16px}.ud-lightbox-nav.ud-lightbox-next:hover{transform:translateY(-50%) scale(1.08)}.ud-lightbox-counter{position:fixed;bottom:20px;left:50%;transform:translate(-50%);color:#ffffffb3;font-size:14px;font-weight:500;letter-spacing:.05em;z-index:1001}.ud-pagination{position:absolute;bottom:14px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;z-index:10}.ud-dot{display:block;height:8px;width:8px;border-radius:100px;border:none;padding:0;background:#ffffff73;cursor:pointer;transition:width .3s cubic-bezier(.34,1.56,.64,1),background .25s ease}.ud-dot.active{width:28px;background:#fff}.ud-dot:hover:not(.active){background:#ffffffbf}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
93
132
  }
94
133
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: CarouselComponent, decorators: [{
95
134
  type: Component,
96
- args: [{ selector: 'ud-carousel', imports: [CommonModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<div class=\"ud-carousel\">\n <swiper-container\n #swiperRef\n init=\"false\"\n [slidesPerView]=\"1\"\n [pagination]=\"false\"\n [direction]=\"direction\"\n [style.height.px]=\"height\">\n @for (picture of pictures; track $index) {\n <swiper-slide>\n <img [src]=\"picture\" alt=\"Slide {{ $index + 1 }}\" />\n </swiper-slide>\n }\n </swiper-container>\n\n <button class=\"ud-nav ud-nav-prev\" (click)=\"prev()\" aria-label=\"Previous slide\">\n <span class=\"material-icons-outlined\">chevron_left</span>\n </button>\n <button class=\"ud-nav ud-nav-next\" (click)=\"next()\" aria-label=\"Next slide\">\n <span class=\"material-icons-outlined\">chevron_right</span>\n </button>\n\n <div class=\"ud-pagination\">\n @for (picture of pictures; track $index) {\n <button\n class=\"ud-dot\"\n [class.active]=\"activeIndex === $index\"\n (click)=\"goTo($index)\"\n [attr.aria-label]=\"'Go to slide ' + ($index + 1)\">\n </button>\n }\n </div>\n</div>\n", styles: [":host{display:block;width:100%}.ud-carousel{position:relative;width:100%;border-radius:12px;overflow:hidden}.ud-carousel:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:80px;background:linear-gradient(to top,rgba(15,20,30,.6) 0%,transparent 100%);border-radius:0 0 12px 12px;pointer-events:none;z-index:5}swiper-container{width:100%;border-radius:12px}swiper-slide{display:flex;justify-content:center;align-items:center;background:#1b2535;overflow:hidden}swiper-slide img{display:block;width:100%;height:100%;object-fit:cover}.ud-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;border:none;background:#0f141e73;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);color:#fff;cursor:pointer;opacity:.8;transition:background .2s ease,opacity .2s ease,transform .2s ease}.ud-nav span{font-size:22px;line-height:1;display:flex}.ud-nav:hover{background:#0f141ed1;opacity:1;transform:translateY(-50%) scale(1.08)}.ud-nav.ud-nav-prev{left:12px}.ud-nav.ud-nav-next{right:12px}.ud-pagination{position:absolute;bottom:14px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;z-index:10}.ud-dot{display:block;height:8px;width:8px;border-radius:100px;border:none;padding:0;background:#ffffff73;cursor:pointer;transition:width .3s cubic-bezier(.34,1.56,.64,1),background .25s ease}.ud-dot.active{width:28px;background:#fff}.ud-dot:hover:not(.active){background:#ffffffbf}\n"] }]
135
+ args: [{ selector: 'ud-carousel', imports: [CommonModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<div class=\"ud-carousel\">\n <swiper-container\n #swiperRef\n init=\"false\"\n [slidesPerView]=\"1\"\n [pagination]=\"false\"\n [direction]=\"direction\"\n [style.height.px]=\"height\">\n @for (picture of pictures; track $index) {\n <swiper-slide>\n <img\n [src]=\"picture\"\n alt=\"Slide {{ $index + 1 }}\"\n [style.object-fit]=\"objectFit\"\n (click)=\"openLightbox($index)\"\n class=\"ud-slide-img\" />\n </swiper-slide>\n }\n </swiper-container>\n\n <button class=\"ud-nav ud-nav-prev\" (click)=\"prev()\" aria-label=\"Previous slide\">\n <span class=\"material-icons-outlined\">chevron_left</span>\n </button>\n <button class=\"ud-nav ud-nav-next\" (click)=\"next()\" aria-label=\"Next slide\">\n <span class=\"material-icons-outlined\">chevron_right</span>\n </button>\n\n <div class=\"ud-pagination\">\n @for (picture of pictures; track $index) {\n <button\n class=\"ud-dot\"\n [class.active]=\"activeIndex === $index\"\n (click)=\"goTo($index)\"\n [attr.aria-label]=\"'Go to slide ' + ($index + 1)\">\n </button>\n }\n </div>\n</div>\n\n@if (lightboxOpen) {\n <div\n #lightboxBackdrop\n class=\"ud-lightbox-backdrop\"\n (click)=\"closeLightbox()\"\n (keydown)=\"onLightboxKeydown($event)\"\n tabindex=\"0\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Image fullscreen view\">\n\n <button class=\"ud-lightbox-close\" (click)=\"closeLightbox()\" aria-label=\"Close\">\n <span class=\"material-icons-outlined\">close</span>\n </button>\n\n <button\n class=\"ud-lightbox-nav ud-lightbox-prev\"\n (click)=\"$event.stopPropagation(); lightboxPrev()\"\n aria-label=\"Previous image\">\n <span class=\"material-icons-outlined\">chevron_left</span>\n </button>\n\n <div class=\"ud-lightbox-img-wrap\" (click)=\"$event.stopPropagation()\">\n <img\n [src]=\"pictures[lightboxIndex!]\"\n alt=\"Fullscreen image {{ lightboxIndex! + 1 }}\"\n class=\"ud-lightbox-img\" />\n </div>\n\n <button\n class=\"ud-lightbox-nav ud-lightbox-next\"\n (click)=\"$event.stopPropagation(); lightboxNext()\"\n aria-label=\"Next image\">\n <span class=\"material-icons-outlined\">chevron_right</span>\n </button>\n\n <div class=\"ud-lightbox-counter\">\n {{ lightboxIndex! + 1 }} / {{ pictures.length }}\n </div>\n </div>\n}\n", styles: [":host{display:block;width:100%}.ud-carousel{position:relative;width:100%;border-radius:12px;overflow:hidden}.ud-carousel:after{content:\"\";position:absolute;bottom:0;left:0;right:0;height:80px;background:linear-gradient(to top,rgba(15,20,30,.6) 0%,transparent 100%);border-radius:0 0 12px 12px;pointer-events:none;z-index:5}swiper-container{width:100%;border-radius:12px}swiper-slide{display:flex;justify-content:center;align-items:center;background:#1b2535;overflow:hidden}swiper-slide .ud-slide-img{display:block;width:100%;height:100%;cursor:zoom-in}.ud-nav{position:absolute;top:50%;transform:translateY(-50%);z-index:10;display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:50%;border:none;background:#0f141e73;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);color:#fff;cursor:pointer;opacity:.8;transition:background .2s ease,opacity .2s ease,transform .2s ease}.ud-nav span{font-size:22px;line-height:1;display:flex}.ud-nav:hover{background:#0f141ed1;opacity:1;transform:translateY(-50%) scale(1.08)}.ud-nav.ud-nav-prev{left:12px}.ud-nav.ud-nav-next{right:12px}.ud-lightbox-backdrop{position:fixed;inset:0;z-index:1000;background:#000000e0;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;animation:ud-lightbox-in .2s ease;outline:none}@keyframes ud-lightbox-in{0%{opacity:0}to{opacity:1}}.ud-lightbox-img-wrap{max-width:calc(100vw - 120px);max-height:calc(100vh - 80px);display:flex;align-items:center;justify-content:center}.ud-lightbox-img{max-width:100%;max-height:calc(100vh - 80px);border-radius:10px;box-shadow:0 24px 64px #0009;object-fit:contain;display:block;animation:ud-lightbox-scale-in .22s cubic-bezier(.34,1.56,.64,1)}@keyframes ud-lightbox-scale-in{0%{transform:scale(.88);opacity:0}to{transform:scale(1);opacity:1}}.ud-lightbox-close{position:fixed;top:20px;right:20px;z-index:1001;width:44px;height:44px;border-radius:50%;border:none;background:#ffffff1f;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease,transform .2s ease}.ud-lightbox-close span{font-size:22px}.ud-lightbox-close:hover{background:#ffffff38;transform:scale(1.1)}.ud-lightbox-nav{position:fixed;top:50%;transform:translateY(-50%);z-index:1001;width:52px;height:52px;border-radius:50%;border:none;background:#ffffff1f;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .2s ease,transform .2s ease}.ud-lightbox-nav span{font-size:28px}.ud-lightbox-nav:hover{background:#ffffff38}.ud-lightbox-nav.ud-lightbox-prev{left:16px}.ud-lightbox-nav.ud-lightbox-prev:hover{transform:translateY(-50%) scale(1.08)}.ud-lightbox-nav.ud-lightbox-next{right:16px}.ud-lightbox-nav.ud-lightbox-next:hover{transform:translateY(-50%) scale(1.08)}.ud-lightbox-counter{position:fixed;bottom:20px;left:50%;transform:translate(-50%);color:#ffffffb3;font-size:14px;font-weight:500;letter-spacing:.05em;z-index:1001}.ud-pagination{position:absolute;bottom:14px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:6px;z-index:10}.ud-dot{display:block;height:8px;width:8px;border-radius:100px;border:none;padding:0;background:#ffffff73;cursor:pointer;transition:width .3s cubic-bezier(.34,1.56,.64,1),background .25s ease}.ud-dot.active{width:28px;background:#fff}.ud-dot:hover:not(.active){background:#ffffffbf}\n"] }]
97
136
  }], propDecorators: { pictures: [{
98
137
  type: Input
99
138
  }], height: [{
@@ -102,9 +141,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
102
141
  type: Input
103
142
  }], autoplay: [{
104
143
  type: Input
144
+ }], objectFit: [{
145
+ type: Input
105
146
  }], swiperRef: [{
106
147
  type: ViewChild,
107
148
  args: ['swiperRef']
149
+ }], lightboxBackdrop: [{
150
+ type: ViewChild,
151
+ args: ['lightboxBackdrop']
108
152
  }] } });
109
153
 
110
154
  class CustomInputComponent {
@@ -2233,56 +2277,58 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
2233
2277
  type: Input
2234
2278
  }] } });
2235
2279
 
2236
- /**
2237
- * Styled time picker that participates in the parent FormGroup.
2238
- *
2239
- * Usage (interval mode — value is in minutes):
2240
- * <ud-time-picker
2241
- * controlName="checkInTime"
2242
- * label="Check-in time"
2243
- * [intervalMinutes]="30" />
2244
- *
2245
- * Usage (custom options mode):
2246
- * <ud-time-picker
2247
- * controlName="checkInTime"
2248
- * label="Check-in time"
2249
- * [options]="timeOptions" />
2250
- */
2251
2280
  class TimePickerComponent {
2252
2281
  controlName;
2253
2282
  label = '';
2254
2283
  placeholder = '';
2255
- icon = 'schedule';
2256
- iconFontSet = 'material-icons-outlined';
2257
- /**
2258
- * Minutes between each option in the dropdown (e.g. 30 → every half hour).
2259
- * Omit to use Material's default (30 min). Ignored when `options` is provided.
2260
- * Internally converted to seconds for mat-timepicker.
2261
- */
2262
2284
  intervalMinutes;
2263
- /** Pre-defined list of selectable times. When set, `intervalMinutes` is ignored. */
2264
2285
  options;
2265
2286
  disabled = false;
2266
2287
  hint = '';
2267
2288
  focused = false;
2289
+ matOptions = [];
2268
2290
  controlContainer = inject(ControlContainer);
2269
2291
  get control() {
2270
2292
  return this.controlContainer.control.get(this.controlName);
2271
2293
  }
2294
+ ngOnInit() {
2295
+ this.matOptions = this.buildOptions();
2296
+ }
2272
2297
  ngOnChanges(changes) {
2273
2298
  if (changes['disabled']) {
2274
2299
  this.disabled ? this.control.disable() : this.control.enable();
2275
2300
  }
2301
+ if (changes['intervalMinutes'] || changes['options']) {
2302
+ this.matOptions = this.buildOptions();
2303
+ }
2304
+ }
2305
+ buildOptions() {
2306
+ if (this.options?.length) {
2307
+ return this.options.map(o => ({
2308
+ value: o.value,
2309
+ label: o.label ?? this.formatTime(o.value),
2310
+ }));
2311
+ }
2312
+ const interval = this.intervalMinutes ?? 30;
2313
+ const result = [];
2314
+ for (let h = 0; h < 24; h++) {
2315
+ for (let m = 0; m < 60; m += interval) {
2316
+ const d = new Date();
2317
+ d.setHours(h, m, 0, 0);
2318
+ result.push({ value: new Date(d), label: this.formatTime(d) });
2319
+ }
2320
+ }
2321
+ return result;
2276
2322
  }
2277
- get _intervalSeconds() {
2278
- return this.intervalMinutes != null ? this.intervalMinutes * 60 : null;
2323
+ formatTime(d) {
2324
+ return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
2279
2325
  }
2280
2326
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: TimePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2281
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: TimePickerComponent, isStandalone: true, selector: "ud-time-picker", inputs: { controlName: "controlName", label: "label", placeholder: "placeholder", icon: "icon", iconFontSet: "iconFontSet", intervalMinutes: "intervalMinutes", options: "options", disabled: "disabled", hint: "hint" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ud-input\"\n [class.ud-input--focused]=\"focused\"\n [class.ud-input--disabled]=\"control.disabled\">\n @if (label) {\n <label class=\"ud-input__label\" [for]=\"'ud-input-' + controlName\">{{ label }}</label>\n }\n <div class=\"ud-input__wrapper\">\n @if (icon) {\n <mat-icon class=\"ud-input__icon\" [fontSet]=\"iconFontSet\">{{ icon }}</mat-icon>\n }\n @if (options && options.length) {\n <input\n class=\"ud-input__field\"\n [id]=\"'ud-input-' + controlName\"\n [formControlName]=\"controlName\"\n [placeholder]=\"placeholder\"\n [matTimepicker]=\"optionsPicker\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n <mat-timepicker-toggle class=\"ud-timepicker-toggle\" [for]=\"optionsPicker\">\n <mat-icon matTimepickerToggleIcon fontSet=\"material-icons-outlined\">schedule</mat-icon>\n </mat-timepicker-toggle>\n <mat-timepicker #optionsPicker [options]=\"options\" />\n } @else {\n <input\n class=\"ud-input__field\"\n [id]=\"'ud-input-' + controlName\"\n [formControlName]=\"controlName\"\n [placeholder]=\"placeholder\"\n [matTimepicker]=\"intervalPicker\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n <mat-timepicker-toggle class=\"ud-timepicker-toggle\" [for]=\"intervalPicker\">\n <mat-icon matTimepickerToggleIcon fontSet=\"material-icons-outlined\">schedule</mat-icon>\n </mat-timepicker-toggle>\n <mat-timepicker #intervalPicker [interval]=\"_intervalSeconds\" />\n }\n </div>\n @if (hint) {\n <span class=\"ud-input__hint\">{{ hint }}</span>\n }\n</div>\n", styles: [":host{display:block;width:100%}.ud-input{display:flex;flex-direction:column;gap:5px;width:100%}.ud-input__label{font-family:DM Sans,system-ui,sans-serif;font-size:13px;font-weight:500;color:#2a3548;line-height:1;padding-left:1px}.ud-input__wrapper{display:flex;align-items:center;background:#f8fafc;border:1px solid #d8dde6;border-radius:8px;padding:0 12px;gap:7px;overflow:hidden;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}.ud-input--focused .ud-input__wrapper{border-color:#1b2535;box-shadow:0 0 0 3px #1b253514;background:#fff}.ud-input--error .ud-input__wrapper{border-color:#e53935;box-shadow:0 0 0 3px #e539351a}.ud-input--error.ud-input--focused .ud-input__wrapper{border-color:#e53935}.ud-input--disabled .ud-input__wrapper{background:#f4f5f7;border-color:#e8eaef;cursor:not-allowed;opacity:.6}.ud-input__icon{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585;transition:color .18s ease;line-height:1}.ud-input--focused .ud-input__icon{color:#1b2535}.ud-input__field{flex:1;min-width:0;border:none;background:transparent;outline:none;font-family:DM Sans,system-ui,sans-serif;font-size:14px;color:#2a3548;width:100%}.ud-input__field::placeholder{color:#9099a8}.ud-input__field:disabled{cursor:not-allowed;color:#9099a8}.ud-input__hint{font-family:DM Sans,system-ui,sans-serif;font-size:12px;color:#6b7585;line-height:1.3}.ud-input__suffix{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585}.ud-input__loading{animation:ud-fw-spin .8s linear infinite}@keyframes ud-fw-spin{to{transform:rotate(360deg)}}.ud-timepicker-toggle{flex-shrink:0;margin-right:-4px}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base{width:32px;height:32px;padding:4px;color:#6b7585;transition:color .18s ease}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base:hover{color:#1b2535}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTimepickerModule }, { kind: "component", type: i2$5.MatTimepicker, selector: "mat-timepicker", inputs: ["interval", "options", "disableRipple", "aria-label", "aria-labelledby"], outputs: ["selected", "opened", "closed"], exportAs: ["matTimepicker"] }, { kind: "directive", type: i2$5.MatTimepickerInput, selector: "input[matTimepicker]", inputs: ["value", "matTimepicker", "matTimepickerMin", "matTimepickerMax", "disabled"], outputs: ["valueChange"], exportAs: ["matTimepickerInput"] }, { kind: "component", type: i2$5.MatTimepickerToggle, selector: "mat-timepicker-toggle", inputs: ["for", "aria-label", "aria-labelledby", "disabled", "tabIndex", "disableRipple"], exportAs: ["matTimepickerToggle"] }], viewProviders: [{ provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) }] });
2327
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: TimePickerComponent, isStandalone: true, selector: "ud-time-picker", inputs: { controlName: "controlName", label: "label", placeholder: "placeholder", intervalMinutes: "intervalMinutes", options: "options", disabled: "disabled", hint: "hint" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ud-input\"\n [class.ud-input--focused]=\"focused\"\n [class.ud-input--disabled]=\"control.disabled\"\n [class.ud-input--error]=\"control.invalid && control.touched\">\n @if (label) {\n <label class=\"ud-input__label\" [for]=\"'ud-input-' + controlName\">{{ label }}</label>\n }\n <div class=\"ud-input__wrapper\">\n <mat-icon class=\"ud-input__icon\" fontSet=\"material-icons-outlined\">schedule</mat-icon>\n <input\n class=\"ud-input__field\"\n [id]=\"'ud-input-' + controlName\"\n [formControlName]=\"controlName\"\n [matTimepicker]=\"picker\"\n [placeholder]=\"placeholder\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n <mat-timepicker-toggle class=\"ud-timepicker-toggle\" [for]=\"picker\">\n <mat-icon matTimepickerToggleIcon fontSet=\"material-icons-outlined\">expand_more</mat-icon>\n </mat-timepicker-toggle>\n <mat-timepicker #picker [options]=\"matOptions\" />\n </div>\n @if (hint) {\n <span class=\"ud-input__hint\">{{ hint }}</span>\n }\n</div>\n", styles: [":host{display:block;width:100%}.ud-input{display:flex;flex-direction:column;gap:5px;width:100%}.ud-input__label{font-family:DM Sans,system-ui,sans-serif;font-size:13px;font-weight:500;color:#2a3548;line-height:1;padding-left:1px}.ud-input__wrapper{display:flex;align-items:center;background:#f8fafc;border:1px solid #d8dde6;border-radius:8px;padding:0 12px;gap:7px;overflow:hidden;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}.ud-input--focused .ud-input__wrapper{border-color:#1b2535;box-shadow:0 0 0 3px #1b253514;background:#fff}.ud-input--error .ud-input__wrapper{border-color:#e53935;box-shadow:0 0 0 3px #e539351a}.ud-input--error.ud-input--focused .ud-input__wrapper{border-color:#e53935}.ud-input--disabled .ud-input__wrapper{background:#f4f5f7;border-color:#e8eaef;cursor:not-allowed;opacity:.6}.ud-input__icon{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585;transition:color .18s ease;line-height:1}.ud-input--focused .ud-input__icon{color:#1b2535}.ud-input__field{flex:1;min-width:0;border:none;background:transparent;outline:none;font-family:DM Sans,system-ui,sans-serif;font-size:14px;color:#2a3548;width:100%}.ud-input__field::placeholder{color:#9099a8}.ud-input__field:disabled{cursor:not-allowed;color:#9099a8}.ud-input__hint{font-family:DM Sans,system-ui,sans-serif;font-size:12px;color:#6b7585;line-height:1.3}.ud-input__suffix{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585}.ud-input__loading{animation:ud-fw-spin .8s linear infinite}@keyframes ud-fw-spin{to{transform:rotate(360deg)}}.ud-timepicker-toggle{flex-shrink:0;margin-right:-4px}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base{width:32px;height:32px;padding:4px;color:#6b7585;transition:color .18s ease}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base:hover{color:#1b2535}::ng-deep .mat-timepicker-panel{background:#fff!important;border-radius:10px!important;box-shadow:0 8px 24px #1b25351f,0 2px 8px #1b25350f!important;padding:6px 0!important;max-height:280px!important}::ng-deep .mat-timepicker-panel .mat-mdc-option{font-family:DM Sans,system-ui,sans-serif!important;font-size:.9rem!important;color:#1b2535!important;min-height:44px!important;padding:0 1.25rem!important;transition:background .12s ease}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background:#f4f5f7!important}::ng-deep .mat-timepicker-panel .mat-mdc-option.mdc-list-item--selected{background:#1b25350d!important}::ng-deep .mat-timepicker-panel .mat-mdc-option.mdc-list-item--selected .mdc-list-item__primary-text{color:#1b2535!important;font-weight:600!important}::ng-deep .mat-timepicker-panel .mat-mdc-option .mdc-list-item__primary-text{font-family:DM Sans,system-ui,sans-serif!important;font-size:.9rem!important;color:#1b2535!important}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTimepickerModule }, { kind: "component", type: i2$5.MatTimepicker, selector: "mat-timepicker", inputs: ["interval", "options", "disableRipple", "aria-label", "aria-labelledby"], outputs: ["selected", "opened", "closed"], exportAs: ["matTimepicker"] }, { kind: "directive", type: i2$5.MatTimepickerInput, selector: "input[matTimepicker]", inputs: ["value", "matTimepicker", "matTimepickerMin", "matTimepickerMax", "disabled"], outputs: ["valueChange"], exportAs: ["matTimepickerInput"] }, { kind: "component", type: i2$5.MatTimepickerToggle, selector: "mat-timepicker-toggle", inputs: ["for", "aria-label", "aria-labelledby", "disabled", "tabIndex", "disableRipple"], exportAs: ["matTimepickerToggle"] }], viewProviders: [{ provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) }] });
2282
2328
  }
2283
2329
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: TimePickerComponent, decorators: [{
2284
2330
  type: Component,
2285
- args: [{ selector: 'ud-time-picker', standalone: true, imports: [CommonModule, ReactiveFormsModule, MatIcon, MatTimepickerModule], viewProviders: [{ provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) }], template: "<div\n class=\"ud-input\"\n [class.ud-input--focused]=\"focused\"\n [class.ud-input--disabled]=\"control.disabled\">\n @if (label) {\n <label class=\"ud-input__label\" [for]=\"'ud-input-' + controlName\">{{ label }}</label>\n }\n <div class=\"ud-input__wrapper\">\n @if (icon) {\n <mat-icon class=\"ud-input__icon\" [fontSet]=\"iconFontSet\">{{ icon }}</mat-icon>\n }\n @if (options && options.length) {\n <input\n class=\"ud-input__field\"\n [id]=\"'ud-input-' + controlName\"\n [formControlName]=\"controlName\"\n [placeholder]=\"placeholder\"\n [matTimepicker]=\"optionsPicker\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n <mat-timepicker-toggle class=\"ud-timepicker-toggle\" [for]=\"optionsPicker\">\n <mat-icon matTimepickerToggleIcon fontSet=\"material-icons-outlined\">schedule</mat-icon>\n </mat-timepicker-toggle>\n <mat-timepicker #optionsPicker [options]=\"options\" />\n } @else {\n <input\n class=\"ud-input__field\"\n [id]=\"'ud-input-' + controlName\"\n [formControlName]=\"controlName\"\n [placeholder]=\"placeholder\"\n [matTimepicker]=\"intervalPicker\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n <mat-timepicker-toggle class=\"ud-timepicker-toggle\" [for]=\"intervalPicker\">\n <mat-icon matTimepickerToggleIcon fontSet=\"material-icons-outlined\">schedule</mat-icon>\n </mat-timepicker-toggle>\n <mat-timepicker #intervalPicker [interval]=\"_intervalSeconds\" />\n }\n </div>\n @if (hint) {\n <span class=\"ud-input__hint\">{{ hint }}</span>\n }\n</div>\n", styles: [":host{display:block;width:100%}.ud-input{display:flex;flex-direction:column;gap:5px;width:100%}.ud-input__label{font-family:DM Sans,system-ui,sans-serif;font-size:13px;font-weight:500;color:#2a3548;line-height:1;padding-left:1px}.ud-input__wrapper{display:flex;align-items:center;background:#f8fafc;border:1px solid #d8dde6;border-radius:8px;padding:0 12px;gap:7px;overflow:hidden;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}.ud-input--focused .ud-input__wrapper{border-color:#1b2535;box-shadow:0 0 0 3px #1b253514;background:#fff}.ud-input--error .ud-input__wrapper{border-color:#e53935;box-shadow:0 0 0 3px #e539351a}.ud-input--error.ud-input--focused .ud-input__wrapper{border-color:#e53935}.ud-input--disabled .ud-input__wrapper{background:#f4f5f7;border-color:#e8eaef;cursor:not-allowed;opacity:.6}.ud-input__icon{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585;transition:color .18s ease;line-height:1}.ud-input--focused .ud-input__icon{color:#1b2535}.ud-input__field{flex:1;min-width:0;border:none;background:transparent;outline:none;font-family:DM Sans,system-ui,sans-serif;font-size:14px;color:#2a3548;width:100%}.ud-input__field::placeholder{color:#9099a8}.ud-input__field:disabled{cursor:not-allowed;color:#9099a8}.ud-input__hint{font-family:DM Sans,system-ui,sans-serif;font-size:12px;color:#6b7585;line-height:1.3}.ud-input__suffix{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585}.ud-input__loading{animation:ud-fw-spin .8s linear infinite}@keyframes ud-fw-spin{to{transform:rotate(360deg)}}.ud-timepicker-toggle{flex-shrink:0;margin-right:-4px}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base{width:32px;height:32px;padding:4px;color:#6b7585;transition:color .18s ease}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base:hover{color:#1b2535}\n"] }]
2331
+ args: [{ selector: 'ud-time-picker', standalone: true, imports: [ReactiveFormsModule, MatIcon, MatTimepickerModule], viewProviders: [{ provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) }], template: "<div\n class=\"ud-input\"\n [class.ud-input--focused]=\"focused\"\n [class.ud-input--disabled]=\"control.disabled\"\n [class.ud-input--error]=\"control.invalid && control.touched\">\n @if (label) {\n <label class=\"ud-input__label\" [for]=\"'ud-input-' + controlName\">{{ label }}</label>\n }\n <div class=\"ud-input__wrapper\">\n <mat-icon class=\"ud-input__icon\" fontSet=\"material-icons-outlined\">schedule</mat-icon>\n <input\n class=\"ud-input__field\"\n [id]=\"'ud-input-' + controlName\"\n [formControlName]=\"controlName\"\n [matTimepicker]=\"picker\"\n [placeholder]=\"placeholder\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n <mat-timepicker-toggle class=\"ud-timepicker-toggle\" [for]=\"picker\">\n <mat-icon matTimepickerToggleIcon fontSet=\"material-icons-outlined\">expand_more</mat-icon>\n </mat-timepicker-toggle>\n <mat-timepicker #picker [options]=\"matOptions\" />\n </div>\n @if (hint) {\n <span class=\"ud-input__hint\">{{ hint }}</span>\n }\n</div>\n", styles: [":host{display:block;width:100%}.ud-input{display:flex;flex-direction:column;gap:5px;width:100%}.ud-input__label{font-family:DM Sans,system-ui,sans-serif;font-size:13px;font-weight:500;color:#2a3548;line-height:1;padding-left:1px}.ud-input__wrapper{display:flex;align-items:center;background:#f8fafc;border:1px solid #d8dde6;border-radius:8px;padding:0 12px;gap:7px;overflow:hidden;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}.ud-input--focused .ud-input__wrapper{border-color:#1b2535;box-shadow:0 0 0 3px #1b253514;background:#fff}.ud-input--error .ud-input__wrapper{border-color:#e53935;box-shadow:0 0 0 3px #e539351a}.ud-input--error.ud-input--focused .ud-input__wrapper{border-color:#e53935}.ud-input--disabled .ud-input__wrapper{background:#f4f5f7;border-color:#e8eaef;cursor:not-allowed;opacity:.6}.ud-input__icon{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585;transition:color .18s ease;line-height:1}.ud-input--focused .ud-input__icon{color:#1b2535}.ud-input__field{flex:1;min-width:0;border:none;background:transparent;outline:none;font-family:DM Sans,system-ui,sans-serif;font-size:14px;color:#2a3548;width:100%}.ud-input__field::placeholder{color:#9099a8}.ud-input__field:disabled{cursor:not-allowed;color:#9099a8}.ud-input__hint{font-family:DM Sans,system-ui,sans-serif;font-size:12px;color:#6b7585;line-height:1.3}.ud-input__suffix{flex-shrink:0;font-size:18px;width:18px;height:18px;color:#6b7585}.ud-input__loading{animation:ud-fw-spin .8s linear infinite}@keyframes ud-fw-spin{to{transform:rotate(360deg)}}.ud-timepicker-toggle{flex-shrink:0;margin-right:-4px}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base{width:32px;height:32px;padding:4px;color:#6b7585;transition:color .18s ease}.ud-timepicker-toggle ::ng-deep .mat-mdc-icon-button.mat-mdc-button-base:hover{color:#1b2535}::ng-deep .mat-timepicker-panel{background:#fff!important;border-radius:10px!important;box-shadow:0 8px 24px #1b25351f,0 2px 8px #1b25350f!important;padding:6px 0!important;max-height:280px!important}::ng-deep .mat-timepicker-panel .mat-mdc-option{font-family:DM Sans,system-ui,sans-serif!important;font-size:.9rem!important;color:#1b2535!important;min-height:44px!important;padding:0 1.25rem!important;transition:background .12s ease}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background:#f4f5f7!important}::ng-deep .mat-timepicker-panel .mat-mdc-option.mdc-list-item--selected{background:#1b25350d!important}::ng-deep .mat-timepicker-panel .mat-mdc-option.mdc-list-item--selected .mdc-list-item__primary-text{color:#1b2535!important;font-weight:600!important}::ng-deep .mat-timepicker-panel .mat-mdc-option .mdc-list-item__primary-text{font-family:DM Sans,system-ui,sans-serif!important;font-size:.9rem!important;color:#1b2535!important}\n"] }]
2286
2332
  }], propDecorators: { controlName: [{
2287
2333
  type: Input,
2288
2334
  args: [{ required: true }]
@@ -2290,10 +2336,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
2290
2336
  type: Input
2291
2337
  }], placeholder: [{
2292
2338
  type: Input
2293
- }], icon: [{
2294
- type: Input
2295
- }], iconFontSet: [{
2296
- type: Input
2297
2339
  }], intervalMinutes: [{
2298
2340
  type: Input
2299
2341
  }], options: [{
@@ -2469,6 +2511,10 @@ class ModalComponent {
2469
2511
  if (this.data.showClose != undefined)
2470
2512
  this.showClose = this.data.showClose;
2471
2513
  }
2514
+ onDelete() {
2515
+ this.data.delete?.();
2516
+ this.dialogRef?.close();
2517
+ }
2472
2518
  close() {
2473
2519
  // If the caller is listening on (cancel) they own the close lifecycle —
2474
2520
  // emit and let them decide. Otherwise fall back to auto-closing the
@@ -2506,7 +2552,7 @@ class ModalComponent {
2506
2552
  }
2507
2553
  }
2508
2554
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ModalComponent, deps: [{ token: MAT_DIALOG_DATA, optional: true }, { token: i1$4.MatDialogRef, optional: true }], target: i0.ɵɵFactoryTarget.Component });
2509
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: ModalComponent, isStandalone: true, selector: "ud-modal", inputs: { title: "title", eyebrow: "eyebrow", lede: "lede", bodyText: "bodyText", showClose: "showClose", showFooter: "showFooter", confirmLabel: "confirmLabel", cancelLabel: "cancelLabel", confirmDisabled: "confirmDisabled" }, outputs: { confirm: "confirm", cancel: "cancel" }, ngImport: i0, template: "<div class=\"ud-modal-shell\">\n <header class=\"ud-modal-header\">\n @if (eyebrow) {\n <p class=\"ud-modal-eyebrow\">{{ eyebrow | translate | capitalize }}</p>\n }\n @if (title) {\n <h3 class=\"ud-modal-title\">{{ title | translate | capitalize }}</h3>\n }\n @if (lede) {\n <p class=\"ud-modal-lede\">{{ lede | translate | capitalize }}</p>\n }\n </header>\n\n <div class=\"ud-modal-content\" mat-dialog-content>\n @if (pictureUrls && currentPictureIndex != undefined) {\n <div class=\"image-container\">\n <mat-icon class=\"arrow-picture-icon\" (click)=\"previous()\">keyboard_arrow_left</mat-icon>\n <img [src]=\"pictureUrls[currentPictureIndex]\" alt=\"Picture\" />\n <mat-icon class=\"arrow-picture-icon\" (click)=\"next()\">keyboard_arrow_right</mat-icon>\n </div>\n <div class=\"image-footer\">\n {{ currentPictureIndex + 1 }} {{ 'names.of' | translate }} {{ pictureUrls.length }}\n </div>\n } @else if (form) {\n <form [formGroup]=\"form\">\n <div class=\"modal-form-fields\">\n @for (formType of data.forms; track formType) {\n @switch (formType.type) {\n @case (modalInputType.INPUT) {\n <ud-text-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [type]=\"formType.inputType ?? 'text'\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.PHONE) {\n <ud-phone-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.TEXT_AREA) {\n <ud-textarea\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [minRows]=\"formType.minRows ?? 3\"\n [maxRows]=\"formType.maxRows ?? 6\" />\n }\n @case (modalInputType.OPTIONS) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"false\" />\n }\n @case (modalInputType.MULTI_SELECT) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"true\" />\n }\n @case (modalInputType.AUTOCOMPLETE) {\n <ud-autocomplete\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.DATERANGE) {\n <ud-date-range-input\n [startControlName]=\"'start' + formType.property\"\n [endControlName]=\"'end' + formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\" />\n }\n @case (modalInputType.DATETIME) {\n <ud-date-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [min]=\"formType.min\"\n [max]=\"formType.max\" />\n }\n @case (modalInputType.TIME) {\n <ud-time-picker\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [intervalMinutes]=\"formType.intervalMinutes ?? 30\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.CHIPS) {\n <mat-form-field appearance=\"outline\">\n <mat-label>\n {{ formType.title | translate | singular | capitalize }}\n </mat-label>\n <mat-chip-grid #chipGrid [formControlName]=\"formType.property\">\n @for (option of formType.availableOptions; track option) {\n <mat-chip-row (removed)=\"formType.removeOption(option)\">\n {{ option }}\n <button matChipRemove>\n <mat-icon>cancel</mat-icon>\n </button>\n </mat-chip-row>\n }\n </mat-chip-grid>\n <input\n placeholder=\"New option...\"\n [matChipInputFor]=\"chipGrid\"\n (matChipInputTokenEnd)=\"formType.addOption($event)\" />\n </mat-form-field>\n }\n }\n }\n </div>\n </form>\n } @else if (bodyText) {\n <p class=\"ud-modal-body-text\">{{ bodyText }}</p>\n } @else {\n <ng-content></ng-content>\n }\n </div>\n\n @if (showFooter) {\n <footer class=\"ud-modal-footer\">\n @if (showClose) {\n <ud-button variant=\"stroked\" color=\"secondary\" (click)=\"close()\">\n {{ (cancelLabel ?? 'actions.close') | translate | capitalize }}\n </ud-button>\n }\n <ud-button\n [disabled]=\"!!(form && form.invalid) || confirmDisabled\"\n (click)=\"onAction()\">\n {{ (confirmLabel ?? (form ? 'actions.save' : 'actions.ok')) | translate | capitalize }}\n </ud-button>\n </footer>\n }\n</div>\n", styles: [".ud-modal-shell{display:flex;flex-direction:column;overflow:hidden;min-width:360px;width:100%}.ud-modal-header{position:relative;padding:28px 28px 18px;overflow:hidden}.ud-modal-header:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at 20% 0%,rgba(27,37,53,.1),transparent 60%),radial-gradient(circle at 85% 10%,rgba(58,74,102,.07),transparent 55%);pointer-events:none}.ud-modal-eyebrow{margin:0 0 6px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#1b2535}.ud-modal-title{margin:0;font-size:22px;line-height:1.2;font-weight:600;color:#2a3548;letter-spacing:-.01em}.ud-modal-lede{margin:8px 0 0;color:#6b7585;font-size:13.5px;line-height:1.5;max-width:40ch}.ud-modal-content{padding:4px 28px 12px;max-height:60vh;overflow-y:auto}.ud-modal-content.mat-mdc-dialog-content{padding:4px 28px 12px;margin:0;max-height:60vh}.modal-form-fields{display:flex;flex-direction:column;gap:4px;padding-top:8px}ud-text-input,ud-textarea,ud-multi-select,ud-autocomplete,ud-date-input,ud-date-range-input,ud-time-picker,ud-phone-input,mat-form-field{display:block;width:100%}.ud-modal-body-text{margin:4px 0;font-size:14px;color:#4a5568;line-height:1.55}.image-container{display:flex;justify-content:space-between;align-items:center}.image-container .arrow-picture-icon{display:flex;justify-content:center;align-items:center;padding:1rem;font-size:2rem;cursor:pointer;background:#1b2535;color:#fff;border-radius:8px;transition:background .15s ease}.image-container .arrow-picture-icon:hover{background:#253347}.image-container img{width:100%;max-width:460px;border-radius:8px}.image-footer{display:flex;justify-content:center;align-items:center;padding:8px 0 0;font-size:13px;color:#6b7585}.ud-modal-footer{display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:16px 28px 24px;border-top:1px solid #e2e5ea;background:#fbfbfd}\n"], dependencies: [{ kind: "pipe", type: CapitalizePipe, name: "capitalize" }, { kind: "pipe", type: SingularPipe, name: "singular" }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "component", type: MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "component", type: MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "directive", type: MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: TextInputComponent, selector: "ud-text-input", inputs: ["controlName", "label", "placeholder", "type", "icon", "iconFontSet", "loading", "step", "min", "max", "disabled", "hint", "size", "clearable"] }, { kind: "component", type: TextareaComponent, selector: "ud-textarea", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "minRows", "maxRows", "disabled", "hint"] }, { kind: "component", type: MultiSelectComponent, selector: "ud-multi-select", inputs: ["controlName", "label", "icon", "iconFontSet", "options", "multiple", "maxChipsVisible", "moreText", "loading", "disabled", "hint"] }, { kind: "component", type: AutocompleteComponent, selector: "ud-autocomplete", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "options", "loading", "disabled", "hint", "size"], outputs: ["searchChange"] }, { kind: "component", type: DateInputComponent, selector: "ud-date-input", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "min", "max", "disabled", "hint"] }, { kind: "component", type: DateRangeInputComponent, selector: "ud-date-range-input", inputs: ["startControlName", "endControlName", "label", "icon", "iconFontSet", "startPlaceholder", "endPlaceholder", "min", "max", "disabled", "hint"] }, { kind: "component", type: TimePickerComponent, selector: "ud-time-picker", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "intervalMinutes", "options", "disabled", "hint"] }, { kind: "component", type: PhoneInputComponent, selector: "ud-phone-input", inputs: ["controlName", "label", "placeholder", "disabled", "hint", "size"] }, { kind: "component", type: UdButtonComponent, selector: "ud-button", inputs: ["variant", "color", "size", "type", "icon", "iconPosition", "iconFontSet", "loading", "disabled", "fullWidth"] }] });
2555
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: ModalComponent, isStandalone: true, selector: "ud-modal", inputs: { title: "title", eyebrow: "eyebrow", lede: "lede", bodyText: "bodyText", showClose: "showClose", showFooter: "showFooter", confirmLabel: "confirmLabel", cancelLabel: "cancelLabel", confirmDisabled: "confirmDisabled" }, outputs: { confirm: "confirm", cancel: "cancel" }, ngImport: i0, template: "<div class=\"ud-modal-shell\">\n <header class=\"ud-modal-header\">\n @if (eyebrow) {\n <p class=\"ud-modal-eyebrow\">{{ eyebrow | translate | capitalize }}</p>\n }\n @if (title) {\n <h3 class=\"ud-modal-title\">{{ title | translate | capitalize }}</h3>\n }\n @if (lede) {\n <p class=\"ud-modal-lede\">{{ lede | translate | capitalize }}</p>\n }\n </header>\n\n <div class=\"ud-modal-content\" mat-dialog-content>\n @if (pictureUrls && currentPictureIndex != undefined) {\n <div class=\"image-container\">\n <mat-icon class=\"arrow-picture-icon\" (click)=\"previous()\">keyboard_arrow_left</mat-icon>\n <img [src]=\"pictureUrls[currentPictureIndex]\" alt=\"Picture\" />\n <mat-icon class=\"arrow-picture-icon\" (click)=\"next()\">keyboard_arrow_right</mat-icon>\n </div>\n <div class=\"image-footer\">\n {{ currentPictureIndex + 1 }} {{ 'names.of' | translate }} {{ pictureUrls.length }}\n </div>\n } @else if (form) {\n <form [formGroup]=\"form\">\n <div class=\"modal-form-fields\">\n @for (formType of data.forms; track formType) {\n @switch (formType.type) {\n @case (modalInputType.INPUT) {\n <ud-text-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [type]=\"formType.inputType ?? 'text'\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.PHONE) {\n <ud-phone-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.TEXT_AREA) {\n <ud-textarea\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [minRows]=\"formType.minRows ?? 3\"\n [maxRows]=\"formType.maxRows ?? 6\" />\n }\n @case (modalInputType.OPTIONS) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"false\" />\n }\n @case (modalInputType.MULTI_SELECT) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"true\" />\n }\n @case (modalInputType.AUTOCOMPLETE) {\n <ud-autocomplete\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.DATERANGE) {\n <ud-date-range-input\n [startControlName]=\"'start' + formType.property\"\n [endControlName]=\"'end' + formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\" />\n }\n @case (modalInputType.DATETIME) {\n <ud-date-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [min]=\"formType.min\"\n [max]=\"formType.max\" />\n }\n @case (modalInputType.TIME) {\n <ud-time-picker\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [intervalMinutes]=\"formType.intervalMinutes ?? 30\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.CHIPS) {\n <mat-form-field appearance=\"outline\">\n <mat-label>\n {{ formType.title | translate | singular | capitalize }}\n </mat-label>\n <mat-chip-grid #chipGrid [formControlName]=\"formType.property\">\n @for (option of formType.availableOptions; track option) {\n <mat-chip-row (removed)=\"formType.removeOption(option)\">\n {{ option }}\n <button matChipRemove>\n <mat-icon>cancel</mat-icon>\n </button>\n </mat-chip-row>\n }\n </mat-chip-grid>\n <input\n placeholder=\"New option...\"\n [matChipInputFor]=\"chipGrid\"\n (matChipInputTokenEnd)=\"formType.addOption($event)\" />\n </mat-form-field>\n }\n }\n }\n </div>\n </form>\n } @else if (bodyText) {\n <p class=\"ud-modal-body-text\">{{ bodyText }}</p>\n } @else {\n <ng-content></ng-content>\n }\n </div>\n\n @if (showFooter) {\n <footer class=\"ud-modal-footer\">\n @if (data.delete) {\n <ud-button variant=\"stroked\" color=\"danger\" class=\"ud-modal-footer__delete\" (click)=\"onDelete()\">\n {{ data.deleteLabel ?? 'Delete' }}\n </ud-button>\n }\n @if (showClose) {\n <ud-button variant=\"stroked\" color=\"secondary\" (click)=\"close()\">\n {{ (cancelLabel ?? 'actions.close') | translate | capitalize }}\n </ud-button>\n }\n <ud-button\n [disabled]=\"!!(form && form.invalid) || confirmDisabled\"\n (click)=\"onAction()\">\n {{ (confirmLabel ?? (form ? 'actions.save' : 'actions.ok')) | translate | capitalize }}\n </ud-button>\n </footer>\n }\n</div>\n", styles: [".ud-modal-shell{display:flex;flex-direction:column;overflow:hidden;min-width:360px;width:100%}.ud-modal-header{position:relative;padding:28px 28px 18px;overflow:hidden}.ud-modal-header:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at 20% 0%,rgba(27,37,53,.1),transparent 60%),radial-gradient(circle at 85% 10%,rgba(58,74,102,.07),transparent 55%);pointer-events:none}.ud-modal-eyebrow{margin:0 0 6px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#1b2535}.ud-modal-title{margin:0;font-size:22px;line-height:1.2;font-weight:600;color:#2a3548;letter-spacing:-.01em}.ud-modal-lede{margin:8px 0 0;color:#6b7585;font-size:13.5px;line-height:1.5;max-width:40ch}.ud-modal-content{padding:4px 28px 12px;max-height:60vh;overflow-y:auto}.ud-modal-content.mat-mdc-dialog-content{padding:4px 28px 12px;margin:0;max-height:60vh}.modal-form-fields{display:flex;flex-direction:column;gap:4px;padding-top:8px}ud-text-input,ud-textarea,ud-multi-select,ud-autocomplete,ud-date-input,ud-date-range-input,ud-time-picker,ud-phone-input,mat-form-field{display:block;width:100%}.ud-modal-body-text{margin:4px 0;font-size:14px;color:#4a5568;line-height:1.55}.image-container{display:flex;justify-content:space-between;align-items:center}.image-container .arrow-picture-icon{display:flex;justify-content:center;align-items:center;padding:1rem;font-size:2rem;cursor:pointer;background:#1b2535;color:#fff;border-radius:8px;transition:background .15s ease}.image-container .arrow-picture-icon:hover{background:#253347}.image-container img{width:100%;max-width:460px;border-radius:8px}.image-footer{display:flex;justify-content:center;align-items:center;padding:8px 0 0;font-size:13px;color:#6b7585}.ud-modal-footer{display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:16px 28px 24px;border-top:1px solid #e2e5ea;background:#fbfbfd}.ud-modal-footer__delete{margin-right:auto}\n"], dependencies: [{ kind: "pipe", type: CapitalizePipe, name: "capitalize" }, { kind: "pipe", type: SingularPipe, name: "singular" }, { kind: "pipe", type: TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "component", type: MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "directive", type: MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "component", type: MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "directive", type: MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: TextInputComponent, selector: "ud-text-input", inputs: ["controlName", "label", "placeholder", "type", "icon", "iconFontSet", "loading", "step", "min", "max", "disabled", "hint", "size", "clearable"] }, { kind: "component", type: TextareaComponent, selector: "ud-textarea", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "minRows", "maxRows", "disabled", "hint"] }, { kind: "component", type: MultiSelectComponent, selector: "ud-multi-select", inputs: ["controlName", "label", "icon", "iconFontSet", "options", "multiple", "maxChipsVisible", "moreText", "loading", "disabled", "hint"] }, { kind: "component", type: AutocompleteComponent, selector: "ud-autocomplete", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "options", "loading", "disabled", "hint", "size"], outputs: ["searchChange"] }, { kind: "component", type: DateInputComponent, selector: "ud-date-input", inputs: ["controlName", "label", "placeholder", "icon", "iconFontSet", "min", "max", "disabled", "hint"] }, { kind: "component", type: DateRangeInputComponent, selector: "ud-date-range-input", inputs: ["startControlName", "endControlName", "label", "icon", "iconFontSet", "startPlaceholder", "endPlaceholder", "min", "max", "disabled", "hint"] }, { kind: "component", type: TimePickerComponent, selector: "ud-time-picker", inputs: ["controlName", "label", "placeholder", "intervalMinutes", "options", "disabled", "hint"] }, { kind: "component", type: PhoneInputComponent, selector: "ud-phone-input", inputs: ["controlName", "label", "placeholder", "disabled", "hint", "size"] }, { kind: "component", type: UdButtonComponent, selector: "ud-button", inputs: ["variant", "color", "size", "type", "icon", "iconPosition", "iconFontSet", "loading", "disabled", "fullWidth"] }] });
2510
2556
  }
2511
2557
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ModalComponent, decorators: [{
2512
2558
  type: Component,
@@ -2532,7 +2578,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
2532
2578
  TimePickerComponent,
2533
2579
  PhoneInputComponent,
2534
2580
  UdButtonComponent,
2535
- ], template: "<div class=\"ud-modal-shell\">\n <header class=\"ud-modal-header\">\n @if (eyebrow) {\n <p class=\"ud-modal-eyebrow\">{{ eyebrow | translate | capitalize }}</p>\n }\n @if (title) {\n <h3 class=\"ud-modal-title\">{{ title | translate | capitalize }}</h3>\n }\n @if (lede) {\n <p class=\"ud-modal-lede\">{{ lede | translate | capitalize }}</p>\n }\n </header>\n\n <div class=\"ud-modal-content\" mat-dialog-content>\n @if (pictureUrls && currentPictureIndex != undefined) {\n <div class=\"image-container\">\n <mat-icon class=\"arrow-picture-icon\" (click)=\"previous()\">keyboard_arrow_left</mat-icon>\n <img [src]=\"pictureUrls[currentPictureIndex]\" alt=\"Picture\" />\n <mat-icon class=\"arrow-picture-icon\" (click)=\"next()\">keyboard_arrow_right</mat-icon>\n </div>\n <div class=\"image-footer\">\n {{ currentPictureIndex + 1 }} {{ 'names.of' | translate }} {{ pictureUrls.length }}\n </div>\n } @else if (form) {\n <form [formGroup]=\"form\">\n <div class=\"modal-form-fields\">\n @for (formType of data.forms; track formType) {\n @switch (formType.type) {\n @case (modalInputType.INPUT) {\n <ud-text-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [type]=\"formType.inputType ?? 'text'\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.PHONE) {\n <ud-phone-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.TEXT_AREA) {\n <ud-textarea\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [minRows]=\"formType.minRows ?? 3\"\n [maxRows]=\"formType.maxRows ?? 6\" />\n }\n @case (modalInputType.OPTIONS) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"false\" />\n }\n @case (modalInputType.MULTI_SELECT) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"true\" />\n }\n @case (modalInputType.AUTOCOMPLETE) {\n <ud-autocomplete\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.DATERANGE) {\n <ud-date-range-input\n [startControlName]=\"'start' + formType.property\"\n [endControlName]=\"'end' + formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\" />\n }\n @case (modalInputType.DATETIME) {\n <ud-date-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [min]=\"formType.min\"\n [max]=\"formType.max\" />\n }\n @case (modalInputType.TIME) {\n <ud-time-picker\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [intervalMinutes]=\"formType.intervalMinutes ?? 30\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.CHIPS) {\n <mat-form-field appearance=\"outline\">\n <mat-label>\n {{ formType.title | translate | singular | capitalize }}\n </mat-label>\n <mat-chip-grid #chipGrid [formControlName]=\"formType.property\">\n @for (option of formType.availableOptions; track option) {\n <mat-chip-row (removed)=\"formType.removeOption(option)\">\n {{ option }}\n <button matChipRemove>\n <mat-icon>cancel</mat-icon>\n </button>\n </mat-chip-row>\n }\n </mat-chip-grid>\n <input\n placeholder=\"New option...\"\n [matChipInputFor]=\"chipGrid\"\n (matChipInputTokenEnd)=\"formType.addOption($event)\" />\n </mat-form-field>\n }\n }\n }\n </div>\n </form>\n } @else if (bodyText) {\n <p class=\"ud-modal-body-text\">{{ bodyText }}</p>\n } @else {\n <ng-content></ng-content>\n }\n </div>\n\n @if (showFooter) {\n <footer class=\"ud-modal-footer\">\n @if (showClose) {\n <ud-button variant=\"stroked\" color=\"secondary\" (click)=\"close()\">\n {{ (cancelLabel ?? 'actions.close') | translate | capitalize }}\n </ud-button>\n }\n <ud-button\n [disabled]=\"!!(form && form.invalid) || confirmDisabled\"\n (click)=\"onAction()\">\n {{ (confirmLabel ?? (form ? 'actions.save' : 'actions.ok')) | translate | capitalize }}\n </ud-button>\n </footer>\n }\n</div>\n", styles: [".ud-modal-shell{display:flex;flex-direction:column;overflow:hidden;min-width:360px;width:100%}.ud-modal-header{position:relative;padding:28px 28px 18px;overflow:hidden}.ud-modal-header:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at 20% 0%,rgba(27,37,53,.1),transparent 60%),radial-gradient(circle at 85% 10%,rgba(58,74,102,.07),transparent 55%);pointer-events:none}.ud-modal-eyebrow{margin:0 0 6px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#1b2535}.ud-modal-title{margin:0;font-size:22px;line-height:1.2;font-weight:600;color:#2a3548;letter-spacing:-.01em}.ud-modal-lede{margin:8px 0 0;color:#6b7585;font-size:13.5px;line-height:1.5;max-width:40ch}.ud-modal-content{padding:4px 28px 12px;max-height:60vh;overflow-y:auto}.ud-modal-content.mat-mdc-dialog-content{padding:4px 28px 12px;margin:0;max-height:60vh}.modal-form-fields{display:flex;flex-direction:column;gap:4px;padding-top:8px}ud-text-input,ud-textarea,ud-multi-select,ud-autocomplete,ud-date-input,ud-date-range-input,ud-time-picker,ud-phone-input,mat-form-field{display:block;width:100%}.ud-modal-body-text{margin:4px 0;font-size:14px;color:#4a5568;line-height:1.55}.image-container{display:flex;justify-content:space-between;align-items:center}.image-container .arrow-picture-icon{display:flex;justify-content:center;align-items:center;padding:1rem;font-size:2rem;cursor:pointer;background:#1b2535;color:#fff;border-radius:8px;transition:background .15s ease}.image-container .arrow-picture-icon:hover{background:#253347}.image-container img{width:100%;max-width:460px;border-radius:8px}.image-footer{display:flex;justify-content:center;align-items:center;padding:8px 0 0;font-size:13px;color:#6b7585}.ud-modal-footer{display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:16px 28px 24px;border-top:1px solid #e2e5ea;background:#fbfbfd}\n"] }]
2581
+ ], template: "<div class=\"ud-modal-shell\">\n <header class=\"ud-modal-header\">\n @if (eyebrow) {\n <p class=\"ud-modal-eyebrow\">{{ eyebrow | translate | capitalize }}</p>\n }\n @if (title) {\n <h3 class=\"ud-modal-title\">{{ title | translate | capitalize }}</h3>\n }\n @if (lede) {\n <p class=\"ud-modal-lede\">{{ lede | translate | capitalize }}</p>\n }\n </header>\n\n <div class=\"ud-modal-content\" mat-dialog-content>\n @if (pictureUrls && currentPictureIndex != undefined) {\n <div class=\"image-container\">\n <mat-icon class=\"arrow-picture-icon\" (click)=\"previous()\">keyboard_arrow_left</mat-icon>\n <img [src]=\"pictureUrls[currentPictureIndex]\" alt=\"Picture\" />\n <mat-icon class=\"arrow-picture-icon\" (click)=\"next()\">keyboard_arrow_right</mat-icon>\n </div>\n <div class=\"image-footer\">\n {{ currentPictureIndex + 1 }} {{ 'names.of' | translate }} {{ pictureUrls.length }}\n </div>\n } @else if (form) {\n <form [formGroup]=\"form\">\n <div class=\"modal-form-fields\">\n @for (formType of data.forms; track formType) {\n @switch (formType.type) {\n @case (modalInputType.INPUT) {\n <ud-text-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [type]=\"formType.inputType ?? 'text'\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.PHONE) {\n <ud-phone-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\" />\n }\n @case (modalInputType.TEXT_AREA) {\n <ud-textarea\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [minRows]=\"formType.minRows ?? 3\"\n [maxRows]=\"formType.maxRows ?? 6\" />\n }\n @case (modalInputType.OPTIONS) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"false\" />\n }\n @case (modalInputType.MULTI_SELECT) {\n <ud-multi-select\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [options]=\"formType.availableOptions\"\n [multiple]=\"true\" />\n }\n @case (modalInputType.AUTOCOMPLETE) {\n <ud-autocomplete\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [placeholder]=\"formType.placeholder ?? ''\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.DATERANGE) {\n <ud-date-range-input\n [startControlName]=\"'start' + formType.property\"\n [endControlName]=\"'end' + formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\" />\n }\n @case (modalInputType.DATETIME) {\n <ud-date-input\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [min]=\"formType.min\"\n [max]=\"formType.max\" />\n }\n @case (modalInputType.TIME) {\n <ud-time-picker\n [controlName]=\"formType.property\"\n [label]=\"formType.title | translate | singular | capitalize\"\n [intervalMinutes]=\"formType.intervalMinutes ?? 30\"\n [options]=\"formType.availableOptions\" />\n }\n @case (modalInputType.CHIPS) {\n <mat-form-field appearance=\"outline\">\n <mat-label>\n {{ formType.title | translate | singular | capitalize }}\n </mat-label>\n <mat-chip-grid #chipGrid [formControlName]=\"formType.property\">\n @for (option of formType.availableOptions; track option) {\n <mat-chip-row (removed)=\"formType.removeOption(option)\">\n {{ option }}\n <button matChipRemove>\n <mat-icon>cancel</mat-icon>\n </button>\n </mat-chip-row>\n }\n </mat-chip-grid>\n <input\n placeholder=\"New option...\"\n [matChipInputFor]=\"chipGrid\"\n (matChipInputTokenEnd)=\"formType.addOption($event)\" />\n </mat-form-field>\n }\n }\n }\n </div>\n </form>\n } @else if (bodyText) {\n <p class=\"ud-modal-body-text\">{{ bodyText }}</p>\n } @else {\n <ng-content></ng-content>\n }\n </div>\n\n @if (showFooter) {\n <footer class=\"ud-modal-footer\">\n @if (data.delete) {\n <ud-button variant=\"stroked\" color=\"danger\" class=\"ud-modal-footer__delete\" (click)=\"onDelete()\">\n {{ data.deleteLabel ?? 'Delete' }}\n </ud-button>\n }\n @if (showClose) {\n <ud-button variant=\"stroked\" color=\"secondary\" (click)=\"close()\">\n {{ (cancelLabel ?? 'actions.close') | translate | capitalize }}\n </ud-button>\n }\n <ud-button\n [disabled]=\"!!(form && form.invalid) || confirmDisabled\"\n (click)=\"onAction()\">\n {{ (confirmLabel ?? (form ? 'actions.save' : 'actions.ok')) | translate | capitalize }}\n </ud-button>\n </footer>\n }\n</div>\n", styles: [".ud-modal-shell{display:flex;flex-direction:column;overflow:hidden;min-width:360px;width:100%}.ud-modal-header{position:relative;padding:28px 28px 18px;overflow:hidden}.ud-modal-header:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at 20% 0%,rgba(27,37,53,.1),transparent 60%),radial-gradient(circle at 85% 10%,rgba(58,74,102,.07),transparent 55%);pointer-events:none}.ud-modal-eyebrow{margin:0 0 6px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#1b2535}.ud-modal-title{margin:0;font-size:22px;line-height:1.2;font-weight:600;color:#2a3548;letter-spacing:-.01em}.ud-modal-lede{margin:8px 0 0;color:#6b7585;font-size:13.5px;line-height:1.5;max-width:40ch}.ud-modal-content{padding:4px 28px 12px;max-height:60vh;overflow-y:auto}.ud-modal-content.mat-mdc-dialog-content{padding:4px 28px 12px;margin:0;max-height:60vh}.modal-form-fields{display:flex;flex-direction:column;gap:4px;padding-top:8px}ud-text-input,ud-textarea,ud-multi-select,ud-autocomplete,ud-date-input,ud-date-range-input,ud-time-picker,ud-phone-input,mat-form-field{display:block;width:100%}.ud-modal-body-text{margin:4px 0;font-size:14px;color:#4a5568;line-height:1.55}.image-container{display:flex;justify-content:space-between;align-items:center}.image-container .arrow-picture-icon{display:flex;justify-content:center;align-items:center;padding:1rem;font-size:2rem;cursor:pointer;background:#1b2535;color:#fff;border-radius:8px;transition:background .15s ease}.image-container .arrow-picture-icon:hover{background:#253347}.image-container img{width:100%;max-width:460px;border-radius:8px}.image-footer{display:flex;justify-content:center;align-items:center;padding:8px 0 0;font-size:13px;color:#6b7585}.ud-modal-footer{display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:16px 28px 24px;border-top:1px solid #e2e5ea;background:#fbfbfd}.ud-modal-footer__delete{margin-right:auto}\n"] }]
2536
2582
  }], ctorParameters: () => [{ type: undefined, decorators: [{
2537
2583
  type: Optional
2538
2584
  }, {
@@ -3120,6 +3166,497 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
3120
3166
  type: Input
3121
3167
  }] } });
3122
3168
 
3169
+ class ChipInputComponent {
3170
+ controlName;
3171
+ label = '';
3172
+ placeholder = '';
3173
+ icon;
3174
+ iconFontSet = 'material-icons-outlined';
3175
+ disabled = false;
3176
+ hint = '';
3177
+ focused = false;
3178
+ separatorKeysCodes = [ENTER, COMMA];
3179
+ controlContainer = inject(ControlContainer);
3180
+ get control() {
3181
+ return this.controlContainer.control.get(this.controlName);
3182
+ }
3183
+ get chips() {
3184
+ return this.control.value ?? [];
3185
+ }
3186
+ ngOnChanges(changes) {
3187
+ if (changes['disabled']) {
3188
+ this.disabled ? this.control.disable() : this.control.enable();
3189
+ }
3190
+ }
3191
+ addChip(event) {
3192
+ const value = (event.value ?? '').trim();
3193
+ if (value) {
3194
+ this.control.setValue([...this.chips, value]);
3195
+ this.control.markAsDirty();
3196
+ }
3197
+ event.chipInput.clear();
3198
+ }
3199
+ removeChip(index) {
3200
+ const updated = this.chips.filter((_, i) => i !== index);
3201
+ this.control.setValue(updated);
3202
+ this.control.markAsDirty();
3203
+ }
3204
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ChipInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3205
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: ChipInputComponent, isStandalone: true, selector: "ud-chip-input", inputs: { controlName: "controlName", label: "label", placeholder: "placeholder", icon: "icon", iconFontSet: "iconFontSet", disabled: "disabled", hint: "hint" }, usesOnChanges: true, ngImport: i0, template: "<div\n class=\"ud-chip-input\"\n [class.ud-chip-input--focused]=\"focused\"\n [class.ud-chip-input--disabled]=\"control.disabled\"\n [class.ud-chip-input--error]=\"control.invalid && control.touched\">\n @if (label) {\n <label class=\"ud-chip-input__label\">{{ label }}</label>\n }\n <div class=\"ud-chip-input__wrapper\">\n @if (icon) {\n <mat-icon class=\"ud-chip-input__icon\" [fontSet]=\"iconFontSet\">{{ icon }}</mat-icon>\n }\n <mat-chip-grid #chipGrid class=\"ud-chip-input__grid\" aria-label=\"Chip input\">\n @for (chip of chips; track chip; let i = $index) {\n <mat-chip-row class=\"ud-chip\" (removed)=\"removeChip(i)\">\n {{ chip }}\n <button matChipRemove aria-label=\"Remove chip\">\n <mat-icon>cancel</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n class=\"ud-chip-input__field\"\n [placeholder]=\"chips.length === 0 ? placeholder : ''\"\n [matChipInputFor]=\"chipGrid\"\n [matChipInputSeparatorKeyCodes]=\"separatorKeysCodes\"\n [disabled]=\"control.disabled\"\n (matChipInputTokenEnd)=\"addChip($event)\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n </mat-chip-grid>\n </div>\n @if (hint) {\n <span class=\"ud-chip-input__hint\">{{ hint }}</span>\n }\n</div>\n", styles: [":host{display:block;width:100%}.ud-chip-input{display:flex;flex-direction:column;gap:5px;width:100%}.ud-chip-input__label{font-family:DM Sans,system-ui,sans-serif;font-size:13px;font-weight:500;color:var(--eu-text, #2a3548);line-height:1}.ud-chip-input__wrapper{display:flex;align-items:flex-start;gap:8px;background:#f8fafc;border:1px solid var(--eu-border-mid, #d8dde6);border-radius:8px;padding:6px 12px;min-height:40px;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}.ud-chip-input--focused .ud-chip-input__wrapper{border-color:var(--eu-navy, #1b2535);box-shadow:0 0 0 3px #1b253514;background:#fff}.ud-chip-input--error .ud-chip-input__wrapper{border-color:#e53935;box-shadow:0 0 0 3px #e539351a}.ud-chip-input--error.ud-chip-input--focused .ud-chip-input__wrapper{border-color:#e53935}.ud-chip-input--disabled .ud-chip-input__wrapper{background:var(--eu-bg, #f4f5f7);border-color:var(--eu-border-light, #e8eaef);cursor:not-allowed;opacity:.6}.ud-chip-input__icon{flex-shrink:0;color:var(--eu-muted, #6b7585);font-size:18px;width:18px;height:18px;margin-top:3px;line-height:1;transition:color .18s ease}.ud-chip-input--focused .ud-chip-input__icon{color:var(--eu-navy, #1b2535)}.ud-chip-input__grid{flex:1;min-width:0}:host ::ng-deep .ud-chip-input__grid .mdc-evolution-chip-set__chips{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin:0;padding:0}.ud-chip-input__field{flex:1;min-width:80px;border:none;background:transparent;outline:none;font-family:DM Sans,system-ui,sans-serif;font-size:14px;color:var(--eu-text, #2a3548);line-height:1;padding:2px 0}.ud-chip-input__field::placeholder{color:var(--eu-muted, #9099a8)}.ud-chip-input__field:disabled{cursor:not-allowed;color:var(--eu-muted, #9099a8)}:host ::ng-deep mat-chip-row.ud-chip{--mdc-chip-container-color: rgba(27, 37, 53, .07);--mdc-chip-label-text-color: var(--eu-text, #2a3548);--mdc-chip-label-text-font: \"DM Sans\", system-ui, sans-serif;--mdc-chip-label-text-size: 12px;--mdc-chip-label-text-weight: 500;--mdc-chip-container-shape-radius: 20px;--mdc-chip-with-trailing-icon-trailing-icon-color: var(--eu-muted, #6b7585);--mdc-chip-elevated-container-color: rgba(27, 37, 53, .07);height:24px;border:none}:host ::ng-deep mat-chip-row.ud-chip .mdc-evolution-chip__action--trailing{padding:0 4px}:host ::ng-deep mat-chip-row.ud-chip .mdc-evolution-chip__action--trailing .mat-icon{font-size:14px;width:14px;height:14px;line-height:14px;color:var(--eu-muted, #6b7585);transition:color .15s ease}:host ::ng-deep mat-chip-row.ud-chip .mdc-evolution-chip__action--trailing:hover .mat-icon{color:var(--eu-navy, #1b2535)}.ud-chip-input__hint{font-family:DM Sans,system-ui,sans-serif;font-size:12px;color:var(--eu-muted, #6b7585);line-height:1.3}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "component", type: MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "directive", type: MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputFor", "matChipInputAddOnBlur", "matChipInputSeparatorKeyCodes", "placeholder", "id", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { kind: "directive", type: MatChipRemove, selector: "[matChipRemove]" }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], viewProviders: [{ provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) }] });
3206
+ }
3207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: ChipInputComponent, decorators: [{
3208
+ type: Component,
3209
+ args: [{ selector: 'ud-chip-input', standalone: true, imports: [ReactiveFormsModule, MatChipGrid, MatChipRow, MatChipInput, MatChipRemove, MatIcon], viewProviders: [{ provide: ControlContainer, useFactory: () => inject(ControlContainer, { skipSelf: true }) }], template: "<div\n class=\"ud-chip-input\"\n [class.ud-chip-input--focused]=\"focused\"\n [class.ud-chip-input--disabled]=\"control.disabled\"\n [class.ud-chip-input--error]=\"control.invalid && control.touched\">\n @if (label) {\n <label class=\"ud-chip-input__label\">{{ label }}</label>\n }\n <div class=\"ud-chip-input__wrapper\">\n @if (icon) {\n <mat-icon class=\"ud-chip-input__icon\" [fontSet]=\"iconFontSet\">{{ icon }}</mat-icon>\n }\n <mat-chip-grid #chipGrid class=\"ud-chip-input__grid\" aria-label=\"Chip input\">\n @for (chip of chips; track chip; let i = $index) {\n <mat-chip-row class=\"ud-chip\" (removed)=\"removeChip(i)\">\n {{ chip }}\n <button matChipRemove aria-label=\"Remove chip\">\n <mat-icon>cancel</mat-icon>\n </button>\n </mat-chip-row>\n }\n <input\n class=\"ud-chip-input__field\"\n [placeholder]=\"chips.length === 0 ? placeholder : ''\"\n [matChipInputFor]=\"chipGrid\"\n [matChipInputSeparatorKeyCodes]=\"separatorKeysCodes\"\n [disabled]=\"control.disabled\"\n (matChipInputTokenEnd)=\"addChip($event)\"\n (focus)=\"focused = true\"\n (blur)=\"focused = false\" />\n </mat-chip-grid>\n </div>\n @if (hint) {\n <span class=\"ud-chip-input__hint\">{{ hint }}</span>\n }\n</div>\n", styles: [":host{display:block;width:100%}.ud-chip-input{display:flex;flex-direction:column;gap:5px;width:100%}.ud-chip-input__label{font-family:DM Sans,system-ui,sans-serif;font-size:13px;font-weight:500;color:var(--eu-text, #2a3548);line-height:1}.ud-chip-input__wrapper{display:flex;align-items:flex-start;gap:8px;background:#f8fafc;border:1px solid var(--eu-border-mid, #d8dde6);border-radius:8px;padding:6px 12px;min-height:40px;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease}.ud-chip-input--focused .ud-chip-input__wrapper{border-color:var(--eu-navy, #1b2535);box-shadow:0 0 0 3px #1b253514;background:#fff}.ud-chip-input--error .ud-chip-input__wrapper{border-color:#e53935;box-shadow:0 0 0 3px #e539351a}.ud-chip-input--error.ud-chip-input--focused .ud-chip-input__wrapper{border-color:#e53935}.ud-chip-input--disabled .ud-chip-input__wrapper{background:var(--eu-bg, #f4f5f7);border-color:var(--eu-border-light, #e8eaef);cursor:not-allowed;opacity:.6}.ud-chip-input__icon{flex-shrink:0;color:var(--eu-muted, #6b7585);font-size:18px;width:18px;height:18px;margin-top:3px;line-height:1;transition:color .18s ease}.ud-chip-input--focused .ud-chip-input__icon{color:var(--eu-navy, #1b2535)}.ud-chip-input__grid{flex:1;min-width:0}:host ::ng-deep .ud-chip-input__grid .mdc-evolution-chip-set__chips{display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin:0;padding:0}.ud-chip-input__field{flex:1;min-width:80px;border:none;background:transparent;outline:none;font-family:DM Sans,system-ui,sans-serif;font-size:14px;color:var(--eu-text, #2a3548);line-height:1;padding:2px 0}.ud-chip-input__field::placeholder{color:var(--eu-muted, #9099a8)}.ud-chip-input__field:disabled{cursor:not-allowed;color:var(--eu-muted, #9099a8)}:host ::ng-deep mat-chip-row.ud-chip{--mdc-chip-container-color: rgba(27, 37, 53, .07);--mdc-chip-label-text-color: var(--eu-text, #2a3548);--mdc-chip-label-text-font: \"DM Sans\", system-ui, sans-serif;--mdc-chip-label-text-size: 12px;--mdc-chip-label-text-weight: 500;--mdc-chip-container-shape-radius: 20px;--mdc-chip-with-trailing-icon-trailing-icon-color: var(--eu-muted, #6b7585);--mdc-chip-elevated-container-color: rgba(27, 37, 53, .07);height:24px;border:none}:host ::ng-deep mat-chip-row.ud-chip .mdc-evolution-chip__action--trailing{padding:0 4px}:host ::ng-deep mat-chip-row.ud-chip .mdc-evolution-chip__action--trailing .mat-icon{font-size:14px;width:14px;height:14px;line-height:14px;color:var(--eu-muted, #6b7585);transition:color .15s ease}:host ::ng-deep mat-chip-row.ud-chip .mdc-evolution-chip__action--trailing:hover .mat-icon{color:var(--eu-navy, #1b2535)}.ud-chip-input__hint{font-family:DM Sans,system-ui,sans-serif;font-size:12px;color:var(--eu-muted, #6b7585);line-height:1.3}\n"] }]
3210
+ }], propDecorators: { controlName: [{
3211
+ type: Input,
3212
+ args: [{ required: true }]
3213
+ }], label: [{
3214
+ type: Input
3215
+ }], placeholder: [{
3216
+ type: Input
3217
+ }], icon: [{
3218
+ type: Input
3219
+ }], iconFontSet: [{
3220
+ type: Input
3221
+ }], disabled: [{
3222
+ type: Input
3223
+ }], hint: [{
3224
+ type: Input
3225
+ }] } });
3226
+
3227
+ class CalendarComponent {
3228
+ slots = input([]);
3229
+ mode = input('admin');
3230
+ defaultView = input('week');
3231
+ slotDuration = input(30);
3232
+ minHour = input(8);
3233
+ maxHour = input(20);
3234
+ slotAdded = output();
3235
+ slotUpdated = output();
3236
+ slotRemoved = output();
3237
+ slotBooked = output();
3238
+ activeView = signal('week');
3239
+ navDate = signal(new Date());
3240
+ viewOptions = [
3241
+ { id: 'day', label: 'Day' },
3242
+ { id: 'week', label: 'Week' },
3243
+ { id: 'month', label: 'Month' },
3244
+ ];
3245
+ CELL_H = 56;
3246
+ TIME_COL_W = 52;
3247
+ dialog = inject(MatDialog);
3248
+ elRef = inject(ElementRef);
3249
+ // ── Drag state ─────────────────────────────────────────────────────────────
3250
+ draggingSlot = null;
3251
+ dragMoved = false;
3252
+ dragTarget = null;
3253
+ dragSlotDurationMin = 0;
3254
+ dragStartPos = null;
3255
+ constructor() {
3256
+ effect(() => {
3257
+ this.activeView.set(this.defaultView());
3258
+ }, { allowSignalWrites: true });
3259
+ }
3260
+ // ── Time axis ──────────────────────────────────────────────────────────────
3261
+ timeSlots = computed(() => {
3262
+ const slots = [];
3263
+ for (let h = this.minHour(); h < this.maxHour(); h++) {
3264
+ slots.push({ hour: h, minute: 0, label: this.formatHour(h, 0) });
3265
+ slots.push({ hour: h, minute: 30, label: '' });
3266
+ }
3267
+ return slots;
3268
+ });
3269
+ // ── Week view ──────────────────────────────────────────────────────────────
3270
+ weekDays = computed(() => {
3271
+ const d = this.navDate();
3272
+ const monday = this.getMonday(d);
3273
+ return Array.from({ length: 7 }, (_, i) => {
3274
+ const day = new Date(monday);
3275
+ day.setDate(monday.getDate() + i);
3276
+ return day;
3277
+ });
3278
+ });
3279
+ // ── Month view ─────────────────────────────────────────────────────────────
3280
+ monthWeeks = computed(() => {
3281
+ const d = this.navDate();
3282
+ const year = d.getFullYear();
3283
+ const month = d.getMonth();
3284
+ const firstDay = new Date(year, month, 1);
3285
+ const lastDay = new Date(year, month + 1, 0);
3286
+ const start = this.getMonday(firstDay);
3287
+ const weeks = [];
3288
+ let current = new Date(start);
3289
+ while (current <= lastDay || weeks.length < 6) {
3290
+ const week = [];
3291
+ for (let i = 0; i < 7; i++) {
3292
+ week.push(new Date(current));
3293
+ current.setDate(current.getDate() + 1);
3294
+ }
3295
+ weeks.push(week);
3296
+ if (current > lastDay && weeks.length >= 4)
3297
+ break;
3298
+ }
3299
+ return weeks;
3300
+ });
3301
+ // ── Header label ───────────────────────────────────────────────────────────
3302
+ headerLabel = computed(() => {
3303
+ const d = this.navDate();
3304
+ const view = this.activeView();
3305
+ if (view === 'month') {
3306
+ return d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
3307
+ }
3308
+ if (view === 'week') {
3309
+ const days = this.weekDays();
3310
+ const start = days[0];
3311
+ const end = days[6];
3312
+ if (start.getMonth() === end.getMonth()) {
3313
+ return `${start.toLocaleDateString('en-US', { month: 'long' })} ${start.getDate()}–${end.getDate()}, ${start.getFullYear()}`;
3314
+ }
3315
+ return `${start.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} – ${end.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
3316
+ }
3317
+ return d.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
3318
+ });
3319
+ // ── Navigation ─────────────────────────────────────────────────────────────
3320
+ navigate(dir) {
3321
+ const d = new Date(this.navDate());
3322
+ const view = this.activeView();
3323
+ if (view === 'day')
3324
+ d.setDate(d.getDate() + dir);
3325
+ else if (view === 'week')
3326
+ d.setDate(d.getDate() + dir * 7);
3327
+ else
3328
+ d.setMonth(d.getMonth() + dir);
3329
+ this.navDate.set(d);
3330
+ }
3331
+ goToday() {
3332
+ this.navDate.set(new Date());
3333
+ }
3334
+ switchView(view) {
3335
+ this.activeView.set(view);
3336
+ }
3337
+ clickDay(day) {
3338
+ this.navDate.set(new Date(day));
3339
+ this.activeView.set('day');
3340
+ }
3341
+ // ── Slot helpers ───────────────────────────────────────────────────────────
3342
+ slotsForDay(day) {
3343
+ return this.slots().filter(s => this.isSameDay(s.start, day));
3344
+ }
3345
+ slotTop(slot) {
3346
+ const startMinutes = slot.start.getHours() * 60 + slot.start.getMinutes();
3347
+ const gridStart = this.minHour() * 60;
3348
+ return ((startMinutes - gridStart) / 30) * this.CELL_H;
3349
+ }
3350
+ slotHeight(slot) {
3351
+ const durationMs = slot.end.getTime() - slot.start.getTime();
3352
+ const durationMin = durationMs / 60000;
3353
+ return Math.max((durationMin / 30) * this.CELL_H - 2, 24);
3354
+ }
3355
+ slotBg(slot) {
3356
+ if (slot.color)
3357
+ return this.hexToRgba(slot.color, 0.15);
3358
+ if (slot.booked)
3359
+ return 'rgba(229,57,53,0.10)';
3360
+ return 'rgba(27,37,53,0.10)';
3361
+ }
3362
+ slotTextColor(slot) {
3363
+ if (slot.color)
3364
+ return slot.color;
3365
+ if (slot.booked)
3366
+ return '#c62828';
3367
+ return 'var(--eu-navy)';
3368
+ }
3369
+ slotBorderColor(slot) {
3370
+ if (slot.color)
3371
+ return slot.color;
3372
+ if (slot.booked)
3373
+ return '#e53935';
3374
+ return 'var(--eu-navy)';
3375
+ }
3376
+ formatSlotTime(slot) {
3377
+ return `${this.formatTime(slot.start)} – ${this.formatTime(slot.end)}`;
3378
+ }
3379
+ isToday(day) {
3380
+ return this.isSameDay(day, new Date());
3381
+ }
3382
+ isCurrentMonth(day) {
3383
+ return day.getMonth() === this.navDate().getMonth();
3384
+ }
3385
+ dayLabel(day) {
3386
+ return day.toLocaleDateString('en-US', { weekday: 'short' });
3387
+ }
3388
+ dayNum(day) {
3389
+ return day.getDate();
3390
+ }
3391
+ // ── Cell / slot interactions ───────────────────────────────────────────────
3392
+ onCellClick(day, hour, minute) {
3393
+ if (this.mode() !== 'admin')
3394
+ return;
3395
+ this.openAddModal(day, hour, minute);
3396
+ }
3397
+ onSlotMouseDown(e, slot) {
3398
+ e.stopPropagation();
3399
+ if (this.mode() === 'student' && !slot.booked) {
3400
+ this.slotBooked.emit({ ...slot, booked: true });
3401
+ return;
3402
+ }
3403
+ if (this.mode() !== 'admin')
3404
+ return;
3405
+ this.draggingSlot = slot;
3406
+ this.dragMoved = false;
3407
+ this.dragStartPos = { x: e.clientX, y: e.clientY };
3408
+ this.dragSlotDurationMin = (slot.end.getTime() - slot.start.getTime()) / 60000;
3409
+ }
3410
+ onDocMouseMove(e) {
3411
+ if (!this.draggingSlot || !this.dragStartPos)
3412
+ return;
3413
+ const dx = e.clientX - this.dragStartPos.x;
3414
+ const dy = e.clientY - this.dragStartPos.y;
3415
+ if (!this.dragMoved && Math.sqrt(dx * dx + dy * dy) > 6) {
3416
+ this.dragMoved = true;
3417
+ document.body.style.cursor = 'grabbing';
3418
+ document.body.style.userSelect = 'none';
3419
+ }
3420
+ if (this.dragMoved) {
3421
+ this.dragTarget = this.getGridTarget(e);
3422
+ }
3423
+ }
3424
+ onDocMouseUp(e) {
3425
+ if (!this.draggingSlot)
3426
+ return;
3427
+ if (this.dragMoved) {
3428
+ const target = this.getGridTarget(e);
3429
+ if (target) {
3430
+ const newStart = new Date(target.day);
3431
+ newStart.setHours(target.hour, target.minute, 0, 0);
3432
+ const newEnd = new Date(newStart.getTime() + this.dragSlotDurationMin * 60000);
3433
+ this.slotUpdated.emit({ ...this.draggingSlot, start: newStart, end: newEnd });
3434
+ }
3435
+ }
3436
+ else {
3437
+ this.openEditModal(this.draggingSlot);
3438
+ }
3439
+ this.draggingSlot = null;
3440
+ this.dragMoved = false;
3441
+ this.dragTarget = null;
3442
+ this.dragStartPos = null;
3443
+ document.body.style.cursor = '';
3444
+ document.body.style.userSelect = '';
3445
+ }
3446
+ previewTop(target) {
3447
+ const min = target.hour * 60 + target.minute;
3448
+ return ((min - this.minHour() * 60) / 30) * this.CELL_H;
3449
+ }
3450
+ previewHeight() {
3451
+ return Math.max((this.dragSlotDurationMin / 30) * this.CELL_H - 2, 24);
3452
+ }
3453
+ getGridTarget(e) {
3454
+ if (this.activeView() === 'month')
3455
+ return null;
3456
+ const scrollEl = this.elRef.nativeElement.querySelector('.ud-cal__week-scroll');
3457
+ const headerEl = this.elRef.nativeElement.querySelector('.ud-cal__week-header');
3458
+ const gridEl = this.elRef.nativeElement.querySelector('.ud-cal__week-grid');
3459
+ if (!scrollEl || !headerEl || !gridEl)
3460
+ return null;
3461
+ const scrollRect = scrollEl.getBoundingClientRect();
3462
+ const headerH = headerEl.getBoundingClientRect().height;
3463
+ const mouseYInGrid = (e.clientY - scrollRect.top) + scrollEl.scrollTop - headerH;
3464
+ if (mouseYInGrid < 0)
3465
+ return null;
3466
+ const slotIndex = Math.floor(mouseYInGrid / this.CELL_H);
3467
+ const ts = this.timeSlots();
3468
+ if (slotIndex >= ts.length)
3469
+ return null;
3470
+ const { hour, minute } = ts[slotIndex];
3471
+ const gridRect = gridEl.getBoundingClientRect();
3472
+ const days = this.activeView() === 'day' ? [this.navDate()] : this.weekDays();
3473
+ const totalDayW = gridRect.width - this.TIME_COL_W;
3474
+ const relX = e.clientX - gridRect.left - this.TIME_COL_W;
3475
+ if (relX < 0 || relX >= totalDayW)
3476
+ return null;
3477
+ const dayIndex = Math.min(days.length - 1, Math.floor(relX / (totalDayW / days.length)));
3478
+ return { day: new Date(days[dayIndex]), hour, minute };
3479
+ }
3480
+ // ── Modal open ─────────────────────────────────────────────────────────────
3481
+ openAddModal(day, hour, minute) {
3482
+ const base = day ? new Date(day) : new Date();
3483
+ const h = hour ?? base.getHours();
3484
+ const m = minute ?? 0;
3485
+ const startDate = new Date(base);
3486
+ startDate.setHours(h, m, 0, 0);
3487
+ const totalEnd = m + this.slotDuration();
3488
+ const endDate = new Date(base);
3489
+ endDate.setHours(h + Math.floor(totalEnd / 60), totalEnd % 60, 0, 0);
3490
+ const form = this.buildForm({
3491
+ date: new Date(base),
3492
+ startTime: this.formatTime(startDate),
3493
+ endTime: this.formatTime(endDate),
3494
+ });
3495
+ const ref = this.dialog.open(ModalComponent, {
3496
+ width: '480px',
3497
+ data: {
3498
+ eyebrow: 'Calendar',
3499
+ title: 'Add time slot',
3500
+ formGroup: form,
3501
+ forms: this.slotForms(),
3502
+ save: (v) => this.slotAdded.emit(this.buildSlot(crypto.randomUUID(), v)),
3503
+ },
3504
+ });
3505
+ form.get('startTime')?.valueChanges
3506
+ .pipe(takeUntil(ref.afterClosed()))
3507
+ .subscribe(timeStr => {
3508
+ if (!timeStr)
3509
+ return;
3510
+ form.get('endTime')?.setValue(this.shiftTimeStr(timeStr, this.slotDuration()));
3511
+ });
3512
+ }
3513
+ openEditModal(slot) {
3514
+ const form = this.buildForm({
3515
+ title: slot.title ?? '',
3516
+ date: new Date(slot.start),
3517
+ startTime: this.formatTime(slot.start),
3518
+ endTime: this.formatTime(slot.end),
3519
+ bookedBy: slot.bookedBy ?? '',
3520
+ booked: slot.booked ?? false,
3521
+ });
3522
+ const ref = this.dialog.open(ModalComponent, {
3523
+ width: '480px',
3524
+ data: {
3525
+ eyebrow: 'Calendar',
3526
+ title: 'Edit time slot',
3527
+ formGroup: form,
3528
+ forms: this.slotForms(),
3529
+ save: (v) => {
3530
+ const updated = this.buildSlot(slot.id, v);
3531
+ if (updated.booked && !slot.booked) {
3532
+ this.slotBooked.emit(updated);
3533
+ }
3534
+ else {
3535
+ this.slotUpdated.emit(updated);
3536
+ }
3537
+ },
3538
+ deleteLabel: 'Delete slot',
3539
+ delete: () => this.slotRemoved.emit(slot.id),
3540
+ },
3541
+ });
3542
+ form.get('startTime')?.valueChanges
3543
+ .pipe(takeUntil(ref.afterClosed()))
3544
+ .subscribe(timeStr => {
3545
+ if (!timeStr)
3546
+ return;
3547
+ form.get('endTime')?.setValue(this.shiftTimeStr(timeStr, this.slotDuration()));
3548
+ });
3549
+ }
3550
+ // ── Helpers ────────────────────────────────────────────────────────────────
3551
+ buildForm(defaults) {
3552
+ return new FormGroup({
3553
+ title: new FormControl(defaults.title ?? ''),
3554
+ date: new FormControl(defaults.date ?? null, Validators.required),
3555
+ startTime: new FormControl(defaults.startTime ?? null, Validators.required),
3556
+ endTime: new FormControl(defaults.endTime ?? null, Validators.required),
3557
+ bookedBy: new FormControl(defaults.bookedBy ?? ''),
3558
+ booked: new FormControl(defaults.booked ?? false),
3559
+ });
3560
+ }
3561
+ slotForms() {
3562
+ return [
3563
+ { type: ModalInputType.INPUT, title: 'Title', property: 'title', placeholder: 'e.g. Office hours' },
3564
+ { type: ModalInputType.DATETIME, title: 'Date', property: 'date' },
3565
+ { type: ModalInputType.TIME, title: 'Start Time', property: 'startTime', intervalMinutes: this.slotDuration() },
3566
+ { type: ModalInputType.TIME, title: 'End Time', property: 'endTime', intervalMinutes: this.slotDuration() },
3567
+ { type: ModalInputType.INPUT, title: 'Booked By', property: 'bookedBy', placeholder: 'Student name or ID' },
3568
+ {
3569
+ type: ModalInputType.OPTIONS,
3570
+ title: 'Status',
3571
+ property: 'booked',
3572
+ availableOptions: [
3573
+ { value: false, label: 'Available' },
3574
+ { value: true, label: 'Booked' },
3575
+ ],
3576
+ },
3577
+ ];
3578
+ }
3579
+ buildSlot(id, v) {
3580
+ const start = this.applyTimeStr(new Date(v.date), v.startTime);
3581
+ const end = this.applyTimeStr(new Date(v.date), v.endTime);
3582
+ return {
3583
+ id,
3584
+ title: v.title || undefined,
3585
+ start,
3586
+ end,
3587
+ booked: !!v.booked,
3588
+ bookedBy: v.bookedBy || undefined,
3589
+ };
3590
+ }
3591
+ shiftTimeStr(timeStr, minutes) {
3592
+ const m = timeStr?.match(/(\d+):(\d+)\s*(AM|PM)/i);
3593
+ if (!m)
3594
+ return timeStr;
3595
+ let h = parseInt(m[1], 10);
3596
+ const min = parseInt(m[2], 10);
3597
+ if (m[3].toUpperCase() === 'PM' && h !== 12)
3598
+ h += 12;
3599
+ if (m[3].toUpperCase() === 'AM' && h === 12)
3600
+ h = 0;
3601
+ const total = h * 60 + min + minutes;
3602
+ const d = new Date();
3603
+ d.setHours(Math.floor(total / 60) % 24, total % 60, 0, 0);
3604
+ return this.formatTime(d);
3605
+ }
3606
+ applyTimeStr(base, timeStr) {
3607
+ const m = timeStr?.match(/(\d+):(\d+)\s*(AM|PM)/i);
3608
+ if (m) {
3609
+ let h = parseInt(m[1], 10);
3610
+ const min = parseInt(m[2], 10);
3611
+ if (m[3].toUpperCase() === 'PM' && h !== 12)
3612
+ h += 12;
3613
+ if (m[3].toUpperCase() === 'AM' && h === 12)
3614
+ h = 0;
3615
+ base.setHours(h, min, 0, 0);
3616
+ }
3617
+ return base;
3618
+ }
3619
+ getMonday(d) {
3620
+ const day = new Date(d);
3621
+ const dow = day.getDay();
3622
+ const diff = dow === 0 ? -6 : 1 - dow;
3623
+ day.setDate(day.getDate() + diff);
3624
+ day.setHours(0, 0, 0, 0);
3625
+ return day;
3626
+ }
3627
+ isSameDay(a, b) {
3628
+ return a.getFullYear() === b.getFullYear() &&
3629
+ a.getMonth() === b.getMonth() &&
3630
+ a.getDate() === b.getDate();
3631
+ }
3632
+ formatHour(h, m) {
3633
+ const period = h < 12 ? 'am' : 'pm';
3634
+ const hour = h % 12 || 12;
3635
+ return m === 0 ? `${hour}${period}` : `${hour}:${m.toString().padStart(2, '0')}`;
3636
+ }
3637
+ formatTime(d) {
3638
+ return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
3639
+ }
3640
+ hexToRgba(hex, alpha) {
3641
+ const r = parseInt(hex.slice(1, 3), 16);
3642
+ const g = parseInt(hex.slice(3, 5), 16);
3643
+ const b = parseInt(hex.slice(5, 7), 16);
3644
+ return `rgba(${r},${g},${b},${alpha})`;
3645
+ }
3646
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: CalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3647
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.19", type: CalendarComponent, isStandalone: true, selector: "ud-calendar", inputs: { slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, defaultView: { classPropertyName: "defaultView", publicName: "defaultView", isSignal: true, isRequired: false, transformFunction: null }, slotDuration: { classPropertyName: "slotDuration", publicName: "slotDuration", isSignal: true, isRequired: false, transformFunction: null }, minHour: { classPropertyName: "minHour", publicName: "minHour", isSignal: true, isRequired: false, transformFunction: null }, maxHour: { classPropertyName: "maxHour", publicName: "maxHour", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { slotAdded: "slotAdded", slotUpdated: "slotUpdated", slotRemoved: "slotRemoved", slotBooked: "slotBooked" }, host: { listeners: { "document:mousemove": "onDocMouseMove($event)", "document:mouseup": "onDocMouseUp($event)" } }, ngImport: i0, template: "<div class=\"ud-cal\" #calendarHost>\n\n <!-- Header -->\n <div class=\"ud-cal__header\">\n <div class=\"ud-cal__nav\">\n <ud-button variant=\"icon-only\" color=\"secondary\" size=\"sm\" icon=\"chevron_left\" (click)=\"navigate(-1)\" />\n <ud-button variant=\"icon-only\" color=\"secondary\" size=\"sm\" icon=\"chevron_right\" (click)=\"navigate(1)\" />\n <ud-button variant=\"stroked\" color=\"secondary\" size=\"sm\" (click)=\"goToday()\">Today</ud-button>\n </div>\n\n <span class=\"ud-cal__period\">{{ headerLabel() }}</span>\n\n <div class=\"ud-cal__header-right\">\n <div class=\"ud-cal__view-switcher\">\n @for (v of viewOptions; track v.id) {\n <button\n class=\"ud-cal__view-btn\"\n [class.ud-cal__view-btn--active]=\"activeView() === v.id\"\n (click)=\"switchView(v.id)\"\n type=\"button\">\n {{ v.label }}\n </button>\n }\n </div>\n @if (mode() === 'admin') {\n <ud-button variant=\"flat\" color=\"primary\" size=\"sm\" icon=\"add\" (click)=\"openAddModal()\">\n Add slot\n </ud-button>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 WEEK VIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeView() === 'week') {\n <div class=\"ud-cal__week-scroll\">\n <div class=\"ud-cal__week-header\">\n <div class=\"ud-cal__time-gutter\"></div>\n @for (day of weekDays(); track day.toISOString()) {\n <div class=\"ud-cal__day-header\" [class.ud-cal__day-header--today]=\"isToday(day)\">\n <span class=\"ud-cal__day-name\">{{ dayLabel(day) }}</span>\n <span class=\"ud-cal__day-num\" [class.ud-cal__day-num--today]=\"isToday(day)\">{{ dayNum(day) }}</span>\n </div>\n }\n </div>\n <div class=\"ud-cal__week-grid\">\n <div class=\"ud-cal__time-col\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div class=\"ud-cal__time-cell\">\n @if (ts.label) {\n <span class=\"ud-cal__time-label\">{{ ts.label }}</span>\n }\n </div>\n }\n </div>\n @for (day of weekDays(); track day.toISOString()) {\n <div class=\"ud-cal__day-col\" [class.ud-cal__day-col--today]=\"isToday(day)\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div\n class=\"ud-cal__grid-cell\"\n [class.ud-cal__grid-cell--half]=\"ts.minute === 30\"\n (click)=\"onCellClick(day, ts.hour, ts.minute)\">\n </div>\n }\n @for (slot of slotsForDay(day); track slot.id) {\n <div\n class=\"ud-cal__slot\"\n [class.ud-cal__slot--booked]=\"slot.booked\"\n [class.ud-cal__slot--clickable]=\"mode() === 'admin' || (mode() === 'student' && !slot.booked)\"\n [class.ud-cal__slot--dragging]=\"draggingSlot?.id === slot.id && dragMoved\"\n [style.top.px]=\"slotTop(slot)\"\n [style.height.px]=\"slotHeight(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">lock</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n }\n </div>\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n @if (slot.booked && slot.bookedBy) {\n <span class=\"ud-cal__slot-booked-by\">{{ slot.bookedBy }}</span>\n }\n </div>\n }\n @if (draggingSlot && dragMoved && dragTarget && isSameDay(dragTarget.day, day)) {\n <div\n class=\"ud-cal__slot ud-cal__slot--drag-preview\"\n [style.top.px]=\"previewTop(dragTarget)\"\n [style.height.px]=\"previewHeight()\"\n [style.background]=\"slotBg(draggingSlot)\"\n [style.color]=\"slotTextColor(draggingSlot)\"\n [style.border-color]=\"slotBorderColor(draggingSlot)\">\n @if (draggingSlot.title) {\n <span class=\"ud-cal__slot-title\">{{ draggingSlot.title }}</span>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 MONTH VIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeView() === 'month') {\n <div class=\"ud-cal__month\">\n <div class=\"ud-cal__month-header\">\n @for (name of ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; track name) {\n <div class=\"ud-cal__month-day-name\">{{ name }}</div>\n }\n </div>\n <div class=\"ud-cal__month-grid\">\n @for (week of monthWeeks(); track $index) {\n @for (day of week; track day.toISOString()) {\n <div\n class=\"ud-cal__month-cell\"\n [class.ud-cal__month-cell--today]=\"isToday(day)\"\n [class.ud-cal__month-cell--other]=\"!isCurrentMonth(day)\"\n (click)=\"clickDay(day)\">\n <span class=\"ud-cal__month-num\" [class.ud-cal__month-num--today]=\"isToday(day)\">\n {{ dayNum(day) }}\n </span>\n <div class=\"ud-cal__month-dots\">\n @for (slot of slotsForDay(day).slice(0, 3); track slot.id) {\n <span\n class=\"ud-cal__month-dot\"\n [style.background]=\"slotBorderColor(slot)\"\n [title]=\"slot.title ?? formatSlotTime(slot)\">\n </span>\n }\n @if (slotsForDay(day).length > 3) {\n <span class=\"ud-cal__month-more\">+{{ slotsForDay(day).length - 3 }}</span>\n }\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 DAY VIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeView() === 'day') {\n <div class=\"ud-cal__week-scroll\">\n <div class=\"ud-cal__week-header\">\n <div class=\"ud-cal__time-gutter\"></div>\n <div class=\"ud-cal__day-header\" [class.ud-cal__day-header--today]=\"isToday(navDate())\">\n <span class=\"ud-cal__day-name\">{{ dayLabel(navDate()) }}</span>\n <span class=\"ud-cal__day-num\" [class.ud-cal__day-num--today]=\"isToday(navDate())\">{{ dayNum(navDate()) }}</span>\n </div>\n </div>\n <div class=\"ud-cal__week-grid\">\n <div class=\"ud-cal__time-col\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div class=\"ud-cal__time-cell\">\n @if (ts.label) {\n <span class=\"ud-cal__time-label\">{{ ts.label }}</span>\n }\n </div>\n }\n </div>\n <div class=\"ud-cal__day-col\" [class.ud-cal__day-col--today]=\"isToday(navDate())\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div\n class=\"ud-cal__grid-cell\"\n [class.ud-cal__grid-cell--half]=\"ts.minute === 30\"\n (click)=\"onCellClick(navDate(), ts.hour, ts.minute)\">\n </div>\n }\n @for (slot of slotsForDay(navDate()); track slot.id) {\n <div\n class=\"ud-cal__slot\"\n [class.ud-cal__slot--booked]=\"slot.booked\"\n [class.ud-cal__slot--clickable]=\"mode() === 'admin' || (mode() === 'student' && !slot.booked)\"\n [class.ud-cal__slot--dragging]=\"draggingSlot?.id === slot.id && dragMoved\"\n [style.top.px]=\"slotTop(slot)\"\n [style.height.px]=\"slotHeight(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">lock</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n }\n </div>\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n @if (slot.booked && slot.bookedBy) {\n <span class=\"ud-cal__slot-booked-by\">{{ slot.bookedBy }}</span>\n }\n </div>\n }\n @if (draggingSlot && dragMoved && dragTarget) {\n <div\n class=\"ud-cal__slot ud-cal__slot--drag-preview\"\n [style.top.px]=\"previewTop(dragTarget)\"\n [style.height.px]=\"previewHeight()\"\n [style.background]=\"slotBg(draggingSlot)\"\n [style.color]=\"slotTextColor(draggingSlot)\"\n [style.border-color]=\"slotBorderColor(draggingSlot)\">\n @if (draggingSlot.title) {\n <span class=\"ud-cal__slot-title\">{{ draggingSlot.title }}</span>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n</div>\n\n", styles: [":host{display:block;width:100%;font-family:DM Sans,system-ui,sans-serif}.ud-cal{position:relative;background:#fff;border:1px solid var(--eu-border-mid, #d8dde6);border-radius:12px;box-shadow:0 2px 8px #1b25350f;overflow:hidden}.ud-cal__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--eu-border-light, #e8eaef);gap:12px;flex-wrap:wrap}.ud-cal__nav{display:flex;align-items:center;gap:4px}.ud-cal__period{flex:1;text-align:center;font-size:14px;font-weight:600;color:var(--eu-text, #2a3548);white-space:nowrap}.ud-cal__header-right{display:flex;align-items:center;gap:10px}.ud-cal__view-switcher{display:flex;align-items:center;background:var(--eu-bg, #f4f5f7);border-radius:8px;padding:3px;gap:2px}.ud-cal__view-btn{padding:4px 12px;border:none;border-radius:6px;background:transparent;font-family:DM Sans,system-ui,sans-serif;font-size:12px;font-weight:500;color:var(--eu-muted, #6b7585);cursor:pointer;transition:background .15s,color .15s}.ud-cal__view-btn--active{background:var(--eu-navy, #1b2535);color:#fff}.ud-cal__view-btn:not(.ud-cal__view-btn--active):hover{background:#1b253514;color:var(--eu-text, #2a3548)}.ud-cal__week-scroll{overflow-y:auto;max-height:580px}.ud-cal__week-header{position:sticky;top:0;z-index:10;display:flex;background:#fafbfc;border-bottom:1px solid var(--eu-border-light, #e8eaef);flex-shrink:0}.ud-cal__time-gutter{width:52px;flex-shrink:0;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__day-header{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px 4px;gap:2px;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__day-header:last-child{border-right:none}.ud-cal__day-header--today{background:#1b25350a}.ud-cal__day-name{font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);text-transform:uppercase;letter-spacing:.06em}.ud-cal__day-num{font-size:15px;font-weight:600;color:var(--eu-text, #2a3548)}.ud-cal__day-num--today{display:flex;align-items:center;justify-content:center;width:26px;height:26px;background:var(--eu-navy, #1b2535);color:#fff;border-radius:50%;font-size:13px}.ud-cal__week-grid{display:flex}.ud-cal__time-col{width:52px;flex-shrink:0;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__time-cell{height:56px;position:relative;display:flex;align-items:flex-start;justify-content:flex-end;padding:0 6px}.ud-cal__time-label{font-size:10px;color:var(--eu-muted, #9099a8);font-weight:500;white-space:nowrap;margin-top:-6px}.ud-cal__day-col{flex:1;position:relative;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__day-col:last-child{border-right:none}.ud-cal__day-col--today{background:#1b253506}.ud-cal__grid-cell{height:56px;border-bottom:1px solid var(--eu-border-light, #e8eaef);transition:background .1s;cursor:pointer}.ud-cal__grid-cell--half{border-bottom-style:dashed;border-bottom-color:#d8dde673}.ud-cal__grid-cell:hover{background:#1b253508}.ud-cal__slot{position:absolute;left:3px;right:3px;border-left:3px solid;border-radius:6px;padding:3px 6px;display:flex;flex-direction:column;gap:1px;overflow:hidden;z-index:1;transition:filter .15s,transform .1s}.ud-cal__slot--clickable{cursor:grab}.ud-cal__slot--clickable:hover{filter:brightness(.93);transform:translate(1px)}.ud-cal__slot-inner{display:flex;align-items:center;gap:3px;overflow:hidden;min-width:0}.ud-cal__slot-title{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2;min-width:0}.ud-cal__slot-time{font-size:10px;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.ud-cal__slot--dragging{opacity:.3;pointer-events:none}.ud-cal__slot--drag-preview{pointer-events:none;opacity:.75;border-left-style:dashed;border-top:1px dashed currentColor;z-index:2}.ud-cal__slot--booked{background-image:repeating-linear-gradient(-45deg,transparent,transparent 4px,rgba(229,57,53,.07) 4px,rgba(229,57,53,.07) 8px)!important;cursor:not-allowed;border-top:1px solid rgba(229,57,53,.18)}.ud-cal__slot--booked:hover{filter:none!important;transform:none!important}.ud-cal__slot--booked .ud-cal__slot-title{text-decoration:line-through;text-decoration-color:#c6282866;text-decoration-thickness:1px;opacity:.9}.ud-cal__slot-lock{font-size:10px!important;width:10px!important;height:10px!important;line-height:10px!important;flex-shrink:0;opacity:.7}.ud-cal__slot-booked-by{font-size:9px;font-weight:500;opacity:.6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.01em;margin-top:1px}.ud-cal__month{display:flex;flex-direction:column}.ud-cal__month-header{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--eu-border-light, #e8eaef);background:#fafbfc}.ud-cal__month-day-name{text-align:center;padding:8px 4px;font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);text-transform:uppercase;letter-spacing:.06em;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__month-day-name:last-child{border-right:none}.ud-cal__month-grid{display:grid;grid-template-columns:repeat(7,1fr)}.ud-cal__month-cell{min-height:80px;padding:6px;border-right:1px solid var(--eu-border-light, #e8eaef);border-bottom:1px solid var(--eu-border-light, #e8eaef);cursor:pointer;transition:background .12s}.ud-cal__month-cell:nth-child(7n){border-right:none}.ud-cal__month-cell:hover{background:#1b253508}.ud-cal__month-cell--today{background:#1b25350a}.ud-cal__month-cell--other{opacity:.4}.ud-cal__month-num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;font-size:12px;font-weight:600;color:var(--eu-text, #2a3548);border-radius:50%}.ud-cal__month-num--today{background:var(--eu-navy, #1b2535);color:#fff}.ud-cal__month-dots{display:flex;flex-wrap:wrap;align-items:center;gap:3px;margin-top:4px}.ud-cal__month-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.ud-cal__month-more{font-size:9px;font-weight:600;color:var(--eu-muted, #6b7585)}\n"], dependencies: [{ kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: UdButtonComponent, selector: "ud-button", inputs: ["variant", "color", "size", "type", "icon", "iconPosition", "iconFontSet", "loading", "disabled", "fullWidth"] }] });
3648
+ }
3649
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImport: i0, type: CalendarComponent, decorators: [{
3650
+ type: Component,
3651
+ args: [{ selector: 'ud-calendar', standalone: true, imports: [MatIcon, UdButtonComponent], template: "<div class=\"ud-cal\" #calendarHost>\n\n <!-- Header -->\n <div class=\"ud-cal__header\">\n <div class=\"ud-cal__nav\">\n <ud-button variant=\"icon-only\" color=\"secondary\" size=\"sm\" icon=\"chevron_left\" (click)=\"navigate(-1)\" />\n <ud-button variant=\"icon-only\" color=\"secondary\" size=\"sm\" icon=\"chevron_right\" (click)=\"navigate(1)\" />\n <ud-button variant=\"stroked\" color=\"secondary\" size=\"sm\" (click)=\"goToday()\">Today</ud-button>\n </div>\n\n <span class=\"ud-cal__period\">{{ headerLabel() }}</span>\n\n <div class=\"ud-cal__header-right\">\n <div class=\"ud-cal__view-switcher\">\n @for (v of viewOptions; track v.id) {\n <button\n class=\"ud-cal__view-btn\"\n [class.ud-cal__view-btn--active]=\"activeView() === v.id\"\n (click)=\"switchView(v.id)\"\n type=\"button\">\n {{ v.label }}\n </button>\n }\n </div>\n @if (mode() === 'admin') {\n <ud-button variant=\"flat\" color=\"primary\" size=\"sm\" icon=\"add\" (click)=\"openAddModal()\">\n Add slot\n </ud-button>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 WEEK VIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeView() === 'week') {\n <div class=\"ud-cal__week-scroll\">\n <div class=\"ud-cal__week-header\">\n <div class=\"ud-cal__time-gutter\"></div>\n @for (day of weekDays(); track day.toISOString()) {\n <div class=\"ud-cal__day-header\" [class.ud-cal__day-header--today]=\"isToday(day)\">\n <span class=\"ud-cal__day-name\">{{ dayLabel(day) }}</span>\n <span class=\"ud-cal__day-num\" [class.ud-cal__day-num--today]=\"isToday(day)\">{{ dayNum(day) }}</span>\n </div>\n }\n </div>\n <div class=\"ud-cal__week-grid\">\n <div class=\"ud-cal__time-col\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div class=\"ud-cal__time-cell\">\n @if (ts.label) {\n <span class=\"ud-cal__time-label\">{{ ts.label }}</span>\n }\n </div>\n }\n </div>\n @for (day of weekDays(); track day.toISOString()) {\n <div class=\"ud-cal__day-col\" [class.ud-cal__day-col--today]=\"isToday(day)\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div\n class=\"ud-cal__grid-cell\"\n [class.ud-cal__grid-cell--half]=\"ts.minute === 30\"\n (click)=\"onCellClick(day, ts.hour, ts.minute)\">\n </div>\n }\n @for (slot of slotsForDay(day); track slot.id) {\n <div\n class=\"ud-cal__slot\"\n [class.ud-cal__slot--booked]=\"slot.booked\"\n [class.ud-cal__slot--clickable]=\"mode() === 'admin' || (mode() === 'student' && !slot.booked)\"\n [class.ud-cal__slot--dragging]=\"draggingSlot?.id === slot.id && dragMoved\"\n [style.top.px]=\"slotTop(slot)\"\n [style.height.px]=\"slotHeight(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">lock</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n }\n </div>\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n @if (slot.booked && slot.bookedBy) {\n <span class=\"ud-cal__slot-booked-by\">{{ slot.bookedBy }}</span>\n }\n </div>\n }\n @if (draggingSlot && dragMoved && dragTarget && isSameDay(dragTarget.day, day)) {\n <div\n class=\"ud-cal__slot ud-cal__slot--drag-preview\"\n [style.top.px]=\"previewTop(dragTarget)\"\n [style.height.px]=\"previewHeight()\"\n [style.background]=\"slotBg(draggingSlot)\"\n [style.color]=\"slotTextColor(draggingSlot)\"\n [style.border-color]=\"slotBorderColor(draggingSlot)\">\n @if (draggingSlot.title) {\n <span class=\"ud-cal__slot-title\">{{ draggingSlot.title }}</span>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 MONTH VIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeView() === 'month') {\n <div class=\"ud-cal__month\">\n <div class=\"ud-cal__month-header\">\n @for (name of ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; track name) {\n <div class=\"ud-cal__month-day-name\">{{ name }}</div>\n }\n </div>\n <div class=\"ud-cal__month-grid\">\n @for (week of monthWeeks(); track $index) {\n @for (day of week; track day.toISOString()) {\n <div\n class=\"ud-cal__month-cell\"\n [class.ud-cal__month-cell--today]=\"isToday(day)\"\n [class.ud-cal__month-cell--other]=\"!isCurrentMonth(day)\"\n (click)=\"clickDay(day)\">\n <span class=\"ud-cal__month-num\" [class.ud-cal__month-num--today]=\"isToday(day)\">\n {{ dayNum(day) }}\n </span>\n <div class=\"ud-cal__month-dots\">\n @for (slot of slotsForDay(day).slice(0, 3); track slot.id) {\n <span\n class=\"ud-cal__month-dot\"\n [style.background]=\"slotBorderColor(slot)\"\n [title]=\"slot.title ?? formatSlotTime(slot)\">\n </span>\n }\n @if (slotsForDay(day).length > 3) {\n <span class=\"ud-cal__month-more\">+{{ slotsForDay(day).length - 3 }}</span>\n }\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n\n <!-- \u2500\u2500 DAY VIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (activeView() === 'day') {\n <div class=\"ud-cal__week-scroll\">\n <div class=\"ud-cal__week-header\">\n <div class=\"ud-cal__time-gutter\"></div>\n <div class=\"ud-cal__day-header\" [class.ud-cal__day-header--today]=\"isToday(navDate())\">\n <span class=\"ud-cal__day-name\">{{ dayLabel(navDate()) }}</span>\n <span class=\"ud-cal__day-num\" [class.ud-cal__day-num--today]=\"isToday(navDate())\">{{ dayNum(navDate()) }}</span>\n </div>\n </div>\n <div class=\"ud-cal__week-grid\">\n <div class=\"ud-cal__time-col\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div class=\"ud-cal__time-cell\">\n @if (ts.label) {\n <span class=\"ud-cal__time-label\">{{ ts.label }}</span>\n }\n </div>\n }\n </div>\n <div class=\"ud-cal__day-col\" [class.ud-cal__day-col--today]=\"isToday(navDate())\">\n @for (ts of timeSlots(); track ts.hour + ':' + ts.minute) {\n <div\n class=\"ud-cal__grid-cell\"\n [class.ud-cal__grid-cell--half]=\"ts.minute === 30\"\n (click)=\"onCellClick(navDate(), ts.hour, ts.minute)\">\n </div>\n }\n @for (slot of slotsForDay(navDate()); track slot.id) {\n <div\n class=\"ud-cal__slot\"\n [class.ud-cal__slot--booked]=\"slot.booked\"\n [class.ud-cal__slot--clickable]=\"mode() === 'admin' || (mode() === 'student' && !slot.booked)\"\n [class.ud-cal__slot--dragging]=\"draggingSlot?.id === slot.id && dragMoved\"\n [style.top.px]=\"slotTop(slot)\"\n [style.height.px]=\"slotHeight(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">lock</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n }\n </div>\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n @if (slot.booked && slot.bookedBy) {\n <span class=\"ud-cal__slot-booked-by\">{{ slot.bookedBy }}</span>\n }\n </div>\n }\n @if (draggingSlot && dragMoved && dragTarget) {\n <div\n class=\"ud-cal__slot ud-cal__slot--drag-preview\"\n [style.top.px]=\"previewTop(dragTarget)\"\n [style.height.px]=\"previewHeight()\"\n [style.background]=\"slotBg(draggingSlot)\"\n [style.color]=\"slotTextColor(draggingSlot)\"\n [style.border-color]=\"slotBorderColor(draggingSlot)\">\n @if (draggingSlot.title) {\n <span class=\"ud-cal__slot-title\">{{ draggingSlot.title }}</span>\n }\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n</div>\n\n", styles: [":host{display:block;width:100%;font-family:DM Sans,system-ui,sans-serif}.ud-cal{position:relative;background:#fff;border:1px solid var(--eu-border-mid, #d8dde6);border-radius:12px;box-shadow:0 2px 8px #1b25350f;overflow:hidden}.ud-cal__header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--eu-border-light, #e8eaef);gap:12px;flex-wrap:wrap}.ud-cal__nav{display:flex;align-items:center;gap:4px}.ud-cal__period{flex:1;text-align:center;font-size:14px;font-weight:600;color:var(--eu-text, #2a3548);white-space:nowrap}.ud-cal__header-right{display:flex;align-items:center;gap:10px}.ud-cal__view-switcher{display:flex;align-items:center;background:var(--eu-bg, #f4f5f7);border-radius:8px;padding:3px;gap:2px}.ud-cal__view-btn{padding:4px 12px;border:none;border-radius:6px;background:transparent;font-family:DM Sans,system-ui,sans-serif;font-size:12px;font-weight:500;color:var(--eu-muted, #6b7585);cursor:pointer;transition:background .15s,color .15s}.ud-cal__view-btn--active{background:var(--eu-navy, #1b2535);color:#fff}.ud-cal__view-btn:not(.ud-cal__view-btn--active):hover{background:#1b253514;color:var(--eu-text, #2a3548)}.ud-cal__week-scroll{overflow-y:auto;max-height:580px}.ud-cal__week-header{position:sticky;top:0;z-index:10;display:flex;background:#fafbfc;border-bottom:1px solid var(--eu-border-light, #e8eaef);flex-shrink:0}.ud-cal__time-gutter{width:52px;flex-shrink:0;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__day-header{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:8px 4px;gap:2px;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__day-header:last-child{border-right:none}.ud-cal__day-header--today{background:#1b25350a}.ud-cal__day-name{font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);text-transform:uppercase;letter-spacing:.06em}.ud-cal__day-num{font-size:15px;font-weight:600;color:var(--eu-text, #2a3548)}.ud-cal__day-num--today{display:flex;align-items:center;justify-content:center;width:26px;height:26px;background:var(--eu-navy, #1b2535);color:#fff;border-radius:50%;font-size:13px}.ud-cal__week-grid{display:flex}.ud-cal__time-col{width:52px;flex-shrink:0;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__time-cell{height:56px;position:relative;display:flex;align-items:flex-start;justify-content:flex-end;padding:0 6px}.ud-cal__time-label{font-size:10px;color:var(--eu-muted, #9099a8);font-weight:500;white-space:nowrap;margin-top:-6px}.ud-cal__day-col{flex:1;position:relative;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__day-col:last-child{border-right:none}.ud-cal__day-col--today{background:#1b253506}.ud-cal__grid-cell{height:56px;border-bottom:1px solid var(--eu-border-light, #e8eaef);transition:background .1s;cursor:pointer}.ud-cal__grid-cell--half{border-bottom-style:dashed;border-bottom-color:#d8dde673}.ud-cal__grid-cell:hover{background:#1b253508}.ud-cal__slot{position:absolute;left:3px;right:3px;border-left:3px solid;border-radius:6px;padding:3px 6px;display:flex;flex-direction:column;gap:1px;overflow:hidden;z-index:1;transition:filter .15s,transform .1s}.ud-cal__slot--clickable{cursor:grab}.ud-cal__slot--clickable:hover{filter:brightness(.93);transform:translate(1px)}.ud-cal__slot-inner{display:flex;align-items:center;gap:3px;overflow:hidden;min-width:0}.ud-cal__slot-title{font-size:11px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2;min-width:0}.ud-cal__slot-time{font-size:10px;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.ud-cal__slot--dragging{opacity:.3;pointer-events:none}.ud-cal__slot--drag-preview{pointer-events:none;opacity:.75;border-left-style:dashed;border-top:1px dashed currentColor;z-index:2}.ud-cal__slot--booked{background-image:repeating-linear-gradient(-45deg,transparent,transparent 4px,rgba(229,57,53,.07) 4px,rgba(229,57,53,.07) 8px)!important;cursor:not-allowed;border-top:1px solid rgba(229,57,53,.18)}.ud-cal__slot--booked:hover{filter:none!important;transform:none!important}.ud-cal__slot--booked .ud-cal__slot-title{text-decoration:line-through;text-decoration-color:#c6282866;text-decoration-thickness:1px;opacity:.9}.ud-cal__slot-lock{font-size:10px!important;width:10px!important;height:10px!important;line-height:10px!important;flex-shrink:0;opacity:.7}.ud-cal__slot-booked-by{font-size:9px;font-weight:500;opacity:.6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.01em;margin-top:1px}.ud-cal__month{display:flex;flex-direction:column}.ud-cal__month-header{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--eu-border-light, #e8eaef);background:#fafbfc}.ud-cal__month-day-name{text-align:center;padding:8px 4px;font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);text-transform:uppercase;letter-spacing:.06em;border-right:1px solid var(--eu-border-light, #e8eaef)}.ud-cal__month-day-name:last-child{border-right:none}.ud-cal__month-grid{display:grid;grid-template-columns:repeat(7,1fr)}.ud-cal__month-cell{min-height:80px;padding:6px;border-right:1px solid var(--eu-border-light, #e8eaef);border-bottom:1px solid var(--eu-border-light, #e8eaef);cursor:pointer;transition:background .12s}.ud-cal__month-cell:nth-child(7n){border-right:none}.ud-cal__month-cell:hover{background:#1b253508}.ud-cal__month-cell--today{background:#1b25350a}.ud-cal__month-cell--other{opacity:.4}.ud-cal__month-num{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;font-size:12px;font-weight:600;color:var(--eu-text, #2a3548);border-radius:50%}.ud-cal__month-num--today{background:var(--eu-navy, #1b2535);color:#fff}.ud-cal__month-dots{display:flex;flex-wrap:wrap;align-items:center;gap:3px;margin-top:4px}.ud-cal__month-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.ud-cal__month-more{font-size:9px;font-weight:600;color:var(--eu-muted, #6b7585)}\n"] }]
3652
+ }], ctorParameters: () => [], propDecorators: { onDocMouseMove: [{
3653
+ type: HostListener,
3654
+ args: ['document:mousemove', ['$event']]
3655
+ }], onDocMouseUp: [{
3656
+ type: HostListener,
3657
+ args: ['document:mouseup', ['$event']]
3658
+ }] } });
3659
+
3123
3660
  /**
3124
3661
  * Segmented button toggle group. Works with [(ngModel)] and reactive forms.
3125
3662
  *
@@ -3408,5 +3945,5 @@ const generateTimeOptions = (start, end, intervalMinutes = 5) => {
3408
3945
  * Generated bundle index. Do not edit.
3409
3946
  */
3410
3947
 
3411
- export { ApplicationStatus, AutocompleteComponent, CapitalizePipe, CarouselComponent, CustomInputComponent, CustomSnackbarComponent, CustomTableComponent, DateInputComponent, DateOperator, DateRangeInputComponent, DynamicComponentComponent, EditViewComponent, EditViewSectionDirective, FeatureFlagKey, FileInputComponent, FilterType, IconColor, KpiComponent, KpiDataType, KpiPillType, KpiProgressBarType, KpiVariant, LoadingStatus, ModalComponent, ModalInputType, MultiSelectComponent, NumberOperator, PhoneInputComponent, PillComponent, PillToggleComponent, PluralizePipe, ProgressBarComponent, SafePipe, SingularPipe, SnackbarType, StringOperator, SummaryViewComponent, TableDisplayColumnType, TabsComponent, TelInputComponent, TextInputComponent, TextareaComponent, TimePickerComponent, ToObservablePipe, ToggleComponent, ToggleOptionComponent, TranslateWrapperService, UdButtonComponent, UdButtonToggleComponent, UdPreviewContainerComponent, UdStepContentDirective, UdStepperComponent, capitalize, formatLocalDate, formatLocalDateTime, formatLocalDateTimeLongForm, formatLocalTime, formatLocalTimeWithMinutes, formatLocalTimeWithMinutesAmPm, formatMonthYear, formatPhoneNumber, formatStringDate, formatStringDateTime, generateTimeOptions, inListValidator, parseLocalDate, pluralize, spaceCase, updateArray, withLoadingState };
3948
+ export { ApplicationStatus, AutocompleteComponent, CalendarComponent, CapitalizePipe, CarouselComponent, ChipInputComponent, CustomInputComponent, CustomSnackbarComponent, CustomTableComponent, DateInputComponent, DateOperator, DateRangeInputComponent, DynamicComponentComponent, EditViewComponent, EditViewSectionDirective, FeatureFlagKey, FileInputComponent, FilterType, IconColor, KpiComponent, KpiDataType, KpiPillType, KpiProgressBarType, KpiVariant, LoadingStatus, ModalComponent, ModalInputType, MultiSelectComponent, NumberOperator, PhoneInputComponent, PillComponent, PillToggleComponent, PluralizePipe, ProgressBarComponent, SafePipe, SingularPipe, SnackbarType, StringOperator, SummaryViewComponent, TableDisplayColumnType, TabsComponent, TelInputComponent, TextInputComponent, TextareaComponent, TimePickerComponent, ToObservablePipe, ToggleComponent, ToggleOptionComponent, TranslateWrapperService, UdButtonComponent, UdButtonToggleComponent, UdPreviewContainerComponent, UdStepContentDirective, UdStepperComponent, capitalize, formatLocalDate, formatLocalDateTime, formatLocalDateTimeLongForm, formatLocalTime, formatLocalTimeWithMinutes, formatLocalTimeWithMinutesAmPm, formatMonthYear, formatPhoneNumber, formatStringDate, formatStringDateTime, generateTimeOptions, inListValidator, parseLocalDate, pluralize, spaceCase, updateArray, withLoadingState };
3412
3949
  //# sourceMappingURL=ud-components.mjs.map