ud-components 0.5.21 → 0.5.23
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.
|
@@ -2543,6 +2543,12 @@ class ModalComponent {
|
|
|
2543
2543
|
this.bodyText = this.data.bodyText;
|
|
2544
2544
|
if (this.data.showClose != undefined)
|
|
2545
2545
|
this.showClose = this.data.showClose;
|
|
2546
|
+
// Let dialog-mode consumers override the action labels too — needed for
|
|
2547
|
+
// contextual flows (e.g. "Reschedule" instead of the default "Save").
|
|
2548
|
+
if (this.data.confirmLabel != undefined)
|
|
2549
|
+
this.confirmLabel = this.data.confirmLabel;
|
|
2550
|
+
if (this.data.cancelLabel != undefined)
|
|
2551
|
+
this.cancelLabel = this.data.cancelLabel;
|
|
2546
2552
|
}
|
|
2547
2553
|
onDelete() {
|
|
2548
2554
|
this.data.delete?.();
|
|
@@ -2585,7 +2591,7 @@ class ModalComponent {
|
|
|
2585
2591
|
}
|
|
2586
2592
|
}
|
|
2587
2593
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ModalComponent, deps: [{ token: MAT_DIALOG_DATA, optional: true }, { token: i1$4.MatDialogRef, optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
2588
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", 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 [minHour]=\"formType.minHour\"\n [maxHour]=\"formType.maxHour\"\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 @if (data.formErrors && form.errors) {\n <div class=\"ud-modal-form-errors\">\n @for (key of objectKeys(form.errors); track key) {\n @if (data.formErrors[key]) {\n <p class=\"ud-modal-form-error\">\n <mat-icon class=\"ud-modal-form-error__icon\">error_outline</mat-icon>\n {{ data.formErrors[key] }}\n </p>\n }\n }\n </div>\n }\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}.ud-modal-form-errors{margin-top:12px}.ud-modal-form-error{display:flex;align-items:center;gap:7px;margin:6px 0 0;padding:8px 11px;border-radius:8px;background:#e5393514;border:1px solid rgba(229,57,53,.22);color:#c62828;font-size:12.5px;font-weight:500;line-height:1.35}.ud-modal-form-error__icon{font-size:16px;width:16px;height:16px;flex-shrink:0}\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", "minHour", "maxHour"] }, { 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"] }] });
|
|
2594
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", 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 [minHour]=\"formType.minHour\"\n [maxHour]=\"formType.maxHour\"\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 @if (data.formErrors && form.errors) {\n <div class=\"ud-modal-form-errors\">\n @for (key of objectKeys(form.errors); track key) {\n @if (data.formErrors[key]) {\n <p class=\"ud-modal-form-error\">\n <mat-icon class=\"ud-modal-form-error__icon\">error_outline</mat-icon>\n {{ data.formErrors[key] }}\n </p>\n }\n }\n </div>\n }\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-family:JetBrains Mono,ui-monospace,monospace;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#1b2535}.ud-modal-title{margin:0;font-family:DM Sans,system-ui,sans-serif;font-size:22px;line-height:1.2;font-weight:600;color:#2a3548;letter-spacing:-.01em}.ud-modal-lede{margin:8px 0 0;font-family:DM Sans,system-ui,sans-serif;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}.ud-modal-form-errors{margin-top:12px}.ud-modal-form-error{display:flex;align-items:center;gap:7px;margin:6px 0 0;padding:8px 11px;border-radius:8px;background:#e5393514;border:1px solid rgba(229,57,53,.22);color:#c62828;font-size:12.5px;font-weight:500;line-height:1.35}.ud-modal-form-error__icon{font-size:16px;width:16px;height:16px;flex-shrink:0}\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", "minHour", "maxHour"] }, { 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"] }] });
|
|
2589
2595
|
}
|
|
2590
2596
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ModalComponent, decorators: [{
|
|
2591
2597
|
type: Component,
|
|
@@ -2611,7 +2617,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
2611
2617
|
TimePickerComponent,
|
|
2612
2618
|
PhoneInputComponent,
|
|
2613
2619
|
UdButtonComponent,
|
|
2614
|
-
], 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 [minHour]=\"formType.minHour\"\n [maxHour]=\"formType.maxHour\"\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 @if (data.formErrors && form.errors) {\n <div class=\"ud-modal-form-errors\">\n @for (key of objectKeys(form.errors); track key) {\n @if (data.formErrors[key]) {\n <p class=\"ud-modal-form-error\">\n <mat-icon class=\"ud-modal-form-error__icon\">error_outline</mat-icon>\n {{ data.formErrors[key] }}\n </p>\n }\n }\n </div>\n }\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}.ud-modal-form-errors{margin-top:12px}.ud-modal-form-error{display:flex;align-items:center;gap:7px;margin:6px 0 0;padding:8px 11px;border-radius:8px;background:#e5393514;border:1px solid rgba(229,57,53,.22);color:#c62828;font-size:12.5px;font-weight:500;line-height:1.35}.ud-modal-form-error__icon{font-size:16px;width:16px;height:16px;flex-shrink:0}\n"] }]
|
|
2620
|
+
], 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 [minHour]=\"formType.minHour\"\n [maxHour]=\"formType.maxHour\"\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 @if (data.formErrors && form.errors) {\n <div class=\"ud-modal-form-errors\">\n @for (key of objectKeys(form.errors); track key) {\n @if (data.formErrors[key]) {\n <p class=\"ud-modal-form-error\">\n <mat-icon class=\"ud-modal-form-error__icon\">error_outline</mat-icon>\n {{ data.formErrors[key] }}\n </p>\n }\n }\n </div>\n }\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-family:JetBrains Mono,ui-monospace,monospace;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:#1b2535}.ud-modal-title{margin:0;font-family:DM Sans,system-ui,sans-serif;font-size:22px;line-height:1.2;font-weight:600;color:#2a3548;letter-spacing:-.01em}.ud-modal-lede{margin:8px 0 0;font-family:DM Sans,system-ui,sans-serif;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}.ud-modal-form-errors{margin-top:12px}.ud-modal-form-error{display:flex;align-items:center;gap:7px;margin:6px 0 0;padding:8px 11px;border-radius:8px;background:#e5393514;border:1px solid rgba(229,57,53,.22);color:#c62828;font-size:12.5px;font-weight:500;line-height:1.35}.ud-modal-form-error__icon{font-size:16px;width:16px;height:16px;flex-shrink:0}\n"] }]
|
|
2615
2621
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
2616
2622
|
type: Optional
|
|
2617
2623
|
}, {
|
|
@@ -3276,10 +3282,40 @@ class CalendarComponent {
|
|
|
3276
3282
|
* free-text input.
|
|
3277
3283
|
*/
|
|
3278
3284
|
bookableUsers = input([]);
|
|
3285
|
+
/**
|
|
3286
|
+
* Optional cutoff: any cell whose start is strictly before this Date is
|
|
3287
|
+
* visually disabled (grey, no hover, no cursor) and click-blocked. The
|
|
3288
|
+
* "Add slot" toolbar button pre-fills the modal with this timestamp
|
|
3289
|
+
* (rounded up to the next `slotDuration` increment) instead of `new Date()`.
|
|
3290
|
+
* The modal's save handler also refuses to emit if a hand-edited time
|
|
3291
|
+
* lands before the cutoff. Applies to both admin and student modes.
|
|
3292
|
+
*/
|
|
3293
|
+
minSlotStart = input(null);
|
|
3294
|
+
/**
|
|
3295
|
+
* Optional initial calendar position. Overrides the internal `navDate`
|
|
3296
|
+
* which otherwise defaults to "today / now". Useful for reschedule flows
|
|
3297
|
+
* or deep links that should land on a specific day.
|
|
3298
|
+
*/
|
|
3299
|
+
defaultDate = input(null);
|
|
3300
|
+
/**
|
|
3301
|
+
* When set, the "Add slot" modal flips into reschedule mode:
|
|
3302
|
+
* • title becomes "Move appointment"
|
|
3303
|
+
* • a lede shows the original time being moved away from
|
|
3304
|
+
* • the user picker auto-selects the lone bookable user (or `userId` if
|
|
3305
|
+
* provided) so the admin doesn't have to choose from a list of one
|
|
3306
|
+
* • Booked defaults to true (a reschedule keeps a booking, not a fresh slot)
|
|
3307
|
+
* • the confirm button reads "Reschedule" instead of "Save"
|
|
3308
|
+
*
|
|
3309
|
+
* The wire-level event is still `slotAdded` — consumers route it to their
|
|
3310
|
+
* reschedule API based on their own context (no extra output needed).
|
|
3311
|
+
*/
|
|
3312
|
+
rescheduleContext = input(null);
|
|
3279
3313
|
slotAdded = output();
|
|
3280
3314
|
slotUpdated = output();
|
|
3281
3315
|
slotRemoved = output();
|
|
3282
3316
|
slotBooked = output();
|
|
3317
|
+
/** Fired when a user attempts to interact with a cell before `minSlotStart`. */
|
|
3318
|
+
slotBlocked = output();
|
|
3283
3319
|
activeView = signal('week');
|
|
3284
3320
|
navDate = signal(new Date());
|
|
3285
3321
|
viewOptions = [
|
|
@@ -3310,6 +3346,12 @@ class CalendarComponent {
|
|
|
3310
3346
|
effect(() => {
|
|
3311
3347
|
this.activeView.set(this.defaultView());
|
|
3312
3348
|
}, { allowSignalWrites: true });
|
|
3349
|
+
// Apply the consumer's preferred starting day, if provided.
|
|
3350
|
+
effect(() => {
|
|
3351
|
+
const d = this.defaultDate();
|
|
3352
|
+
if (d)
|
|
3353
|
+
this.navDate.set(new Date(d));
|
|
3354
|
+
}, { allowSignalWrites: true });
|
|
3313
3355
|
}
|
|
3314
3356
|
ngOnDestroy() {
|
|
3315
3357
|
this.disposeHoverCard();
|
|
@@ -3575,6 +3617,19 @@ class CalendarComponent {
|
|
|
3575
3617
|
formatSlotTime(slot) {
|
|
3576
3618
|
return `${this.formatTime(slot.start)} – ${this.formatTime(slot.end)}`;
|
|
3577
3619
|
}
|
|
3620
|
+
/**
|
|
3621
|
+
* Pretty range like "Mon Jun 8 · 2:00 – 2:30 PM". Used by reschedule-mode's
|
|
3622
|
+
* "Moving from …" lede so the admin sees where the appointment is coming
|
|
3623
|
+
* from while picking the new time.
|
|
3624
|
+
*/
|
|
3625
|
+
formatMomentRange(start, end) {
|
|
3626
|
+
const dayLabel = start.toLocaleDateString(undefined, {
|
|
3627
|
+
weekday: 'short',
|
|
3628
|
+
month: 'short',
|
|
3629
|
+
day: 'numeric',
|
|
3630
|
+
});
|
|
3631
|
+
return `${dayLabel} · ${this.formatTime(start)} – ${this.formatTime(end)}`;
|
|
3632
|
+
}
|
|
3578
3633
|
isToday(day) {
|
|
3579
3634
|
return this.isSameDay(day, new Date());
|
|
3580
3635
|
}
|
|
@@ -3591,12 +3646,52 @@ class CalendarComponent {
|
|
|
3591
3646
|
onCellClick(day, hour, minute) {
|
|
3592
3647
|
if (this.mode() !== 'admin')
|
|
3593
3648
|
return;
|
|
3649
|
+
if (this.isCellDisabled(day, hour, minute)) {
|
|
3650
|
+
const start = this.combineDayTime(day, hour, minute);
|
|
3651
|
+
this.slotBlocked.emit({
|
|
3652
|
+
start,
|
|
3653
|
+
end: new Date(start.getTime() + this.slotDuration() * 60000),
|
|
3654
|
+
reason: 'before-min',
|
|
3655
|
+
});
|
|
3656
|
+
return;
|
|
3657
|
+
}
|
|
3594
3658
|
this.openAddModal(day, hour, minute);
|
|
3595
3659
|
}
|
|
3660
|
+
/**
|
|
3661
|
+
* True when the cell at (day, hour, minute) starts before `minSlotStart`.
|
|
3662
|
+
* Templates bind a CSS class on this to grey out past cells; the click
|
|
3663
|
+
* handler also bails so consumers don't see invalid `slotAdded` events.
|
|
3664
|
+
*/
|
|
3665
|
+
isCellDisabled(day, hour, minute) {
|
|
3666
|
+
const min = this.minSlotStart();
|
|
3667
|
+
if (!min)
|
|
3668
|
+
return false;
|
|
3669
|
+
return this.combineDayTime(day, hour, minute).getTime() < min.getTime();
|
|
3670
|
+
}
|
|
3671
|
+
/** True when `day` is fully before `minSlotStart` (used by month view). */
|
|
3672
|
+
isDayDisabled(day) {
|
|
3673
|
+
const min = this.minSlotStart();
|
|
3674
|
+
if (!min)
|
|
3675
|
+
return false;
|
|
3676
|
+
const endOfDay = new Date(day);
|
|
3677
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
3678
|
+
return endOfDay.getTime() < min.getTime();
|
|
3679
|
+
}
|
|
3680
|
+
combineDayTime(day, hour, minute) {
|
|
3681
|
+
const d = new Date(day);
|
|
3682
|
+
d.setHours(hour, minute, 0, 0);
|
|
3683
|
+
return d;
|
|
3684
|
+
}
|
|
3596
3685
|
onSlotMouseDown(e, slot) {
|
|
3597
3686
|
e.stopPropagation();
|
|
3598
3687
|
this.disposeHoverCard();
|
|
3599
3688
|
if (this.mode() === 'student' && !slot.booked) {
|
|
3689
|
+
// Refuse student bookings on slots that start before the cutoff.
|
|
3690
|
+
const min = this.minSlotStart();
|
|
3691
|
+
if (min && slot.start.getTime() < min.getTime()) {
|
|
3692
|
+
this.slotBlocked.emit({ start: slot.start, end: slot.end, reason: 'before-min' });
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3600
3695
|
this.slotBooked.emit({ ...slot, booked: true });
|
|
3601
3696
|
return;
|
|
3602
3697
|
}
|
|
@@ -3630,7 +3725,15 @@ class CalendarComponent {
|
|
|
3630
3725
|
const newStart = new Date(target.day);
|
|
3631
3726
|
newStart.setHours(target.hour, target.minute, 0, 0);
|
|
3632
3727
|
const newEnd = new Date(newStart.getTime() + this.dragSlotDurationMin * 60000);
|
|
3633
|
-
|
|
3728
|
+
// Refuse a drop that would land before the cutoff; surface as
|
|
3729
|
+
// slotBlocked so the consumer can show feedback if they want.
|
|
3730
|
+
const min = this.minSlotStart();
|
|
3731
|
+
if (min && newStart.getTime() < min.getTime()) {
|
|
3732
|
+
this.slotBlocked.emit({ start: newStart, end: newEnd, reason: 'before-min' });
|
|
3733
|
+
}
|
|
3734
|
+
else {
|
|
3735
|
+
this.slotUpdated.emit({ ...this.draggingSlot, start: newStart, end: newEnd });
|
|
3736
|
+
}
|
|
3634
3737
|
}
|
|
3635
3738
|
}
|
|
3636
3739
|
else {
|
|
@@ -3682,28 +3785,74 @@ class CalendarComponent {
|
|
|
3682
3785
|
}
|
|
3683
3786
|
// ── Modal open ─────────────────────────────────────────────────────────────
|
|
3684
3787
|
openAddModal(day, hour, minute) {
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3788
|
+
// Fall back to minSlotStart (rounded up to the next slotDuration boundary)
|
|
3789
|
+
// when no specific cell was clicked — keeps the toolbar "Add slot" button
|
|
3790
|
+
// from prefilling with a time that fails server-side validation.
|
|
3791
|
+
let startDate;
|
|
3792
|
+
if (day && hour !== undefined && minute !== undefined) {
|
|
3793
|
+
startDate = this.combineDayTime(day, hour, minute);
|
|
3794
|
+
}
|
|
3795
|
+
else if (day) {
|
|
3796
|
+
startDate = new Date(day);
|
|
3797
|
+
startDate.setHours(hour ?? startDate.getHours(), minute ?? 0, 0, 0);
|
|
3798
|
+
}
|
|
3799
|
+
else {
|
|
3800
|
+
// No specific cell clicked — anchor on the day the user is currently
|
|
3801
|
+
// viewing (so the modal's date matches the visible week/day) and then
|
|
3802
|
+
// bump forward if it sits before the cutoff.
|
|
3803
|
+
const anchor = new Date(this.navDate());
|
|
3804
|
+
const min = this.minSlotStart();
|
|
3805
|
+
if (min && anchor.getTime() < min.getTime()) {
|
|
3806
|
+
startDate = this.roundUpToSlot(min);
|
|
3807
|
+
}
|
|
3808
|
+
else {
|
|
3809
|
+
// Keep the navigated day, but pick a sensible default time-of-day
|
|
3810
|
+
// (start of the visible hour range) instead of "now".
|
|
3811
|
+
anchor.setHours(this.minHour(), 0, 0, 0);
|
|
3812
|
+
startDate = this.roundUpToSlot(anchor);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
const endDate = new Date(startDate.getTime() + this.slotDuration() * 60000);
|
|
3816
|
+
const base = new Date(startDate);
|
|
3817
|
+
// Reschedule-mode wiring: pre-pick the target user, default to Booked,
|
|
3818
|
+
// and prepare the contextual modal copy. In normal "add slot" mode all of
|
|
3819
|
+
// this collapses back to the defaults.
|
|
3820
|
+
const reschedule = this.rescheduleContext();
|
|
3821
|
+
const presetUserId = reschedule
|
|
3822
|
+
? reschedule.userId ?? (this.bookableUsers().length === 1 ? this.bookableUsers()[0].value : '')
|
|
3823
|
+
: '';
|
|
3824
|
+
const presetBooked = !!reschedule;
|
|
3693
3825
|
const form = this.buildForm({
|
|
3694
3826
|
date: new Date(base),
|
|
3695
3827
|
startTime: startDate,
|
|
3696
3828
|
endTime: endDate,
|
|
3829
|
+
bookedBy: presetUserId,
|
|
3830
|
+
booked: presetBooked,
|
|
3697
3831
|
});
|
|
3698
3832
|
const ref = this.dialog.open(ModalComponent, {
|
|
3699
3833
|
width: '480px',
|
|
3700
3834
|
data: {
|
|
3701
|
-
eyebrow: 'Calendar',
|
|
3702
|
-
title: 'Add time slot',
|
|
3835
|
+
eyebrow: reschedule ? 'Reschedule' : 'Calendar',
|
|
3836
|
+
title: reschedule ? 'Move appointment' : 'Add time slot',
|
|
3837
|
+
lede: reschedule
|
|
3838
|
+
? `Moving from ${this.formatMomentRange(reschedule.originalStart, reschedule.originalEnd)}`
|
|
3839
|
+
: undefined,
|
|
3840
|
+
confirmLabel: reschedule ? 'actions.reschedule' : undefined,
|
|
3703
3841
|
formGroup: form,
|
|
3704
3842
|
forms: this.slotForms(),
|
|
3705
3843
|
formErrors: this.slotFormErrors,
|
|
3706
|
-
save: (v) =>
|
|
3844
|
+
save: (v) => {
|
|
3845
|
+
const slot = this.buildSlot(crypto.randomUUID(), v);
|
|
3846
|
+
// Last-line defence: refuse if a hand-edited time slipped below
|
|
3847
|
+
// the cutoff. The cross-field validator already surfaces a friendly
|
|
3848
|
+
// error in the modal — this just guarantees we never emit a bad slot.
|
|
3849
|
+
const min = this.minSlotStart();
|
|
3850
|
+
if (min && slot.start.getTime() < min.getTime()) {
|
|
3851
|
+
this.slotBlocked.emit({ start: slot.start, end: slot.end, reason: 'before-min' });
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
this.slotAdded.emit(slot);
|
|
3855
|
+
},
|
|
3707
3856
|
},
|
|
3708
3857
|
});
|
|
3709
3858
|
form.get('startTime')?.valueChanges
|
|
@@ -3714,6 +3863,17 @@ class CalendarComponent {
|
|
|
3714
3863
|
form.get('endTime')?.setValue(this.shiftTime(time, this.slotDuration()));
|
|
3715
3864
|
});
|
|
3716
3865
|
}
|
|
3866
|
+
/** Round a Date up to the next `slotDuration` increment (minute-aligned). */
|
|
3867
|
+
roundUpToSlot(d) {
|
|
3868
|
+
const out = new Date(d);
|
|
3869
|
+
out.setSeconds(0, 0);
|
|
3870
|
+
const step = this.slotDuration();
|
|
3871
|
+
const remainder = out.getMinutes() % step;
|
|
3872
|
+
if (remainder > 0 || out.getTime() <= d.getTime()) {
|
|
3873
|
+
out.setMinutes(out.getMinutes() + (step - remainder));
|
|
3874
|
+
}
|
|
3875
|
+
return out;
|
|
3876
|
+
}
|
|
3717
3877
|
openEditModal(slot) {
|
|
3718
3878
|
const form = this.buildForm({
|
|
3719
3879
|
title: slot.title ?? '',
|
|
@@ -3735,6 +3895,11 @@ class CalendarComponent {
|
|
|
3735
3895
|
formErrors: this.slotFormErrors,
|
|
3736
3896
|
save: (v) => {
|
|
3737
3897
|
const updated = this.buildSlot(slot.id, v);
|
|
3898
|
+
const min = this.minSlotStart();
|
|
3899
|
+
if (min && updated.start.getTime() < min.getTime()) {
|
|
3900
|
+
this.slotBlocked.emit({ start: updated.start, end: updated.end, reason: 'before-min' });
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3738
3903
|
if (updated.booked && !slot.booked) {
|
|
3739
3904
|
this.slotBooked.emit(updated);
|
|
3740
3905
|
}
|
|
@@ -3758,14 +3923,20 @@ class CalendarComponent {
|
|
|
3758
3923
|
// Error messages shown by the modal for group-level (cross-field) errors.
|
|
3759
3924
|
get slotFormErrors() {
|
|
3760
3925
|
const range = `${this.formatHour(this.minHour(), 0)}–${this.formatHour(this.maxHour(), 0)}`;
|
|
3926
|
+
const min = this.minSlotStart();
|
|
3761
3927
|
return {
|
|
3762
3928
|
endBeforeStart: 'End time must be after the start time.',
|
|
3763
3929
|
outOfHours: `Times must be within working hours (${range}).`,
|
|
3930
|
+
beforeMinStart: min
|
|
3931
|
+
? `Slot must start at or after ${this.formatTime(min)} on ${min.toLocaleDateString()}.`
|
|
3932
|
+
: 'Slot must start in the future.',
|
|
3764
3933
|
};
|
|
3765
3934
|
}
|
|
3766
3935
|
// Cross-field validator: the end time must be strictly after the start time,
|
|
3767
3936
|
// and both must fall inside the calendar's [minHour, maxHour] working window.
|
|
3937
|
+
// When `minSlotStart` is set, the start of the slot must also be at/after it.
|
|
3768
3938
|
slotTimeValidator = (group) => {
|
|
3939
|
+
const date = group.get('date')?.value;
|
|
3769
3940
|
const start = group.get('startTime')?.value;
|
|
3770
3941
|
const end = group.get('endTime')?.value;
|
|
3771
3942
|
if (!start || !end)
|
|
@@ -3776,7 +3947,15 @@ class CalendarComponent {
|
|
|
3776
3947
|
const hi = this.maxHour() * 60;
|
|
3777
3948
|
if (startMin < lo || startMin > hi || endMin < lo || endMin > hi)
|
|
3778
3949
|
return { outOfHours: true };
|
|
3779
|
-
|
|
3950
|
+
if (endMin <= startMin)
|
|
3951
|
+
return { endBeforeStart: true };
|
|
3952
|
+
const min = this.minSlotStart();
|
|
3953
|
+
if (min && date) {
|
|
3954
|
+
const slotStart = this.combineDateTime(date, start);
|
|
3955
|
+
if (slotStart.getTime() < min.getTime())
|
|
3956
|
+
return { beforeMinStart: true };
|
|
3957
|
+
}
|
|
3958
|
+
return null;
|
|
3780
3959
|
};
|
|
3781
3960
|
buildForm(defaults) {
|
|
3782
3961
|
return new FormGroup({
|
|
@@ -3799,9 +3978,16 @@ class CalendarComponent {
|
|
|
3799
3978
|
availableOptions: users,
|
|
3800
3979
|
}
|
|
3801
3980
|
: { type: ModalInputType.INPUT, title: 'Booked By', property: 'bookedBy', placeholder: 'Student name or ID' };
|
|
3981
|
+
// Pass the cutoff (date-only) into the date picker so users can't pick a
|
|
3982
|
+
// past day from the calendar. We strip the time so the *whole* cutoff day
|
|
3983
|
+
// is still selectable — time-of-day enforcement happens in startTime/endTime.
|
|
3984
|
+
const minDate = this.minSlotStart();
|
|
3985
|
+
const dateMin = minDate
|
|
3986
|
+
? new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate())
|
|
3987
|
+
: undefined;
|
|
3802
3988
|
return [
|
|
3803
3989
|
{ type: ModalInputType.INPUT, title: 'Title', property: 'title', placeholder: 'e.g. Office hours' },
|
|
3804
|
-
{ type: ModalInputType.DATETIME, title: 'Date', property: 'date' },
|
|
3990
|
+
{ type: ModalInputType.DATETIME, title: 'Date', property: 'date', min: dateMin },
|
|
3805
3991
|
{ type: ModalInputType.TIME, title: 'Start Time', property: 'startTime', intervalMinutes: this.slotDuration(), minHour: this.minHour(), maxHour: this.maxHour() },
|
|
3806
3992
|
{ type: ModalInputType.TIME, title: 'End Time', property: 'endTime', intervalMinutes: this.slotDuration(), minHour: this.minHour(), maxHour: this.maxHour() },
|
|
3807
3993
|
bookedByField,
|
|
@@ -3877,11 +4063,11 @@ class CalendarComponent {
|
|
|
3877
4063
|
return `rgba(${r},${g},${b},${alpha})`;
|
|
3878
4064
|
}
|
|
3879
4065
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3880
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", 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 }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, bookableUsers: { classPropertyName: "bookableUsers", publicName: "bookableUsers", 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)" } }, viewQueries: [{ propertyName: "hoverCardTpl", first: true, predicate: ["hoverCard"], descendants: true, isSignal: true }], 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\" [style.max-height]=\"maxHeight()\">\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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\" [style.max-height]=\"maxHeight()\">\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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<!-- \u2500\u2500 Rich hover card (rendered in a CDK overlay) \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<ng-template #hoverCard>\n @if (hoverSlot(); as s) {\n <div\n class=\"ud-cal__hovercard\"\n [class.ud-cal__hovercard--booked]=\"s.booked\"\n [style.--hc-accent]=\"slotBorderColor(s)\"\n (mouseenter)=\"cancelHoverClose()\"\n (mouseleave)=\"closeHoverCard()\">\n <span class=\"ud-cal__hovercard-bar\"></span>\n <div class=\"ud-cal__hovercard-body\">\n <div class=\"ud-cal__hovercard-head\">\n <span class=\"ud-cal__hovercard-title\">{{ s.title || 'Untitled slot' }}</span>\n <span class=\"ud-cal__hovercard-pill\" [class.ud-cal__hovercard-pill--booked]=\"s.booked\">\n <span class=\"ud-cal__hovercard-pill-dot\"></span>\n {{ s.booked ? 'Booked' : 'Available' }}\n </span>\n </div>\n\n <div class=\"ud-cal__hovercard-row\">\n <mat-icon class=\"ud-cal__hovercard-ico\">schedule</mat-icon>\n <span class=\"ud-cal__hovercard-time\">{{ formatSlotTime(s) }}</span>\n <span class=\"ud-cal__hovercard-dur\">{{ slotDurationLabel(s) }}</span>\n </div>\n\n @if (s.booked && s.bookedBy) {\n <div class=\"ud-cal__hovercard-row ud-cal__hovercard-person\">\n <span class=\"ud-cal__hovercard-avatar\" [style.background]=\"slotBorderColor(s)\">\n {{ slotInitials(s.bookedBy) }}\n </span>\n <span class=\"ud-cal__hovercard-person-name\">{{ s.bookedBy }}</span>\n </div>\n }\n </div>\n </div>\n }\n</ng-template>\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;margin-left:2px;box-sizing:border-box;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--cozy{padding:2px 6px;gap:0}.ud-cal__slot--compact{padding:1px 6px;gap:0;flex-direction:row;align-items:center;justify-content:flex-start}.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{left:3px;right:3px;margin-left:0;pointer-events:none;opacity:.75;border-left-style:dashed;border-top:1px dashed currentColor;z-index:2}.ud-cal__slot--booked{border-left-width:4px;box-shadow:inset 0 0 0 1px #1b253514}.ud-cal__slot--booked .ud-cal__slot-title{font-weight:600}.ud-cal__slot:not(.ud-cal__slot--booked){border-left-style:dashed}.ud-cal__slot-lock{font-size:11px!important;width:11px!important;height:11px!important;line-height:11px!important;flex-shrink:0;opacity:.75}.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)}.ud-cal__hovercard{--hc-accent: var(--eu-navy, #1b2535);position:relative;display:flex;min-width:208px;max-width:288px;background:#fff;border:1px solid var(--eu-border-light, #e8eaef);border-radius:12px;overflow:hidden;font-family:DM Sans,system-ui,sans-serif;box-shadow:0 1px 2px #1b25350d,0 14px 30px -10px #1b25353d;transform-origin:top left;animation:ud-cal-hovercard-in .17s cubic-bezier(.16,1,.3,1) both}.ud-cal__hovercard:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(135deg,color-mix(in srgb,var(--hc-accent) 6%,transparent),transparent 55%)}@keyframes ud-cal-hovercard-in{0%{opacity:0;transform:translateY(5px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.ud-cal__hovercard-bar{width:4px;flex-shrink:0;background:var(--hc-accent)}.ud-cal__hovercard-body{position:relative;z-index:1;flex:1;min-width:0;padding:12px 14px 13px;display:flex;flex-direction:column;gap:9px}.ud-cal__hovercard-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.ud-cal__hovercard-title{font-size:13.5px;font-weight:600;letter-spacing:-.01em;line-height:1.3;color:var(--eu-text, #2a3548);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ud-cal__hovercard-pill{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;padding:3px 9px 3px 7px;border-radius:999px;font-size:9.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;background:#1b253512;color:#1b253599}.ud-cal__hovercard-pill--booked{background:#1b25351f;color:var(--eu-navy)}.ud-cal__hovercard-pill-dot{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,currentColor 22%,transparent)}.ud-cal__hovercard-row{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--eu-muted, #6b7585)}.ud-cal__hovercard-ico{font-size:16px!important;width:16px!important;height:16px!important;line-height:16px!important;color:var(--hc-accent);opacity:.9}.ud-cal__hovercard-time{color:var(--eu-text, #2a3548);font-weight:600;font-variant-numeric:tabular-nums}.ud-cal__hovercard-dur{margin-left:auto;font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);background:var(--eu-bg, #f4f5f7);border:1px solid var(--eu-border-light, #e8eaef);padding:1px 8px;border-radius:6px;white-space:nowrap}.ud-cal__hovercard-person{padding-top:8px;border-top:1px dashed var(--eu-border-light, #e8eaef)}.ud-cal__hovercard-avatar{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--hc-accent);color:#fff;font-size:9px;font-weight:700;letter-spacing:.03em;flex-shrink:0}.ud-cal__hovercard-person-name{color:var(--eu-text, #2a3548);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\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"] }] });
|
|
4066
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", 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 }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, bookableUsers: { classPropertyName: "bookableUsers", publicName: "bookableUsers", isSignal: true, isRequired: false, transformFunction: null }, minSlotStart: { classPropertyName: "minSlotStart", publicName: "minSlotStart", isSignal: true, isRequired: false, transformFunction: null }, defaultDate: { classPropertyName: "defaultDate", publicName: "defaultDate", isSignal: true, isRequired: false, transformFunction: null }, rescheduleContext: { classPropertyName: "rescheduleContext", publicName: "rescheduleContext", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { slotAdded: "slotAdded", slotUpdated: "slotUpdated", slotRemoved: "slotRemoved", slotBooked: "slotBooked", slotBlocked: "slotBlocked" }, host: { listeners: { "document:mousemove": "onDocMouseMove($event)", "document:mouseup": "onDocMouseUp($event)" } }, viewQueries: [{ propertyName: "hoverCardTpl", first: true, predicate: ["hoverCard"], descendants: true, isSignal: true }], 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\" [style.max-height]=\"maxHeight()\">\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 [class.ud-cal__grid-cell--disabled]=\"isCellDisabled(day, ts.hour, ts.minute)\"\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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 [class.ud-cal__month-cell--disabled]=\"isDayDisabled(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\" [style.max-height]=\"maxHeight()\">\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 [class.ud-cal__grid-cell--disabled]=\"isCellDisabled(navDate(), ts.hour, ts.minute)\"\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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<!-- \u2500\u2500 Rich hover card (rendered in a CDK overlay) \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<ng-template #hoverCard>\n @if (hoverSlot(); as s) {\n <div\n class=\"ud-cal__hovercard\"\n [class.ud-cal__hovercard--booked]=\"s.booked\"\n [style.--hc-accent]=\"slotBorderColor(s)\"\n (mouseenter)=\"cancelHoverClose()\"\n (mouseleave)=\"closeHoverCard()\">\n <span class=\"ud-cal__hovercard-bar\"></span>\n <div class=\"ud-cal__hovercard-body\">\n <div class=\"ud-cal__hovercard-head\">\n <span class=\"ud-cal__hovercard-title\">{{ s.title || 'Untitled slot' }}</span>\n <span class=\"ud-cal__hovercard-pill\" [class.ud-cal__hovercard-pill--booked]=\"s.booked\">\n <span class=\"ud-cal__hovercard-pill-dot\"></span>\n {{ s.booked ? 'Booked' : 'Available' }}\n </span>\n </div>\n\n <div class=\"ud-cal__hovercard-row\">\n <mat-icon class=\"ud-cal__hovercard-ico\">schedule</mat-icon>\n <span class=\"ud-cal__hovercard-time\">{{ formatSlotTime(s) }}</span>\n <span class=\"ud-cal__hovercard-dur\">{{ slotDurationLabel(s) }}</span>\n </div>\n\n @if (s.booked && s.bookedBy) {\n <div class=\"ud-cal__hovercard-row ud-cal__hovercard-person\">\n <span class=\"ud-cal__hovercard-avatar\" [style.background]=\"slotBorderColor(s)\">\n {{ slotInitials(s.bookedBy) }}\n </span>\n <span class=\"ud-cal__hovercard-person-name\">{{ s.bookedBy }}</span>\n </div>\n }\n </div>\n </div>\n }\n</ng-template>\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__grid-cell--disabled{cursor:not-allowed;background:repeating-linear-gradient(135deg,#1b253508,#1b253508 6px,#1b25350f 6px 12px)}.ud-cal__grid-cell--disabled:hover{background:repeating-linear-gradient(135deg,#1b253508,#1b253508 6px,#1b25350f 6px 12px)}.ud-cal__slot{position:absolute;margin-left:2px;box-sizing:border-box;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--cozy{padding:2px 6px;gap:0}.ud-cal__slot--compact{padding:1px 6px;gap:0;flex-direction:row;align-items:center;justify-content:flex-start}.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{left:3px;right:3px;margin-left:0;pointer-events:none;opacity:.75;border-left-style:dashed;border-top:1px dashed currentColor;z-index:2}.ud-cal__slot--booked{border-left-width:4px;box-shadow:inset 0 0 0 1px #1b253514}.ud-cal__slot--booked .ud-cal__slot-title{font-weight:600}.ud-cal__slot:not(.ud-cal__slot--booked){border-left-style:dashed}.ud-cal__slot-lock{font-size:11px!important;width:11px!important;height:11px!important;line-height:11px!important;flex-shrink:0;opacity:.75}.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-cell--disabled{cursor:not-allowed;color:#1b253559;background:repeating-linear-gradient(135deg,#1b253505,#1b253505 6px,#1b25350d 6px 12px)}.ud-cal__month-cell--disabled:hover{background:repeating-linear-gradient(135deg,#1b253505,#1b253505 6px,#1b25350d 6px 12px)}.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)}.ud-cal__hovercard{--hc-accent: var(--eu-navy, #1b2535);position:relative;display:flex;min-width:208px;max-width:288px;background:#fff;border:1px solid var(--eu-border-light, #e8eaef);border-radius:12px;overflow:hidden;font-family:DM Sans,system-ui,sans-serif;box-shadow:0 1px 2px #1b25350d,0 14px 30px -10px #1b25353d;transform-origin:top left;animation:ud-cal-hovercard-in .17s cubic-bezier(.16,1,.3,1) both}.ud-cal__hovercard:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(135deg,color-mix(in srgb,var(--hc-accent) 6%,transparent),transparent 55%)}@keyframes ud-cal-hovercard-in{0%{opacity:0;transform:translateY(5px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.ud-cal__hovercard-bar{width:4px;flex-shrink:0;background:var(--hc-accent)}.ud-cal__hovercard-body{position:relative;z-index:1;flex:1;min-width:0;padding:12px 14px 13px;display:flex;flex-direction:column;gap:9px}.ud-cal__hovercard-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.ud-cal__hovercard-title{font-size:13.5px;font-weight:600;letter-spacing:-.01em;line-height:1.3;color:var(--eu-text, #2a3548);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ud-cal__hovercard-pill{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;padding:3px 9px 3px 7px;border-radius:999px;font-size:9.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;background:#1b253512;color:#1b253599}.ud-cal__hovercard-pill--booked{background:#1b25351f;color:var(--eu-navy)}.ud-cal__hovercard-pill-dot{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,currentColor 22%,transparent)}.ud-cal__hovercard-row{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--eu-muted, #6b7585)}.ud-cal__hovercard-ico{font-size:16px!important;width:16px!important;height:16px!important;line-height:16px!important;color:var(--hc-accent);opacity:.9}.ud-cal__hovercard-time{color:var(--eu-text, #2a3548);font-weight:600;font-variant-numeric:tabular-nums}.ud-cal__hovercard-dur{margin-left:auto;font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);background:var(--eu-bg, #f4f5f7);border:1px solid var(--eu-border-light, #e8eaef);padding:1px 8px;border-radius:6px;white-space:nowrap}.ud-cal__hovercard-person{padding-top:8px;border-top:1px dashed var(--eu-border-light, #e8eaef)}.ud-cal__hovercard-avatar{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--hc-accent);color:#fff;font-size:9px;font-weight:700;letter-spacing:.03em;flex-shrink:0}.ud-cal__hovercard-person-name{color:var(--eu-text, #2a3548);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\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"] }] });
|
|
3881
4067
|
}
|
|
3882
4068
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CalendarComponent, decorators: [{
|
|
3883
4069
|
type: Component,
|
|
3884
|
-
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\" [style.max-height]=\"maxHeight()\">\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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\" [style.max-height]=\"maxHeight()\">\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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<!-- \u2500\u2500 Rich hover card (rendered in a CDK overlay) \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<ng-template #hoverCard>\n @if (hoverSlot(); as s) {\n <div\n class=\"ud-cal__hovercard\"\n [class.ud-cal__hovercard--booked]=\"s.booked\"\n [style.--hc-accent]=\"slotBorderColor(s)\"\n (mouseenter)=\"cancelHoverClose()\"\n (mouseleave)=\"closeHoverCard()\">\n <span class=\"ud-cal__hovercard-bar\"></span>\n <div class=\"ud-cal__hovercard-body\">\n <div class=\"ud-cal__hovercard-head\">\n <span class=\"ud-cal__hovercard-title\">{{ s.title || 'Untitled slot' }}</span>\n <span class=\"ud-cal__hovercard-pill\" [class.ud-cal__hovercard-pill--booked]=\"s.booked\">\n <span class=\"ud-cal__hovercard-pill-dot\"></span>\n {{ s.booked ? 'Booked' : 'Available' }}\n </span>\n </div>\n\n <div class=\"ud-cal__hovercard-row\">\n <mat-icon class=\"ud-cal__hovercard-ico\">schedule</mat-icon>\n <span class=\"ud-cal__hovercard-time\">{{ formatSlotTime(s) }}</span>\n <span class=\"ud-cal__hovercard-dur\">{{ slotDurationLabel(s) }}</span>\n </div>\n\n @if (s.booked && s.bookedBy) {\n <div class=\"ud-cal__hovercard-row ud-cal__hovercard-person\">\n <span class=\"ud-cal__hovercard-avatar\" [style.background]=\"slotBorderColor(s)\">\n {{ slotInitials(s.bookedBy) }}\n </span>\n <span class=\"ud-cal__hovercard-person-name\">{{ s.bookedBy }}</span>\n </div>\n }\n </div>\n </div>\n }\n</ng-template>\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;margin-left:2px;box-sizing:border-box;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--cozy{padding:2px 6px;gap:0}.ud-cal__slot--compact{padding:1px 6px;gap:0;flex-direction:row;align-items:center;justify-content:flex-start}.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{left:3px;right:3px;margin-left:0;pointer-events:none;opacity:.75;border-left-style:dashed;border-top:1px dashed currentColor;z-index:2}.ud-cal__slot--booked{border-left-width:4px;box-shadow:inset 0 0 0 1px #1b253514}.ud-cal__slot--booked .ud-cal__slot-title{font-weight:600}.ud-cal__slot:not(.ud-cal__slot--booked){border-left-style:dashed}.ud-cal__slot-lock{font-size:11px!important;width:11px!important;height:11px!important;line-height:11px!important;flex-shrink:0;opacity:.75}.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)}.ud-cal__hovercard{--hc-accent: var(--eu-navy, #1b2535);position:relative;display:flex;min-width:208px;max-width:288px;background:#fff;border:1px solid var(--eu-border-light, #e8eaef);border-radius:12px;overflow:hidden;font-family:DM Sans,system-ui,sans-serif;box-shadow:0 1px 2px #1b25350d,0 14px 30px -10px #1b25353d;transform-origin:top left;animation:ud-cal-hovercard-in .17s cubic-bezier(.16,1,.3,1) both}.ud-cal__hovercard:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(135deg,color-mix(in srgb,var(--hc-accent) 6%,transparent),transparent 55%)}@keyframes ud-cal-hovercard-in{0%{opacity:0;transform:translateY(5px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.ud-cal__hovercard-bar{width:4px;flex-shrink:0;background:var(--hc-accent)}.ud-cal__hovercard-body{position:relative;z-index:1;flex:1;min-width:0;padding:12px 14px 13px;display:flex;flex-direction:column;gap:9px}.ud-cal__hovercard-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.ud-cal__hovercard-title{font-size:13.5px;font-weight:600;letter-spacing:-.01em;line-height:1.3;color:var(--eu-text, #2a3548);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ud-cal__hovercard-pill{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;padding:3px 9px 3px 7px;border-radius:999px;font-size:9.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;background:#1b253512;color:#1b253599}.ud-cal__hovercard-pill--booked{background:#1b25351f;color:var(--eu-navy)}.ud-cal__hovercard-pill-dot{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,currentColor 22%,transparent)}.ud-cal__hovercard-row{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--eu-muted, #6b7585)}.ud-cal__hovercard-ico{font-size:16px!important;width:16px!important;height:16px!important;line-height:16px!important;color:var(--hc-accent);opacity:.9}.ud-cal__hovercard-time{color:var(--eu-text, #2a3548);font-weight:600;font-variant-numeric:tabular-nums}.ud-cal__hovercard-dur{margin-left:auto;font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);background:var(--eu-bg, #f4f5f7);border:1px solid var(--eu-border-light, #e8eaef);padding:1px 8px;border-radius:6px;white-space:nowrap}.ud-cal__hovercard-person{padding-top:8px;border-top:1px dashed var(--eu-border-light, #e8eaef)}.ud-cal__hovercard-avatar{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--hc-accent);color:#fff;font-size:9px;font-weight:700;letter-spacing:.03em;flex-shrink:0}.ud-cal__hovercard-person-name{color:var(--eu-text, #2a3548);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
4070
|
+
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\" [style.max-height]=\"maxHeight()\">\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 [class.ud-cal__grid-cell--disabled]=\"isCellDisabled(day, ts.hour, ts.minute)\"\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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 [class.ud-cal__month-cell--disabled]=\"isDayDisabled(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\" [style.max-height]=\"maxHeight()\">\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 [class.ud-cal__grid-cell--disabled]=\"isCellDisabled(navDate(), ts.hour, ts.minute)\"\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--' + slotDensity(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.left.%]=\"slotLeft(slot)\"\n [style.width.%]=\"slotWidth(slot)\"\n [style.background]=\"slotBg(slot)\"\n [style.color]=\"slotTextColor(slot)\"\n [style.border-color]=\"slotBorderColor(slot)\"\n (mouseenter)=\"openHoverCard(slot, $event.currentTarget)\"\n (mouseleave)=\"closeHoverCard()\"\n (mousedown)=\"onSlotMouseDown($event, slot)\">\n <div class=\"ud-cal__slot-inner\">\n @if (slot.booked) {\n <mat-icon class=\"ud-cal__slot-lock\">person</mat-icon>\n }\n @if (slot.title) {\n <span class=\"ud-cal__slot-title\">{{ slot.title }}</span>\n } @else if (slotDensity(slot) === 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n </div>\n @if (slotDensity(slot) !== 'compact') {\n <span class=\"ud-cal__slot-time\">{{ formatSlotTime(slot) }}</span>\n }\n @if (slotDensity(slot) === 'comfortable' && 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<!-- \u2500\u2500 Rich hover card (rendered in a CDK overlay) \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<ng-template #hoverCard>\n @if (hoverSlot(); as s) {\n <div\n class=\"ud-cal__hovercard\"\n [class.ud-cal__hovercard--booked]=\"s.booked\"\n [style.--hc-accent]=\"slotBorderColor(s)\"\n (mouseenter)=\"cancelHoverClose()\"\n (mouseleave)=\"closeHoverCard()\">\n <span class=\"ud-cal__hovercard-bar\"></span>\n <div class=\"ud-cal__hovercard-body\">\n <div class=\"ud-cal__hovercard-head\">\n <span class=\"ud-cal__hovercard-title\">{{ s.title || 'Untitled slot' }}</span>\n <span class=\"ud-cal__hovercard-pill\" [class.ud-cal__hovercard-pill--booked]=\"s.booked\">\n <span class=\"ud-cal__hovercard-pill-dot\"></span>\n {{ s.booked ? 'Booked' : 'Available' }}\n </span>\n </div>\n\n <div class=\"ud-cal__hovercard-row\">\n <mat-icon class=\"ud-cal__hovercard-ico\">schedule</mat-icon>\n <span class=\"ud-cal__hovercard-time\">{{ formatSlotTime(s) }}</span>\n <span class=\"ud-cal__hovercard-dur\">{{ slotDurationLabel(s) }}</span>\n </div>\n\n @if (s.booked && s.bookedBy) {\n <div class=\"ud-cal__hovercard-row ud-cal__hovercard-person\">\n <span class=\"ud-cal__hovercard-avatar\" [style.background]=\"slotBorderColor(s)\">\n {{ slotInitials(s.bookedBy) }}\n </span>\n <span class=\"ud-cal__hovercard-person-name\">{{ s.bookedBy }}</span>\n </div>\n }\n </div>\n </div>\n }\n</ng-template>\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__grid-cell--disabled{cursor:not-allowed;background:repeating-linear-gradient(135deg,#1b253508,#1b253508 6px,#1b25350f 6px 12px)}.ud-cal__grid-cell--disabled:hover{background:repeating-linear-gradient(135deg,#1b253508,#1b253508 6px,#1b25350f 6px 12px)}.ud-cal__slot{position:absolute;margin-left:2px;box-sizing:border-box;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--cozy{padding:2px 6px;gap:0}.ud-cal__slot--compact{padding:1px 6px;gap:0;flex-direction:row;align-items:center;justify-content:flex-start}.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{left:3px;right:3px;margin-left:0;pointer-events:none;opacity:.75;border-left-style:dashed;border-top:1px dashed currentColor;z-index:2}.ud-cal__slot--booked{border-left-width:4px;box-shadow:inset 0 0 0 1px #1b253514}.ud-cal__slot--booked .ud-cal__slot-title{font-weight:600}.ud-cal__slot:not(.ud-cal__slot--booked){border-left-style:dashed}.ud-cal__slot-lock{font-size:11px!important;width:11px!important;height:11px!important;line-height:11px!important;flex-shrink:0;opacity:.75}.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-cell--disabled{cursor:not-allowed;color:#1b253559;background:repeating-linear-gradient(135deg,#1b253505,#1b253505 6px,#1b25350d 6px 12px)}.ud-cal__month-cell--disabled:hover{background:repeating-linear-gradient(135deg,#1b253505,#1b253505 6px,#1b25350d 6px 12px)}.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)}.ud-cal__hovercard{--hc-accent: var(--eu-navy, #1b2535);position:relative;display:flex;min-width:208px;max-width:288px;background:#fff;border:1px solid var(--eu-border-light, #e8eaef);border-radius:12px;overflow:hidden;font-family:DM Sans,system-ui,sans-serif;box-shadow:0 1px 2px #1b25350d,0 14px 30px -10px #1b25353d;transform-origin:top left;animation:ud-cal-hovercard-in .17s cubic-bezier(.16,1,.3,1) both}.ud-cal__hovercard:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:linear-gradient(135deg,color-mix(in srgb,var(--hc-accent) 6%,transparent),transparent 55%)}@keyframes ud-cal-hovercard-in{0%{opacity:0;transform:translateY(5px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}.ud-cal__hovercard-bar{width:4px;flex-shrink:0;background:var(--hc-accent)}.ud-cal__hovercard-body{position:relative;z-index:1;flex:1;min-width:0;padding:12px 14px 13px;display:flex;flex-direction:column;gap:9px}.ud-cal__hovercard-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.ud-cal__hovercard-title{font-size:13.5px;font-weight:600;letter-spacing:-.01em;line-height:1.3;color:var(--eu-text, #2a3548);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ud-cal__hovercard-pill{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;padding:3px 9px 3px 7px;border-radius:999px;font-size:9.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;background:#1b253512;color:#1b253599}.ud-cal__hovercard-pill--booked{background:#1b25351f;color:var(--eu-navy)}.ud-cal__hovercard-pill-dot{width:5px;height:5px;border-radius:50%;background:currentColor;box-shadow:0 0 0 3px color-mix(in srgb,currentColor 22%,transparent)}.ud-cal__hovercard-row{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:500;color:var(--eu-muted, #6b7585)}.ud-cal__hovercard-ico{font-size:16px!important;width:16px!important;height:16px!important;line-height:16px!important;color:var(--hc-accent);opacity:.9}.ud-cal__hovercard-time{color:var(--eu-text, #2a3548);font-weight:600;font-variant-numeric:tabular-nums}.ud-cal__hovercard-dur{margin-left:auto;font-size:10px;font-weight:600;color:var(--eu-muted, #6b7585);background:var(--eu-bg, #f4f5f7);border:1px solid var(--eu-border-light, #e8eaef);padding:1px 8px;border-radius:6px;white-space:nowrap}.ud-cal__hovercard-person{padding-top:8px;border-top:1px dashed var(--eu-border-light, #e8eaef)}.ud-cal__hovercard-avatar{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:var(--hc-accent);color:#fff;font-size:9px;font-weight:700;letter-spacing:.03em;flex-shrink:0}.ud-cal__hovercard-person-name{color:var(--eu-text, #2a3548);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
3885
4071
|
}], ctorParameters: () => [], propDecorators: { onDocMouseMove: [{
|
|
3886
4072
|
type: HostListener,
|
|
3887
4073
|
args: ['document:mousemove', ['$event']]
|