ud-components 0.5.20 → 0.5.22
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.
|
@@ -2585,7 +2585,7 @@ class ModalComponent {
|
|
|
2585
2585
|
}
|
|
2586
2586
|
}
|
|
2587
2587
|
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"] }] });
|
|
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-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
2589
|
}
|
|
2590
2590
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: ModalComponent, decorators: [{
|
|
2591
2591
|
type: Component,
|
|
@@ -2611,7 +2611,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
|
|
|
2611
2611
|
TimePickerComponent,
|
|
2612
2612
|
PhoneInputComponent,
|
|
2613
2613
|
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"] }]
|
|
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-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
2615
|
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
2616
2616
|
type: Optional
|
|
2617
2617
|
}, {
|
|
@@ -3276,10 +3276,27 @@ class CalendarComponent {
|
|
|
3276
3276
|
* free-text input.
|
|
3277
3277
|
*/
|
|
3278
3278
|
bookableUsers = input([]);
|
|
3279
|
+
/**
|
|
3280
|
+
* Optional cutoff: any cell whose start is strictly before this Date is
|
|
3281
|
+
* visually disabled (grey, no hover, no cursor) and click-blocked. The
|
|
3282
|
+
* "Add slot" toolbar button pre-fills the modal with this timestamp
|
|
3283
|
+
* (rounded up to the next `slotDuration` increment) instead of `new Date()`.
|
|
3284
|
+
* The modal's save handler also refuses to emit if a hand-edited time
|
|
3285
|
+
* lands before the cutoff. Applies to both admin and student modes.
|
|
3286
|
+
*/
|
|
3287
|
+
minSlotStart = input(null);
|
|
3288
|
+
/**
|
|
3289
|
+
* Optional initial calendar position. Overrides the internal `navDate`
|
|
3290
|
+
* which otherwise defaults to "today / now". Useful for reschedule flows
|
|
3291
|
+
* or deep links that should land on a specific day.
|
|
3292
|
+
*/
|
|
3293
|
+
defaultDate = input(null);
|
|
3279
3294
|
slotAdded = output();
|
|
3280
3295
|
slotUpdated = output();
|
|
3281
3296
|
slotRemoved = output();
|
|
3282
3297
|
slotBooked = output();
|
|
3298
|
+
/** Fired when a user attempts to interact with a cell before `minSlotStart`. */
|
|
3299
|
+
slotBlocked = output();
|
|
3283
3300
|
activeView = signal('week');
|
|
3284
3301
|
navDate = signal(new Date());
|
|
3285
3302
|
viewOptions = [
|
|
@@ -3310,6 +3327,12 @@ class CalendarComponent {
|
|
|
3310
3327
|
effect(() => {
|
|
3311
3328
|
this.activeView.set(this.defaultView());
|
|
3312
3329
|
}, { allowSignalWrites: true });
|
|
3330
|
+
// Apply the consumer's preferred starting day, if provided.
|
|
3331
|
+
effect(() => {
|
|
3332
|
+
const d = this.defaultDate();
|
|
3333
|
+
if (d)
|
|
3334
|
+
this.navDate.set(new Date(d));
|
|
3335
|
+
}, { allowSignalWrites: true });
|
|
3313
3336
|
}
|
|
3314
3337
|
ngOnDestroy() {
|
|
3315
3338
|
this.disposeHoverCard();
|
|
@@ -3591,12 +3614,52 @@ class CalendarComponent {
|
|
|
3591
3614
|
onCellClick(day, hour, minute) {
|
|
3592
3615
|
if (this.mode() !== 'admin')
|
|
3593
3616
|
return;
|
|
3617
|
+
if (this.isCellDisabled(day, hour, minute)) {
|
|
3618
|
+
const start = this.combineDayTime(day, hour, minute);
|
|
3619
|
+
this.slotBlocked.emit({
|
|
3620
|
+
start,
|
|
3621
|
+
end: new Date(start.getTime() + this.slotDuration() * 60000),
|
|
3622
|
+
reason: 'before-min',
|
|
3623
|
+
});
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3594
3626
|
this.openAddModal(day, hour, minute);
|
|
3595
3627
|
}
|
|
3628
|
+
/**
|
|
3629
|
+
* True when the cell at (day, hour, minute) starts before `minSlotStart`.
|
|
3630
|
+
* Templates bind a CSS class on this to grey out past cells; the click
|
|
3631
|
+
* handler also bails so consumers don't see invalid `slotAdded` events.
|
|
3632
|
+
*/
|
|
3633
|
+
isCellDisabled(day, hour, minute) {
|
|
3634
|
+
const min = this.minSlotStart();
|
|
3635
|
+
if (!min)
|
|
3636
|
+
return false;
|
|
3637
|
+
return this.combineDayTime(day, hour, minute).getTime() < min.getTime();
|
|
3638
|
+
}
|
|
3639
|
+
/** True when `day` is fully before `minSlotStart` (used by month view). */
|
|
3640
|
+
isDayDisabled(day) {
|
|
3641
|
+
const min = this.minSlotStart();
|
|
3642
|
+
if (!min)
|
|
3643
|
+
return false;
|
|
3644
|
+
const endOfDay = new Date(day);
|
|
3645
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
3646
|
+
return endOfDay.getTime() < min.getTime();
|
|
3647
|
+
}
|
|
3648
|
+
combineDayTime(day, hour, minute) {
|
|
3649
|
+
const d = new Date(day);
|
|
3650
|
+
d.setHours(hour, minute, 0, 0);
|
|
3651
|
+
return d;
|
|
3652
|
+
}
|
|
3596
3653
|
onSlotMouseDown(e, slot) {
|
|
3597
3654
|
e.stopPropagation();
|
|
3598
3655
|
this.disposeHoverCard();
|
|
3599
3656
|
if (this.mode() === 'student' && !slot.booked) {
|
|
3657
|
+
// Refuse student bookings on slots that start before the cutoff.
|
|
3658
|
+
const min = this.minSlotStart();
|
|
3659
|
+
if (min && slot.start.getTime() < min.getTime()) {
|
|
3660
|
+
this.slotBlocked.emit({ start: slot.start, end: slot.end, reason: 'before-min' });
|
|
3661
|
+
return;
|
|
3662
|
+
}
|
|
3600
3663
|
this.slotBooked.emit({ ...slot, booked: true });
|
|
3601
3664
|
return;
|
|
3602
3665
|
}
|
|
@@ -3630,7 +3693,15 @@ class CalendarComponent {
|
|
|
3630
3693
|
const newStart = new Date(target.day);
|
|
3631
3694
|
newStart.setHours(target.hour, target.minute, 0, 0);
|
|
3632
3695
|
const newEnd = new Date(newStart.getTime() + this.dragSlotDurationMin * 60000);
|
|
3633
|
-
|
|
3696
|
+
// Refuse a drop that would land before the cutoff; surface as
|
|
3697
|
+
// slotBlocked so the consumer can show feedback if they want.
|
|
3698
|
+
const min = this.minSlotStart();
|
|
3699
|
+
if (min && newStart.getTime() < min.getTime()) {
|
|
3700
|
+
this.slotBlocked.emit({ start: newStart, end: newEnd, reason: 'before-min' });
|
|
3701
|
+
}
|
|
3702
|
+
else {
|
|
3703
|
+
this.slotUpdated.emit({ ...this.draggingSlot, start: newStart, end: newEnd });
|
|
3704
|
+
}
|
|
3634
3705
|
}
|
|
3635
3706
|
}
|
|
3636
3707
|
else {
|
|
@@ -3682,14 +3753,35 @@ class CalendarComponent {
|
|
|
3682
3753
|
}
|
|
3683
3754
|
// ── Modal open ─────────────────────────────────────────────────────────────
|
|
3684
3755
|
openAddModal(day, hour, minute) {
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3756
|
+
// Fall back to minSlotStart (rounded up to the next slotDuration boundary)
|
|
3757
|
+
// when no specific cell was clicked — keeps the toolbar "Add slot" button
|
|
3758
|
+
// from prefilling with a time that fails server-side validation.
|
|
3759
|
+
let startDate;
|
|
3760
|
+
if (day && hour !== undefined && minute !== undefined) {
|
|
3761
|
+
startDate = this.combineDayTime(day, hour, minute);
|
|
3762
|
+
}
|
|
3763
|
+
else if (day) {
|
|
3764
|
+
startDate = new Date(day);
|
|
3765
|
+
startDate.setHours(hour ?? startDate.getHours(), minute ?? 0, 0, 0);
|
|
3766
|
+
}
|
|
3767
|
+
else {
|
|
3768
|
+
// No specific cell clicked — anchor on the day the user is currently
|
|
3769
|
+
// viewing (so the modal's date matches the visible week/day) and then
|
|
3770
|
+
// bump forward if it sits before the cutoff.
|
|
3771
|
+
const anchor = new Date(this.navDate());
|
|
3772
|
+
const min = this.minSlotStart();
|
|
3773
|
+
if (min && anchor.getTime() < min.getTime()) {
|
|
3774
|
+
startDate = this.roundUpToSlot(min);
|
|
3775
|
+
}
|
|
3776
|
+
else {
|
|
3777
|
+
// Keep the navigated day, but pick a sensible default time-of-day
|
|
3778
|
+
// (start of the visible hour range) instead of "now".
|
|
3779
|
+
anchor.setHours(this.minHour(), 0, 0, 0);
|
|
3780
|
+
startDate = this.roundUpToSlot(anchor);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
const endDate = new Date(startDate.getTime() + this.slotDuration() * 60000);
|
|
3784
|
+
const base = new Date(startDate);
|
|
3693
3785
|
const form = this.buildForm({
|
|
3694
3786
|
date: new Date(base),
|
|
3695
3787
|
startTime: startDate,
|
|
@@ -3703,7 +3795,18 @@ class CalendarComponent {
|
|
|
3703
3795
|
formGroup: form,
|
|
3704
3796
|
forms: this.slotForms(),
|
|
3705
3797
|
formErrors: this.slotFormErrors,
|
|
3706
|
-
save: (v) =>
|
|
3798
|
+
save: (v) => {
|
|
3799
|
+
const slot = this.buildSlot(crypto.randomUUID(), v);
|
|
3800
|
+
// Last-line defence: refuse if a hand-edited time slipped below
|
|
3801
|
+
// the cutoff. The cross-field validator already surfaces a friendly
|
|
3802
|
+
// error in the modal — this just guarantees we never emit a bad slot.
|
|
3803
|
+
const min = this.minSlotStart();
|
|
3804
|
+
if (min && slot.start.getTime() < min.getTime()) {
|
|
3805
|
+
this.slotBlocked.emit({ start: slot.start, end: slot.end, reason: 'before-min' });
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
this.slotAdded.emit(slot);
|
|
3809
|
+
},
|
|
3707
3810
|
},
|
|
3708
3811
|
});
|
|
3709
3812
|
form.get('startTime')?.valueChanges
|
|
@@ -3714,6 +3817,17 @@ class CalendarComponent {
|
|
|
3714
3817
|
form.get('endTime')?.setValue(this.shiftTime(time, this.slotDuration()));
|
|
3715
3818
|
});
|
|
3716
3819
|
}
|
|
3820
|
+
/** Round a Date up to the next `slotDuration` increment (minute-aligned). */
|
|
3821
|
+
roundUpToSlot(d) {
|
|
3822
|
+
const out = new Date(d);
|
|
3823
|
+
out.setSeconds(0, 0);
|
|
3824
|
+
const step = this.slotDuration();
|
|
3825
|
+
const remainder = out.getMinutes() % step;
|
|
3826
|
+
if (remainder > 0 || out.getTime() <= d.getTime()) {
|
|
3827
|
+
out.setMinutes(out.getMinutes() + (step - remainder));
|
|
3828
|
+
}
|
|
3829
|
+
return out;
|
|
3830
|
+
}
|
|
3717
3831
|
openEditModal(slot) {
|
|
3718
3832
|
const form = this.buildForm({
|
|
3719
3833
|
title: slot.title ?? '',
|
|
@@ -3735,6 +3849,11 @@ class CalendarComponent {
|
|
|
3735
3849
|
formErrors: this.slotFormErrors,
|
|
3736
3850
|
save: (v) => {
|
|
3737
3851
|
const updated = this.buildSlot(slot.id, v);
|
|
3852
|
+
const min = this.minSlotStart();
|
|
3853
|
+
if (min && updated.start.getTime() < min.getTime()) {
|
|
3854
|
+
this.slotBlocked.emit({ start: updated.start, end: updated.end, reason: 'before-min' });
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3738
3857
|
if (updated.booked && !slot.booked) {
|
|
3739
3858
|
this.slotBooked.emit(updated);
|
|
3740
3859
|
}
|
|
@@ -3758,14 +3877,20 @@ class CalendarComponent {
|
|
|
3758
3877
|
// Error messages shown by the modal for group-level (cross-field) errors.
|
|
3759
3878
|
get slotFormErrors() {
|
|
3760
3879
|
const range = `${this.formatHour(this.minHour(), 0)}–${this.formatHour(this.maxHour(), 0)}`;
|
|
3880
|
+
const min = this.minSlotStart();
|
|
3761
3881
|
return {
|
|
3762
3882
|
endBeforeStart: 'End time must be after the start time.',
|
|
3763
3883
|
outOfHours: `Times must be within working hours (${range}).`,
|
|
3884
|
+
beforeMinStart: min
|
|
3885
|
+
? `Slot must start at or after ${this.formatTime(min)} on ${min.toLocaleDateString()}.`
|
|
3886
|
+
: 'Slot must start in the future.',
|
|
3764
3887
|
};
|
|
3765
3888
|
}
|
|
3766
3889
|
// Cross-field validator: the end time must be strictly after the start time,
|
|
3767
3890
|
// and both must fall inside the calendar's [minHour, maxHour] working window.
|
|
3891
|
+
// When `minSlotStart` is set, the start of the slot must also be at/after it.
|
|
3768
3892
|
slotTimeValidator = (group) => {
|
|
3893
|
+
const date = group.get('date')?.value;
|
|
3769
3894
|
const start = group.get('startTime')?.value;
|
|
3770
3895
|
const end = group.get('endTime')?.value;
|
|
3771
3896
|
if (!start || !end)
|
|
@@ -3776,7 +3901,15 @@ class CalendarComponent {
|
|
|
3776
3901
|
const hi = this.maxHour() * 60;
|
|
3777
3902
|
if (startMin < lo || startMin > hi || endMin < lo || endMin > hi)
|
|
3778
3903
|
return { outOfHours: true };
|
|
3779
|
-
|
|
3904
|
+
if (endMin <= startMin)
|
|
3905
|
+
return { endBeforeStart: true };
|
|
3906
|
+
const min = this.minSlotStart();
|
|
3907
|
+
if (min && date) {
|
|
3908
|
+
const slotStart = this.combineDateTime(date, start);
|
|
3909
|
+
if (slotStart.getTime() < min.getTime())
|
|
3910
|
+
return { beforeMinStart: true };
|
|
3911
|
+
}
|
|
3912
|
+
return null;
|
|
3780
3913
|
};
|
|
3781
3914
|
buildForm(defaults) {
|
|
3782
3915
|
return new FormGroup({
|
|
@@ -3799,9 +3932,16 @@ class CalendarComponent {
|
|
|
3799
3932
|
availableOptions: users,
|
|
3800
3933
|
}
|
|
3801
3934
|
: { type: ModalInputType.INPUT, title: 'Booked By', property: 'bookedBy', placeholder: 'Student name or ID' };
|
|
3935
|
+
// Pass the cutoff (date-only) into the date picker so users can't pick a
|
|
3936
|
+
// past day from the calendar. We strip the time so the *whole* cutoff day
|
|
3937
|
+
// is still selectable — time-of-day enforcement happens in startTime/endTime.
|
|
3938
|
+
const minDate = this.minSlotStart();
|
|
3939
|
+
const dateMin = minDate
|
|
3940
|
+
? new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate())
|
|
3941
|
+
: undefined;
|
|
3802
3942
|
return [
|
|
3803
3943
|
{ type: ModalInputType.INPUT, title: 'Title', property: 'title', placeholder: 'e.g. Office hours' },
|
|
3804
|
-
{ type: ModalInputType.DATETIME, title: 'Date', property: 'date' },
|
|
3944
|
+
{ type: ModalInputType.DATETIME, title: 'Date', property: 'date', min: dateMin },
|
|
3805
3945
|
{ type: ModalInputType.TIME, title: 'Start Time', property: 'startTime', intervalMinutes: this.slotDuration(), minHour: this.minHour(), maxHour: this.maxHour() },
|
|
3806
3946
|
{ type: ModalInputType.TIME, title: 'End Time', property: 'endTime', intervalMinutes: this.slotDuration(), minHour: this.minHour(), maxHour: this.maxHour() },
|
|
3807
3947
|
bookedByField,
|
|
@@ -3877,11 +4017,11 @@ class CalendarComponent {
|
|
|
3877
4017
|
return `rgba(${r},${g},${b},${alpha})`;
|
|
3878
4018
|
}
|
|
3879
4019
|
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"] }] });
|
|
4020
|
+
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 } }, 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
4021
|
}
|
|
3882
4022
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: CalendarComponent, decorators: [{
|
|
3883
4023
|
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"] }]
|
|
4024
|
+
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
4025
|
}], ctorParameters: () => [], propDecorators: { onDocMouseMove: [{
|
|
3886
4026
|
type: HostListener,
|
|
3887
4027
|
args: ['document:mousemove', ['$event']]
|
|
@@ -3978,18 +4118,25 @@ class UdPreviewContainerComponent {
|
|
|
3978
4118
|
title = '';
|
|
3979
4119
|
icon;
|
|
3980
4120
|
padding = '24px';
|
|
4121
|
+
/** Whether the panel starts collapsed. */
|
|
4122
|
+
collapsed = false;
|
|
4123
|
+
toggle() {
|
|
4124
|
+
this.collapsed = !this.collapsed;
|
|
4125
|
+
}
|
|
3981
4126
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: UdPreviewContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3982
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: UdPreviewContainerComponent, isStandalone: true, selector: "ud-preview-container", inputs: { title: "title", icon: "icon", padding: "padding" }, ngImport: i0, template: "<div class=\"preview-container\">\n <
|
|
4127
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: UdPreviewContainerComponent, isStandalone: true, selector: "ud-preview-container", inputs: { title: "title", icon: "icon", padding: "padding", collapsed: "collapsed" }, ngImport: i0, template: "<div class=\"preview-container\" [class.preview-container--collapsed]=\"collapsed\">\n <button class=\"section-title\" type=\"button\" (click)=\"toggle()\" [attr.aria-expanded]=\"!collapsed\">\n @if (icon) {\n <span class=\"material-icons\">{{ icon }}</span>\n }\n <span class=\"section-title__text\">{{ title }}</span>\n <span class=\"material-icons section-title__chevron\">expand_more</span>\n </button>\n @if (!collapsed) {\n <div class=\"section-content\" [style.padding]=\"padding\">\n <ng-content />\n </div>\n }\n</div>\n", styles: [":host{display:block}.preview-container{background:#fff;border:1px solid #e1e4e8;border-radius:8px;margin-bottom:24px;overflow:hidden}.preview-container .section-title{width:100%;padding:14px 20px;background:#f6f8fa;border:none;border-bottom:1px solid #e1e4e8;font-family:inherit;font-size:13px;font-weight:600;color:#374151;display:flex;align-items:center;gap:8px;cursor:pointer;text-align:left;transition:background .15s}.preview-container .section-title:hover{background:#eef1f5}.preview-container .section-title .material-icons{font-size:16px;color:#6c7086}.preview-container .section-title .section-title__text{flex:1}.preview-container .section-title .section-title__chevron{transition:transform .2s ease}.preview-container--collapsed .section-title{border-bottom:none}.preview-container--collapsed .section-title__chevron{transform:rotate(-90deg)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
3983
4128
|
}
|
|
3984
4129
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: UdPreviewContainerComponent, decorators: [{
|
|
3985
4130
|
type: Component,
|
|
3986
|
-
args: [{ selector: 'ud-preview-container', standalone: true, imports: [CommonModule], template: "<div class=\"preview-container\">\n <
|
|
4131
|
+
args: [{ selector: 'ud-preview-container', standalone: true, imports: [CommonModule], template: "<div class=\"preview-container\" [class.preview-container--collapsed]=\"collapsed\">\n <button class=\"section-title\" type=\"button\" (click)=\"toggle()\" [attr.aria-expanded]=\"!collapsed\">\n @if (icon) {\n <span class=\"material-icons\">{{ icon }}</span>\n }\n <span class=\"section-title__text\">{{ title }}</span>\n <span class=\"material-icons section-title__chevron\">expand_more</span>\n </button>\n @if (!collapsed) {\n <div class=\"section-content\" [style.padding]=\"padding\">\n <ng-content />\n </div>\n }\n</div>\n", styles: [":host{display:block}.preview-container{background:#fff;border:1px solid #e1e4e8;border-radius:8px;margin-bottom:24px;overflow:hidden}.preview-container .section-title{width:100%;padding:14px 20px;background:#f6f8fa;border:none;border-bottom:1px solid #e1e4e8;font-family:inherit;font-size:13px;font-weight:600;color:#374151;display:flex;align-items:center;gap:8px;cursor:pointer;text-align:left;transition:background .15s}.preview-container .section-title:hover{background:#eef1f5}.preview-container .section-title .material-icons{font-size:16px;color:#6c7086}.preview-container .section-title .section-title__text{flex:1}.preview-container .section-title .section-title__chevron{transition:transform .2s ease}.preview-container--collapsed .section-title{border-bottom:none}.preview-container--collapsed .section-title__chevron{transform:rotate(-90deg)}\n"] }]
|
|
3987
4132
|
}], propDecorators: { title: [{
|
|
3988
4133
|
type: Input
|
|
3989
4134
|
}], icon: [{
|
|
3990
4135
|
type: Input
|
|
3991
4136
|
}], padding: [{
|
|
3992
4137
|
type: Input
|
|
4138
|
+
}], collapsed: [{
|
|
4139
|
+
type: Input
|
|
3993
4140
|
}] } });
|
|
3994
4141
|
|
|
3995
4142
|
class ToggleOptionComponent {
|