mn-angular-lib 0.0.52 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, Injectable, Optional, Inject, inject, Input, ChangeDetectionStrategy, Component, HostBinding, signal, ElementRef, DestroyRef, Self, APP_INITIALIZER, HostListener, forwardRef, Directive, EventEmitter, TemplateRef, Output, ViewContainerRef, ViewChild, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent, SkipSelf, Attribute, Pipe } from '@angular/core';
3
3
  export { TemplateRef, Type } from '@angular/core';
4
- import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, map, catchError, of } from 'rxjs';
4
+ import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule, NgClass, NgOptimizedImage, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
7
7
  import { tv } from 'tailwind-variants';
@@ -4861,6 +4861,1274 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4861
4861
 
4862
4862
  // Types
4863
4863
 
4864
+ /**
4865
+ * Available calendar view modes.
4866
+ */
4867
+ var CalendarView;
4868
+ (function (CalendarView) {
4869
+ CalendarView["MONTH"] = "MONTH";
4870
+ CalendarView["WEEK"] = "WEEK";
4871
+ CalendarView["DAY"] = "DAY";
4872
+ })(CalendarView || (CalendarView = {}));
4873
+ /**
4874
+ * Builds locale-derived day name arrays from a BCP 47 locale string.
4875
+ * Uses January 1 2024 (a Monday) as the reference date.
4876
+ */
4877
+ function buildDayNames(locale) {
4878
+ const base = new Date(2024, 0, 1); // 2024-01-01 is a Monday
4879
+ const short = [];
4880
+ const long = [];
4881
+ for (let i = 0; i < 7; i++) {
4882
+ const d = new Date(base);
4883
+ d.setDate(base.getDate() + i);
4884
+ short.push(d.toLocaleDateString(locale, { weekday: 'short' }));
4885
+ long.push(d.toLocaleDateString(locale, { weekday: 'long' }));
4886
+ }
4887
+ return { short, long };
4888
+ }
4889
+ /** Default calendar configuration values. */
4890
+ const DEFAULT_CALENDAR_CONFIG = (() => {
4891
+ const locale = 'en-US';
4892
+ const names = buildDayNames(locale);
4893
+ return {
4894
+ startHour: 7,
4895
+ endHour: 22,
4896
+ locale,
4897
+ todayLabel: 'Today',
4898
+ upcomingEventsTitle: 'Upcoming events',
4899
+ viewLabels: { MONTH: 'Month', WEEK: 'Week', DAY: 'Day' },
4900
+ shortDayNames: names.short,
4901
+ longDayNames: names.long,
4902
+ mobileBreakpoint: 768,
4903
+ };
4904
+ })();
4905
+ /**
4906
+ * Injection token for the resolved calendar configuration.
4907
+ *
4908
+ * Prefer using {@link MN_CALENDAR_CONFIG} with `provideMnComponentConfig`
4909
+ * so that settings can be managed via `mn-config.json5`. This token is
4910
+ * kept for backward compatibility and manual `providers` usage.
4911
+ *
4912
+ * @example
4913
+ * ```ts
4914
+ * providers: [
4915
+ * { provide: CALENDAR_CONFIG, useValue: { startHour: 8, endHour: 20, locale: 'nl-NL' } }
4916
+ * ]
4917
+ * ```
4918
+ */
4919
+ const CALENDAR_CONFIG = new InjectionToken('CalendarConfig', {
4920
+ providedIn: 'root',
4921
+ factory: () => DEFAULT_CALENDAR_CONFIG
4922
+ });
4923
+ /**
4924
+ * Injection token resolved via `MnConfigService` (the `mn-config.json5` system).
4925
+ *
4926
+ * Use the helper {@link provideMnCalendarConfig} in the component's `providers`
4927
+ * array so that calendar settings are read from the config file and support
4928
+ * `$translate` markers, section scoping, and instance-id overrides.
4929
+ *
4930
+ * Component name in the config file: `'mn-calendar'`.
4931
+ *
4932
+ * @example
4933
+ * ```json5
4934
+ * // mn-config.json5
4935
+ * {
4936
+ * defaults: {
4937
+ * "mn-calendar": {
4938
+ * startHour: 8,
4939
+ * endHour: 20,
4940
+ * locale: "nl-NL",
4941
+ * todayLabel: { $translate: "calendar.today" }
4942
+ * }
4943
+ * }
4944
+ * }
4945
+ * ```
4946
+ */
4947
+ const MN_CALENDAR_CONFIG = new InjectionToken('MN_CALENDAR_CONFIG');
4948
+ /** Component name used to look up calendar settings in `mn-config.json5`. */
4949
+ const MN_CALENDAR_COMPONENT_NAME = 'mn-calendar';
4950
+ /**
4951
+ * Provider helper that wires the calendar into the `mn-config` system.
4952
+ *
4953
+ * Add this to the `providers` array of the component (or module) that hosts
4954
+ * `<app-calendar-view>`. It reads defaults and overrides from `mn-config.json5`
4955
+ * under the key `"mn-calendar"` and provides them via {@link MN_CALENDAR_CONFIG}.
4956
+ *
4957
+ * @param initial — optional partial defaults merged before config-file values.
4958
+ */
4959
+ function provideMnCalendarConfig(initial) {
4960
+ return provideMnComponentConfig(MN_CALENDAR_CONFIG, MN_CALENDAR_COMPONENT_NAME, initial);
4961
+ }
4962
+ /**
4963
+ * Merges a partial config with defaults, re-deriving day names from locale when needed.
4964
+ */
4965
+ function resolveCalendarConfig(partial) {
4966
+ if (!partial)
4967
+ return { ...DEFAULT_CALENDAR_CONFIG };
4968
+ const locale = partial.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
4969
+ const names = buildDayNames(locale);
4970
+ return {
4971
+ ...DEFAULT_CALENDAR_CONFIG,
4972
+ ...partial,
4973
+ locale,
4974
+ shortDayNames: partial.shortDayNames ?? names.short,
4975
+ longDayNames: partial.longDayNames ?? names.long,
4976
+ };
4977
+ }
4978
+
4979
+ /**
4980
+ * Injection token for the calendar date formatter.
4981
+ *
4982
+ * @example
4983
+ * ```ts
4984
+ * providers: [
4985
+ * { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
4986
+ * ]
4987
+ * ```
4988
+ */
4989
+ const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
4990
+
4991
+ /**
4992
+ * Default implementation of {@link CalendarDateFormatter} that uses the
4993
+ * browser's `Intl.DateTimeFormat` API for locale-aware formatting.
4994
+ *
4995
+ * The locale is read from the injected {@link CALENDAR_CONFIG}. If no config
4996
+ * is provided, `'en-US'` is used as the fallback.
4997
+ *
4998
+ * This service has no dependency on `@ngx-translate` or any other i18n library,
4999
+ * so the calendar library works out of the box. Consumers can replace it with
5000
+ * their own implementation via the `CALENDAR_DATE_FORMATTER` injection token.
5001
+ */
5002
+ class DefaultCalendarDateFormatter {
5003
+ locale;
5004
+ constructor(config) {
5005
+ this.locale = config?.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
5006
+ }
5007
+ /** Formats an hour and minute pair into a locale time string (e.g. "09:00 AM"). */
5008
+ formatTimeI(hour, minute) {
5009
+ const date = new Date();
5010
+ date.setHours(hour, minute, 0, 0);
5011
+ return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
5012
+ }
5013
+ /** Formats the time portion of a Date (e.g. "2:30 PM"). Returns empty string for undefined. */
5014
+ formatTime(date) {
5015
+ if (!date)
5016
+ return Promise.resolve('');
5017
+ return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
5018
+ }
5019
+ /** Formats a Date as a full date-time string (e.g. "May 15, 2026, 02:30 PM"). */
5020
+ formatDateTime(date) {
5021
+ return of(date.toLocaleString(this.locale, {
5022
+ year: 'numeric', month: 'short', day: 'numeric',
5023
+ hour: '2-digit', minute: '2-digit'
5024
+ }));
5025
+ }
5026
+ /** Formats a Date as a date-only string (e.g. "May 15, 2026"). */
5027
+ formatDate(date) {
5028
+ return of(date.toLocaleDateString(this.locale, {
5029
+ year: 'numeric', month: 'short', day: 'numeric'
5030
+ }));
5031
+ }
5032
+ /** Formats a Date as `YYYY-MM-DD` for use in `<input type="date">` controls. */
5033
+ formatDateForFormControl(date) {
5034
+ const y = date.getFullYear();
5035
+ const m = String(date.getMonth() + 1).padStart(2, '0');
5036
+ const d = String(date.getDate()).padStart(2, '0');
5037
+ return `${y}-${m}-${d}`;
5038
+ }
5039
+ /** Returns `true` if both dates fall on the same calendar day. */
5040
+ isSameDay(date1, date2) {
5041
+ return date1.getFullYear() === date2.getFullYear()
5042
+ && date1.getMonth() === date2.getMonth()
5043
+ && date1.getDate() === date2.getDate();
5044
+ }
5045
+ /** Formats a Date as "Month Year" (e.g. "January 2026"). */
5046
+ formatMonthName(date) {
5047
+ return Promise.resolve(date.toLocaleString(this.locale, { month: 'long', year: 'numeric' }));
5048
+ }
5049
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, deps: [{ token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
5050
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter });
5051
+ }
5052
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, decorators: [{
5053
+ type: Injectable
5054
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5055
+ type: Optional
5056
+ }, {
5057
+ type: Inject,
5058
+ args: [CALENDAR_CONFIG]
5059
+ }] }] });
5060
+
5061
+ /**
5062
+ * Month grid view showing a 7×6 grid of day cells.
5063
+ *
5064
+ * Each cell displays the day number and up to 3 coloured dots representing
5065
+ * events on that day. Clicking a cell emits `dayClicked`.
5066
+ */
5067
+ class CalendarMonthComponent {
5068
+ /** The date whose month is displayed. */
5069
+ focusDay;
5070
+ /** Observable that emits the full event list whenever it changes. */
5071
+ eventsChanged;
5072
+ /** Observable that emits when the focus day changes. */
5073
+ focusDayChanged;
5074
+ /** Resolved calendar configuration passed from the parent view. */
5075
+ config;
5076
+ /** Emits the date of a clicked day cell. */
5077
+ dayClicked = new EventEmitter();
5078
+ monthItems = [];
5079
+ longDayNames;
5080
+ events = [];
5081
+ destroy$ = new Subject();
5082
+ formatter;
5083
+ constructor() {
5084
+ this.formatter = new DefaultCalendarDateFormatter();
5085
+ this.longDayNames = DEFAULT_CALENDAR_CONFIG.longDayNames;
5086
+ }
5087
+ ngOnInit() {
5088
+ const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5089
+ this.longDayNames = resolved.longDayNames;
5090
+ this.buildMonth();
5091
+ if (this.eventsChanged) {
5092
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5093
+ this.events = events;
5094
+ this.buildMonth();
5095
+ });
5096
+ }
5097
+ if (this.focusDayChanged) {
5098
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5099
+ this.focusDay = date;
5100
+ this.buildMonth();
5101
+ });
5102
+ }
5103
+ }
5104
+ ngOnDestroy() {
5105
+ this.destroy$.next();
5106
+ this.destroy$.complete();
5107
+ }
5108
+ /** Emits the clicked day's date. */
5109
+ onDayClick(date) {
5110
+ this.dayClicked.emit(date);
5111
+ }
5112
+ /** trackBy for day name headers. */
5113
+ trackByDayName(index) {
5114
+ return index;
5115
+ }
5116
+ /** trackBy for month grid cells. */
5117
+ trackByMonthItem(_index, item) {
5118
+ return item.date.getTime();
5119
+ }
5120
+ /** trackBy for event dots. */
5121
+ trackByEventDot(_index, event) {
5122
+ return event.id;
5123
+ }
5124
+ /** Builds the 42-cell month grid (6 rows × 7 columns). */
5125
+ buildMonth() {
5126
+ if (!this.focusDay)
5127
+ return;
5128
+ const year = this.focusDay.getFullYear();
5129
+ const month = this.focusDay.getMonth();
5130
+ const firstDay = new Date(year, month, 1);
5131
+ const lastDay = new Date(year, month + 1, 0);
5132
+ let startOffset = firstDay.getDay() - 1;
5133
+ if (startOffset < 0)
5134
+ startOffset = 6;
5135
+ const today = new Date();
5136
+ this.monthItems = [];
5137
+ for (let i = startOffset - 1; i >= 0; i--) {
5138
+ const date = new Date(year, month, -i);
5139
+ this.monthItems.push(this.createMonthItem(date, false, today));
5140
+ }
5141
+ for (let d = 1; d <= lastDay.getDate(); d++) {
5142
+ const date = new Date(year, month, d);
5143
+ this.monthItems.push(this.createMonthItem(date, true, today));
5144
+ }
5145
+ const remaining = 42 - this.monthItems.length;
5146
+ for (let i = 1; i <= remaining; i++) {
5147
+ const date = new Date(year, month + 1, i);
5148
+ this.monthItems.push(this.createMonthItem(date, false, today));
5149
+ }
5150
+ }
5151
+ createMonthItem(date, isCurrentMonth, today) {
5152
+ const isToday = this.formatter.isSameDay(date, today);
5153
+ const dayEvents = this.events.filter(e => this.formatter.isSameDay(e.startTime, date) ||
5154
+ this.formatter.isSameDay(e.endTime, date) ||
5155
+ (e.startTime < date && e.endTime > date));
5156
+ return {
5157
+ date,
5158
+ dayNumber: date.getDate(),
5159
+ isCurrentMonth,
5160
+ isToday,
5161
+ events: dayEvents
5162
+ };
5163
+ }
5164
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5165
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "app-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
5166
+ }
5167
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
5168
+ type: Component,
5169
+ args: [{ selector: 'app-calendar-month', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-month\" role=\"grid\" aria-label=\"Month view\">\n <div class=\"month-header\">\n <div class=\"day-header\" *ngFor=\"let day of longDayNames; trackBy: trackByDayName\" role=\"columnheader\">{{ day }}</div>\n </div>\n <div class=\"month-grid\">\n <div\n class=\"month-cell\"\n *ngFor=\"let item of monthItems; trackBy: trackByMonthItem\"\n [class.other-month]=\"!item.isCurrentMonth\"\n [class.today]=\"item.isToday\"\n (click)=\"onDayClick(item.date)\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"day-number\">{{ item.dayNumber }}</span>\n <div class=\"month-events\">\n <div\n class=\"month-event-dot\"\n *ngFor=\"let event of item.events.slice(0, 3); trackBy: trackByEventDot\"\n [style.background-color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n </div>\n <span class=\"more-events\" *ngIf=\"item.events.length > 3\">+{{ item.events.length - 3 }}</span>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-month{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.month-header{display:grid;grid-template-columns:repeat(7,1fr);text-align:center;font-weight:600;font-size:13px;padding:8px 0;border-bottom:1px solid #e5e7eb}.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);flex:1;min-height:0}.month-cell{min-height:0;padding:4px 8px;border:1px solid #f3f4f6;cursor:pointer;transition:background .15s}.month-cell:hover{background:#f9fafb}.month-cell.other-month{opacity:.4}.month-cell.today .day-number{background:#3b82f6;color:#fff;border-radius:50%;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center}.day-number{font-size:13px;font-weight:500}.month-events{display:flex;gap:2px;flex-wrap:wrap;margin-top:4px}.month-event-dot{width:8px;height:8px;border-radius:50%}.more-events{font-size:10px;color:#6b7280}\n"] }]
5170
+ }], ctorParameters: () => [], propDecorators: { focusDay: [{
5171
+ type: Input
5172
+ }], eventsChanged: [{
5173
+ type: Input
5174
+ }], focusDayChanged: [{
5175
+ type: Input
5176
+ }], config: [{
5177
+ type: Input
5178
+ }], dayClicked: [{
5179
+ type: Output
5180
+ }] } });
5181
+
5182
+ /**
5183
+ * Service that computes the visual layout of calendar events within a
5184
+ * time-grid (week or day view).
5185
+ *
5186
+ * Responsibilities:
5187
+ * - Splitting multi-day events into per-day segments.
5188
+ * - Assigning non-overlapping column indices to concurrent events.
5189
+ * - Computing the width (column span) each event should occupy.
5190
+ *
5191
+ * This service is stateless — all state is passed via method parameters.
5192
+ * Provide it per-component (not root) so each view gets its own instance.
5193
+ */
5194
+ class CalendarEventLayoutService {
5195
+ /**
5196
+ * Returns `true` when two time ranges overlap (exclusive boundaries).
5197
+ */
5198
+ eventsOverlap(startA, endA, startB, endB) {
5199
+ return startA < endB && startB < endA;
5200
+ }
5201
+ /**
5202
+ * Returns all events whose time range overlaps the given `[start, end)` window.
5203
+ */
5204
+ getAllEventsOnSpecificTime(events, start, end) {
5205
+ return events.filter(e => this.eventsOverlap(e.startTime, e.endTime, start, end));
5206
+ }
5207
+ /**
5208
+ * Splits multi-day events into per-day segments that fit within the
5209
+ * visible hour range (`startHour`–`endHour`) and date range.
5210
+ *
5211
+ * Single-day events are shallow-copied as-is. Multi-day events produce
5212
+ * one segment per day with `continued` / `continuedEnd` flags set.
5213
+ */
5214
+ calculateMultiDayEvents(events, startHour, endHour, rangeStart, rangeEnd) {
5215
+ const result = [];
5216
+ for (const event of events) {
5217
+ const eventStart = new Date(event.startTime);
5218
+ const eventEnd = new Date(event.endTime);
5219
+ if (eventStart.toDateString() === eventEnd.toDateString()) {
5220
+ result.push({ ...event });
5221
+ continue;
5222
+ }
5223
+ const current = new Date(eventStart);
5224
+ let isFirst = true;
5225
+ while (current < eventEnd && current < rangeEnd) {
5226
+ if (current >= rangeStart) {
5227
+ const dayStart = new Date(current);
5228
+ const dayEnd = new Date(current);
5229
+ if (isFirst) {
5230
+ dayEnd.setHours(endHour, 0, 0, 0);
5231
+ }
5232
+ else {
5233
+ dayStart.setHours(startHour, 0, 0, 0);
5234
+ if (current.toDateString() === eventEnd.toDateString()) {
5235
+ dayEnd.setHours(eventEnd.getHours(), eventEnd.getMinutes(), 0, 0);
5236
+ }
5237
+ else {
5238
+ dayEnd.setHours(endHour, 0, 0, 0);
5239
+ }
5240
+ }
5241
+ result.push({
5242
+ ...event,
5243
+ startTime: isFirst ? eventStart : dayStart,
5244
+ endTime: current.toDateString() === eventEnd.toDateString() ? eventEnd : dayEnd,
5245
+ continued: !isFirst,
5246
+ continuedEnd: current.toDateString() !== eventEnd.toDateString()
5247
+ });
5248
+ }
5249
+ isFirst = false;
5250
+ current.setDate(current.getDate() + 1);
5251
+ current.setHours(0, 0, 0, 0);
5252
+ }
5253
+ }
5254
+ return result;
5255
+ }
5256
+ /**
5257
+ * Assigns a zero-based `column` index to each event so that overlapping
5258
+ * events occupy different columns.
5259
+ *
5260
+ * Events are processed in start-time order (longest duration first for ties).
5261
+ * Each event gets the earliest column not already occupied by an overlapping event.
5262
+ */
5263
+ assignColumnsToEvents(events) {
5264
+ const sorted = [...events].sort((a, b) => {
5265
+ const diff = a.startTime.getTime() - b.startTime.getTime();
5266
+ if (diff !== 0)
5267
+ return diff;
5268
+ return (b.endTime.getTime() - b.startTime.getTime()) - (a.endTime.getTime() - a.startTime.getTime());
5269
+ });
5270
+ for (const event of sorted) {
5271
+ event.column = this.findEarliestPossibleColumn(event, sorted);
5272
+ }
5273
+ }
5274
+ /**
5275
+ * Assigns a `width` (column span) to each event, expanding it to fill
5276
+ * unused columns to its right within the overlapping group.
5277
+ */
5278
+ assignWidthsToEvents(events, scanStart, scanEnd) {
5279
+ for (const event of events) {
5280
+ const overlapping = this.getAllEventsOnSpecificTime(events, event.startTime, event.endTime);
5281
+ const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
5282
+ const biggestPossible = this.findBiggestPossibleWidth(event, events, scanStart, scanEnd);
5283
+ event.width = Math.max(1, biggestPossible);
5284
+ if ((event.column ?? 0) + event.width > maxCol + 1) {
5285
+ event.width = maxCol + 1 - (event.column ?? 0);
5286
+ }
5287
+ if (event.width < 1)
5288
+ event.width = 1;
5289
+ }
5290
+ }
5291
+ /** Finds the lowest column index not occupied by any overlapping event. */
5292
+ findEarliestPossibleColumn(event, allEvents) {
5293
+ const overlapping = allEvents.filter(e => e !== event
5294
+ && e.column !== undefined
5295
+ && this.eventsOverlap(e.startTime, e.endTime, event.startTime, event.endTime));
5296
+ let column = 0;
5297
+ while (overlapping.some(e => e.column === column)) {
5298
+ column++;
5299
+ }
5300
+ return column;
5301
+ }
5302
+ /** Computes the maximum width an event can span without overlapping a neighbour to its right. */
5303
+ findBiggestPossibleWidth(event, allEvents, _scanStart, _scanEnd) {
5304
+ const overlapping = this.getAllEventsOnSpecificTime(allEvents, event.startTime, event.endTime);
5305
+ const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
5306
+ const totalColumns = maxCol + 1;
5307
+ const occupiedCols = overlapping
5308
+ .filter(e => e !== event && (e.column ?? 0) > (event.column ?? 0))
5309
+ .map(e => e.column ?? 0);
5310
+ if (occupiedCols.length === 0) {
5311
+ return totalColumns - (event.column ?? 0);
5312
+ }
5313
+ return Math.min(...occupiedCols) - (event.column ?? 0);
5314
+ }
5315
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5316
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService });
5317
+ }
5318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, decorators: [{
5319
+ type: Injectable
5320
+ }] });
5321
+
5322
+ /**
5323
+ * Static utility methods for calendar grid positioning.
5324
+ */
5325
+ class CalendarUtility {
5326
+ /**
5327
+ * Converts a weekday (from `Date.getDay()`) to a 1-based Monday-first column index.
5328
+ * Monday = 1, Tuesday = 2, …, Sunday = 7.
5329
+ */
5330
+ static getCorrectColumn(date) {
5331
+ const day = date.getDay();
5332
+ return day === 0 ? 7 : day;
5333
+ }
5334
+ /**
5335
+ * Converts an hour + minute pair to a 1-based CSS grid row index
5336
+ * within a half-hour grid starting at `startHour`.
5337
+ *
5338
+ * Each hour occupies two rows (one per 30-minute slot).
5339
+ * Formula: `(hour - startHour) * 2 + (minute >= 30 ? 1 : 0) + 1`
5340
+ *
5341
+ * @returns Grid row number (minimum 1).
5342
+ */
5343
+ static getCorrectRow(hour, minute, startHour) {
5344
+ const hourOffset = hour - startHour;
5345
+ const row = hourOffset * 2 + (minute >= 30 ? 1 : 0) + 1;
5346
+ return Math.max(1, row);
5347
+ }
5348
+ }
5349
+
5350
+ /**
5351
+ * Default event renderer used when no custom component is provided.
5352
+ *
5353
+ * Displays the event title, formatted time range, and optional description
5354
+ * with the event's colour scheme applied as background and left-border accent.
5355
+ */
5356
+ class CalendarEventDefaultComponent {
5357
+ /** The event to render. Set by {@link CalendarEventComponent} after creation. */
5358
+ event;
5359
+ formattedTime = '';
5360
+ formatter;
5361
+ constructor(formatter) {
5362
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5363
+ }
5364
+ async ngOnInit() {
5365
+ if (this.event) {
5366
+ const start = await this.formatter.formatTime(this.event.startTime);
5367
+ const end = await this.formatter.formatTime(this.event.endTime);
5368
+ this.formattedTime = `${start} - ${end}`;
5369
+ }
5370
+ }
5371
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
5372
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventDefaultComponent, isStandalone: true, selector: "app-calendar-event-default", ngImport: i0, template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
5373
+ }
5374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, decorators: [{
5375
+ type: Component,
5376
+ args: [{ selector: 'app-calendar-event-default', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-default\" [style.background-color]=\"event.color.secondaryColor\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedTime }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".calendar-event-default{padding:4px 8px;border-left:3px solid #3b82f6;border-radius:4px;font-size:12px;height:100%;overflow:hidden;cursor:pointer}.event-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.event-time{font-size:11px;opacity:.8}.event-description{font-size:11px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
5377
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5378
+ type: Optional
5379
+ }, {
5380
+ type: Inject,
5381
+ args: [CALENDAR_DATE_FORMATTER]
5382
+ }] }] });
5383
+
5384
+ /**
5385
+ * Dynamic event renderer that injects a custom or default event component
5386
+ * into its view container.
5387
+ *
5388
+ * The component to render is resolved in this order:
5389
+ * 1. `customComponent` input (set on the parent week/day view)
5390
+ * 2. `event.component` (per-event override)
5391
+ * 3. {@link CalendarEventDefaultComponent} (library default)
5392
+ */
5393
+ class CalendarEventComponent {
5394
+ /** The event data to render. */
5395
+ event;
5396
+ /** Optional custom component type that overrides the default renderer. */
5397
+ customComponent;
5398
+ /** Emits when the rendered event is clicked. */
5399
+ eventClicked = new EventEmitter();
5400
+ eventContainer;
5401
+ rendered = false;
5402
+ ngAfterViewInit() {
5403
+ this.renderComponent();
5404
+ }
5405
+ ngOnChanges(changes) {
5406
+ if (this.rendered && (changes['event'] || changes['customComponent'])) {
5407
+ this.renderComponent();
5408
+ }
5409
+ }
5410
+ /** Emits the event click. */
5411
+ onEventClick() {
5412
+ this.eventClicked.emit(this.event);
5413
+ }
5414
+ /** Creates the event component dynamically and sets its `event` property. */
5415
+ renderComponent() {
5416
+ if (!this.eventContainer)
5417
+ return;
5418
+ this.eventContainer.clear();
5419
+ const component = this.customComponent ?? this.event?.component ?? CalendarEventDefaultComponent;
5420
+ const ref = this.eventContainer.createComponent(component);
5421
+ ref.instance.event = this.event;
5422
+ ref.changeDetectorRef.detectChanges();
5423
+ this.rendered = true;
5424
+ }
5425
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5426
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarEventComponent, isStandalone: true, selector: "app-calendar-event", inputs: { event: "event", customComponent: "customComponent" }, outputs: { eventClicked: "eventClicked" }, viewQueries: [{ propertyName: "eventContainer", first: true, predicate: ["eventContainer"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
5427
+ }
5428
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, decorators: [{
5429
+ type: Component,
5430
+ args: [{ selector: 'app-calendar-event', standalone: true, imports: [CommonModule], template: "<div class=\"calendar-event-wrapper\" (click)=\"onEventClick()\">\n <ng-template #eventContainer></ng-template>\n</div>\n", styles: [".calendar-event-wrapper{height:100%;width:100%}\n"] }]
5431
+ }], propDecorators: { event: [{
5432
+ type: Input
5433
+ }], customComponent: [{
5434
+ type: Input
5435
+ }], eventClicked: [{
5436
+ type: Output
5437
+ }], eventContainer: [{
5438
+ type: ViewChild,
5439
+ args: ['eventContainer', { read: ViewContainerRef, static: true }]
5440
+ }] } });
5441
+
5442
+ /**
5443
+ * Week grid view showing 7 day columns with half-hour time slots.
5444
+ *
5445
+ * Overlapping events within the same day are laid out in sub-columns
5446
+ * so they appear side-by-side rather than stacked.
5447
+ */
5448
+ class CalendarWeekComponent {
5449
+ layoutService;
5450
+ /** The date around which the week is centred. */
5451
+ focusDay;
5452
+ /** Observable that emits the full event list whenever it changes. */
5453
+ eventsChanged;
5454
+ /** Observable that emits when the focus day changes. */
5455
+ focusDayChanged;
5456
+ /** Resolved calendar configuration passed from the parent view. */
5457
+ config;
5458
+ /** Optional custom event renderer component. */
5459
+ calendarEventComponent;
5460
+ /** Emits when a calendar event is clicked. */
5461
+ eventClicked = new EventEmitter();
5462
+ columns = [];
5463
+ hourRows = [];
5464
+ displayEvents = [];
5465
+ totalRows = 0;
5466
+ currentTimeRow = 0;
5467
+ currentTimeCol = '';
5468
+ gridTemplateColumns = 'repeat(7, 1fr)';
5469
+ dayColumnMap = [];
5470
+ events = [];
5471
+ destroy$ = new Subject();
5472
+ formatter;
5473
+ resolvedConfig;
5474
+ currentTimeInterval;
5475
+ constructor(layoutService) {
5476
+ this.layoutService = layoutService;
5477
+ this.formatter = new DefaultCalendarDateFormatter();
5478
+ }
5479
+ ngOnInit() {
5480
+ this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5481
+ this.buildHourRows();
5482
+ this.buildColumns();
5483
+ this.updateCurrentTime();
5484
+ this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
5485
+ if (this.eventsChanged) {
5486
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5487
+ this.events = events;
5488
+ this.refreshEvents();
5489
+ });
5490
+ }
5491
+ if (this.focusDayChanged) {
5492
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5493
+ this.focusDay = date;
5494
+ this.buildColumns();
5495
+ this.refreshEvents();
5496
+ this.updateCurrentTime();
5497
+ });
5498
+ }
5499
+ }
5500
+ ngOnDestroy() {
5501
+ this.destroy$.next();
5502
+ this.destroy$.complete();
5503
+ if (this.currentTimeInterval)
5504
+ clearInterval(this.currentTimeInterval);
5505
+ }
5506
+ /** Returns the CSS `grid-row` value for an event based on its start/end times. */
5507
+ getEventRow(event) {
5508
+ const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
5509
+ const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
5510
+ return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
5511
+ }
5512
+ /** Returns the CSS `grid-column` span for a day header, accounting for sub-columns. */
5513
+ getHeaderColumn(dayIndex) {
5514
+ if (!this.dayColumnMap.length)
5515
+ return `${dayIndex + 2} / span 1`;
5516
+ const dayInfo = this.dayColumnMap[dayIndex];
5517
+ return `${dayInfo.startCol + 1} / span ${dayInfo.subColumns}`;
5518
+ }
5519
+ /** Returns the CSS `grid-column` value for an event within its day's sub-columns. */
5520
+ getEventColumn(event) {
5521
+ const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, event.startTime));
5522
+ if (dayIdx < 0)
5523
+ return '1 / span 1';
5524
+ const dayInfo = this.dayColumnMap[dayIdx];
5525
+ const subCol = (event.column ?? 0) + dayInfo.startCol;
5526
+ const width = event.width ?? 1;
5527
+ return `${subCol} / span ${width}`;
5528
+ }
5529
+ /** Forwards event click to parent. */
5530
+ onEventClick(event) {
5531
+ this.eventClicked.emit(event);
5532
+ }
5533
+ /** trackBy for hour rows. */
5534
+ trackByHour(_index, row) {
5535
+ return row.hour;
5536
+ }
5537
+ /** trackBy for day columns. */
5538
+ trackByColumn(_index, col) {
5539
+ return col.date.getTime();
5540
+ }
5541
+ /** trackBy for events. */
5542
+ trackByEvent(_index, event) {
5543
+ return event.id;
5544
+ }
5545
+ async buildHourRows() {
5546
+ this.hourRows = [];
5547
+ const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
5548
+ this.totalRows = hours * 2;
5549
+ for (let i = 0; i < hours; i++) {
5550
+ const hour = this.resolvedConfig.startHour + i;
5551
+ const label = await this.formatter.formatTimeI(hour, 0);
5552
+ this.hourRows.push({
5553
+ hour,
5554
+ topRow: i * 2 + 1,
5555
+ bottomRow: i * 2 + 3,
5556
+ hourLabel: label
5557
+ });
5558
+ }
5559
+ }
5560
+ /** Builds the 7 day columns for the current week (Monday–Sunday). */
5561
+ buildColumns() {
5562
+ if (!this.focusDay)
5563
+ return;
5564
+ const shortNames = this.resolvedConfig.shortDayNames;
5565
+ const today = new Date();
5566
+ const day = this.focusDay.getDay();
5567
+ const mondayOffset = day === 0 ? -6 : 1 - day;
5568
+ const monday = new Date(this.focusDay);
5569
+ monday.setDate(this.focusDay.getDate() + mondayOffset);
5570
+ this.columns = [];
5571
+ for (let i = 0; i < 7; i++) {
5572
+ const date = new Date(monday);
5573
+ date.setDate(monday.getDate() + i);
5574
+ this.columns.push({
5575
+ date,
5576
+ dayName: shortNames[i],
5577
+ dayNumber: date.getDate(),
5578
+ isToday: this.formatter.isSameDay(date, today)
5579
+ });
5580
+ }
5581
+ }
5582
+ /** Filters, splits, and lays out events for the current week. */
5583
+ refreshEvents() {
5584
+ if (!this.columns.length)
5585
+ return;
5586
+ const rangeStart = this.columns[0].date;
5587
+ const rangeEnd = new Date(this.columns[6].date);
5588
+ rangeEnd.setHours(23, 59, 59, 999);
5589
+ const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
5590
+ this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
5591
+ // Assign columns per day so overlapping events within a day get sub-columns
5592
+ for (let i = 0; i < 7; i++) {
5593
+ const dayStart = new Date(this.columns[i].date);
5594
+ dayStart.setHours(0, 0, 0, 0);
5595
+ const dayEnd = new Date(this.columns[i].date);
5596
+ dayEnd.setHours(23, 59, 59, 999);
5597
+ const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
5598
+ this.layoutService.assignColumnsToEvents(dayEvents);
5599
+ this.layoutService.assignWidthsToEvents(dayEvents, dayStart, dayEnd);
5600
+ }
5601
+ this.buildGridColumns();
5602
+ this.updateCurrentTime();
5603
+ }
5604
+ /** Computes the CSS grid-template-columns string based on per-day sub-column counts. */
5605
+ buildGridColumns() {
5606
+ this.dayColumnMap = [];
5607
+ let currentCol = 1;
5608
+ for (let i = 0; i < 7; i++) {
5609
+ const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
5610
+ let maxSubCols = 1;
5611
+ for (const e of dayEvents) {
5612
+ maxSubCols = Math.max(maxSubCols, (e.column ?? 0) + (e.width ?? 1));
5613
+ }
5614
+ this.dayColumnMap.push({ subColumns: maxSubCols, startCol: currentCol });
5615
+ currentCol += maxSubCols;
5616
+ }
5617
+ const parts = [];
5618
+ for (const day of this.dayColumnMap) {
5619
+ for (let j = 0; j < day.subColumns; j++) {
5620
+ parts.push(`${1 / day.subColumns}fr`);
5621
+ }
5622
+ }
5623
+ this.gridTemplateColumns = parts.join(' ');
5624
+ }
5625
+ /** Updates the current-time red line position. */
5626
+ updateCurrentTime() {
5627
+ const now = new Date();
5628
+ const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, now));
5629
+ if (dayIdx >= 0 && this.dayColumnMap.length > 0) {
5630
+ const dayInfo = this.dayColumnMap[dayIdx];
5631
+ this.currentTimeCol = `${dayInfo.startCol} / span ${dayInfo.subColumns}`;
5632
+ this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
5633
+ }
5634
+ else {
5635
+ this.currentTimeCol = '';
5636
+ this.currentTimeRow = 0;
5637
+ }
5638
+ }
5639
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
5640
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "app-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
5641
+ }
5642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
5643
+ type: Component,
5644
+ args: [{ selector: 'app-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-week\" role=\"grid\" aria-label=\"Week view\">\n <div class=\"week-header\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\"\n *ngFor=\"let col of columns; let i = index; trackBy: trackByColumn\"\n [class.today]=\"col.isToday\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"day-name\">{{ col.dayName }}</span>\n <span class=\"day-number\">{{ col.dayNumber }}</span>\n </div>\n </div>\n <div class=\"week-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"week-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && currentTimeCol\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"week-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-week{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.week-header{display:grid;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.week-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.week-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.week-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
5645
+ }], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
5646
+ type: Input
5647
+ }], eventsChanged: [{
5648
+ type: Input
5649
+ }], focusDayChanged: [{
5650
+ type: Input
5651
+ }], config: [{
5652
+ type: Input
5653
+ }], calendarEventComponent: [{
5654
+ type: Input
5655
+ }], eventClicked: [{
5656
+ type: Output
5657
+ }] } });
5658
+
5659
+ /**
5660
+ * Day grid view showing a single day with half-hour time slots.
5661
+ *
5662
+ * Shares the same layout algorithm as the week view via
5663
+ * {@link CalendarEventLayoutService}.
5664
+ */
5665
+ class CalendarDayComponent {
5666
+ layoutService;
5667
+ /** The date to display. */
5668
+ focusDay;
5669
+ /** Observable that emits the full event list whenever it changes. */
5670
+ eventsChanged;
5671
+ /** Observable that emits when the focus day changes. */
5672
+ focusDayChanged;
5673
+ /** Resolved calendar configuration passed from the parent view. */
5674
+ config;
5675
+ /** Optional custom event renderer component. */
5676
+ calendarEventComponent;
5677
+ /** Emits when a calendar event is clicked. */
5678
+ eventClicked = new EventEmitter();
5679
+ hourRows = [];
5680
+ displayEvents = [];
5681
+ totalRows = 0;
5682
+ totalColumns = 1;
5683
+ currentTimeRow = 0;
5684
+ isToday = false;
5685
+ dayName = '';
5686
+ events = [];
5687
+ destroy$ = new Subject();
5688
+ formatter;
5689
+ resolvedConfig;
5690
+ currentTimeInterval;
5691
+ constructor(layoutService) {
5692
+ this.layoutService = layoutService;
5693
+ this.formatter = new DefaultCalendarDateFormatter();
5694
+ }
5695
+ ngOnInit() {
5696
+ this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5697
+ this.buildHourRows();
5698
+ this.updateDayInfo();
5699
+ this.updateCurrentTime();
5700
+ this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
5701
+ if (this.eventsChanged) {
5702
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5703
+ this.events = events;
5704
+ this.refreshEvents();
5705
+ });
5706
+ }
5707
+ if (this.focusDayChanged) {
5708
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5709
+ this.focusDay = date;
5710
+ this.updateDayInfo();
5711
+ this.refreshEvents();
5712
+ this.updateCurrentTime();
5713
+ });
5714
+ }
5715
+ }
5716
+ ngOnDestroy() {
5717
+ this.destroy$.next();
5718
+ this.destroy$.complete();
5719
+ if (this.currentTimeInterval)
5720
+ clearInterval(this.currentTimeInterval);
5721
+ }
5722
+ /** Returns the CSS `grid-row` value for an event. */
5723
+ getEventRow(event) {
5724
+ const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
5725
+ const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
5726
+ return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
5727
+ }
5728
+ /** Returns the CSS `grid-column` value for an event within its sub-columns. */
5729
+ getEventColumn(event) {
5730
+ const col = (event.column ?? 0) + 1;
5731
+ const width = event.width ?? 1;
5732
+ return `${col} / span ${width}`;
5733
+ }
5734
+ /** Forwards event click to parent. */
5735
+ onEventClick(event) {
5736
+ this.eventClicked.emit(event);
5737
+ }
5738
+ /** trackBy for hour rows. */
5739
+ trackByHour(_index, row) {
5740
+ return row.hour;
5741
+ }
5742
+ /** trackBy for events. */
5743
+ trackByEvent(_index, event) {
5744
+ return event.id;
5745
+ }
5746
+ async buildHourRows() {
5747
+ this.hourRows = [];
5748
+ const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
5749
+ this.totalRows = hours * 2;
5750
+ for (let i = 0; i < hours; i++) {
5751
+ const hour = this.resolvedConfig.startHour + i;
5752
+ const label = await this.formatter.formatTimeI(hour, 0);
5753
+ this.hourRows.push({
5754
+ hour,
5755
+ topRow: i * 2 + 1,
5756
+ bottomRow: i * 2 + 3,
5757
+ hourLabel: label
5758
+ });
5759
+ }
5760
+ }
5761
+ /** Updates the day name and isToday flag. */
5762
+ updateDayInfo() {
5763
+ if (!this.focusDay)
5764
+ return;
5765
+ const today = new Date();
5766
+ this.isToday = this.formatter.isSameDay(this.focusDay, today);
5767
+ const longNames = this.resolvedConfig.longDayNames;
5768
+ const dayIdx = this.focusDay.getDay();
5769
+ const mondayIdx = dayIdx === 0 ? 6 : dayIdx - 1;
5770
+ this.dayName = longNames[mondayIdx];
5771
+ }
5772
+ /** Filters, splits, and lays out events for the focus day. */
5773
+ refreshEvents() {
5774
+ if (!this.focusDay)
5775
+ return;
5776
+ const rangeStart = new Date(this.focusDay);
5777
+ rangeStart.setHours(0, 0, 0, 0);
5778
+ const rangeEnd = new Date(this.focusDay);
5779
+ rangeEnd.setHours(23, 59, 59, 999);
5780
+ const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
5781
+ this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
5782
+ this.layoutService.assignColumnsToEvents(this.displayEvents);
5783
+ this.layoutService.assignWidthsToEvents(this.displayEvents, rangeStart, rangeEnd);
5784
+ const maxCol = this.displayEvents.reduce((max, e) => Math.max(max, (e.column ?? 0) + (e.width ?? 1)), 1);
5785
+ this.totalColumns = maxCol;
5786
+ }
5787
+ /** Updates the current-time red line position. */
5788
+ updateCurrentTime() {
5789
+ const now = new Date();
5790
+ if (this.focusDay && this.formatter.isSameDay(this.focusDay, now)) {
5791
+ this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
5792
+ this.isToday = true;
5793
+ }
5794
+ else {
5795
+ this.currentTimeRow = 0;
5796
+ this.isToday = false;
5797
+ }
5798
+ }
5799
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
5800
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "app-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CalendarEventComponent, selector: "app-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
5801
+ }
5802
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
5803
+ type: Component,
5804
+ args: [{ selector: 'app-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<div class=\"calendar-day\" role=\"grid\" aria-label=\"Day view\">\n <div class=\"day-header\">\n <div class=\"time-gutter-header\"></div>\n <div class=\"day-column-header\" [class.today]=\"isToday\" role=\"columnheader\">\n <span class=\"day-name\">{{ dayName }}</span>\n <span class=\"day-number\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"day-body\">\n <div class=\"time-gutter\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\">\n <div class=\"hour-label\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n </div>\n <div class=\"day-grid\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', 1fr)'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n <div class=\"hour-line\"\n *ngFor=\"let row of hourRows; trackBy: trackByHour\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n <div class=\"current-time-line\" *ngIf=\"currentTimeRow > 0 && isToday\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"current-time-dot\"></div>\n <div class=\"current-time-rule\"></div>\n </div>\n <div class=\"day-event\"\n *ngFor=\"let event of displayEvents; trackBy: trackByEvent\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\">\n <app-calendar-event [event]=\"event\" [customComponent]=\"calendarEventComponent\"></app-calendar-event>\n </div>\n </div>\n </div>\n</div>\n", styles: [".calendar-day{width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden}.day-header{display:grid;grid-template-columns:60px 1fr;border-bottom:1px solid #e5e7eb}.time-gutter-header{min-width:60px}.day-column-header{text-align:center;padding:8px 4px;font-size:13px}.day-column-header.today{color:#3b82f6;font-weight:700}.day-name{display:block;font-size:11px;text-transform:uppercase;color:#6b7280}.day-number{font-size:18px;font-weight:600}.day-body{display:grid;grid-template-columns:60px 1fr;flex:1;min-height:0;overflow:hidden;align-items:stretch}.time-gutter{display:grid;height:100%;min-height:0}.hour-label{font-size:11px;color:#6b7280;text-align:right;padding-right:8px;display:flex;align-items:start;min-height:0;overflow:hidden}.day-grid{display:grid;position:relative;grid-auto-rows:1fr;height:100%;min-height:0}.hour-line{border-top:1px solid #f3f4f6;pointer-events:none;min-height:0}.day-event{z-index:1;padding:1px 2px;overflow:hidden;min-height:0}.current-time-line{position:relative;z-index:2;pointer-events:none}.current-time-dot{width:8px;height:8px;background:#ef4444;border-radius:50%;position:absolute;left:-4px;top:-4px}.current-time-rule{height:2px;background:#ef4444;width:100%}\n"] }]
5805
+ }], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
5806
+ type: Input
5807
+ }], eventsChanged: [{
5808
+ type: Input
5809
+ }], focusDayChanged: [{
5810
+ type: Input
5811
+ }], config: [{
5812
+ type: Input
5813
+ }], calendarEventComponent: [{
5814
+ type: Input
5815
+ }], eventClicked: [{
5816
+ type: Output
5817
+ }] } });
5818
+
5819
+ /**
5820
+ * Renders a single row in the upcoming-events sidebar.
5821
+ * Shows the event title, formatted date/time, and optional description.
5822
+ */
5823
+ class UpcomingEventRowComponent {
5824
+ /** The event to display. */
5825
+ event;
5826
+ /** Emits the event when this row is clicked. */
5827
+ eventClicked = new EventEmitter();
5828
+ formattedDate = '';
5829
+ formatter;
5830
+ constructor(formatter) {
5831
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5832
+ }
5833
+ async ngOnInit() {
5834
+ if (this.event) {
5835
+ const start = await this.formatter.formatTime(this.event.startTime);
5836
+ const end = await this.formatter.formatTime(this.event.endTime);
5837
+ this.formattedDate = `${start} - ${end}`;
5838
+ }
5839
+ }
5840
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
5841
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventRowComponent, isStandalone: true, selector: "app-upcoming-event-row", inputs: { event: "event" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
5842
+ }
5843
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, decorators: [{
5844
+ type: Component,
5845
+ args: [{ selector: 'app-upcoming-event-row', standalone: true, imports: [CommonModule], template: "<div class=\"upcoming-event-row\" (click)=\"eventClicked.emit(event)\" [style.border-left-color]=\"event.color.primaryColor\">\n <div class=\"event-title\">{{ event.title }}</div>\n <div class=\"event-time\">{{ formattedDate }}</div>\n <div class=\"event-description\" *ngIf=\"event.description\">{{ event.description }}</div>\n</div>\n", styles: [".upcoming-event-row{padding:8px 12px;border-left:3px solid #3b82f6;margin-bottom:8px;cursor:pointer;border-radius:4px;transition:background .15s}.upcoming-event-row:hover{background:#f9fafb}.event-title{font-weight:600;font-size:13px}.event-time{font-size:12px;color:#6b7280}.event-description{font-size:12px;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
5846
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5847
+ type: Optional
5848
+ }, {
5849
+ type: Inject,
5850
+ args: [CALENDAR_DATE_FORMATTER]
5851
+ }] }], propDecorators: { event: [{
5852
+ type: Input
5853
+ }], eventClicked: [{
5854
+ type: Output
5855
+ }] } });
5856
+
5857
+ /**
5858
+ * Sidebar component that lists the next 10 upcoming events
5859
+ * (events whose end time is in the future), sorted by start time.
5860
+ */
5861
+ class UpcomingEventsComponent {
5862
+ /** Observable that emits the full event list whenever it changes. */
5863
+ eventsChanged;
5864
+ /** Resolved calendar configuration passed from the parent view. */
5865
+ config;
5866
+ /** Emits when an upcoming event row is clicked. */
5867
+ eventClicked = new EventEmitter();
5868
+ upcomingEvents = [];
5869
+ title;
5870
+ destroy$ = new Subject();
5871
+ constructor() {
5872
+ this.title = DEFAULT_CALENDAR_CONFIG.upcomingEventsTitle;
5873
+ }
5874
+ ngOnInit() {
5875
+ const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5876
+ this.title = resolved.upcomingEventsTitle;
5877
+ if (this.eventsChanged) {
5878
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5879
+ const now = new Date();
5880
+ this.upcomingEvents = events
5881
+ .filter(e => e.endTime > now)
5882
+ .sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
5883
+ .slice(0, 10);
5884
+ });
5885
+ }
5886
+ }
5887
+ ngOnDestroy() {
5888
+ this.destroy$.next();
5889
+ this.destroy$.complete();
5890
+ }
5891
+ /** trackBy for upcoming event rows. */
5892
+ trackByEvent(_index, event) {
5893
+ return event.id;
5894
+ }
5895
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5896
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "app-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, ngImport: i0, template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: UpcomingEventRowComponent, selector: "app-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
5897
+ }
5898
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
5899
+ type: Component,
5900
+ args: [{ selector: 'app-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div class=\"upcoming-events\" role=\"complementary\" aria-label=\"Upcoming events\">\n <div class=\"upcoming-title\">{{ title }}</div>\n <app-upcoming-event-row\n *ngFor=\"let event of upcomingEvents; trackBy: trackByEvent\"\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </app-upcoming-event-row>\n <div class=\"no-events\" *ngIf=\"upcomingEvents.length === 0\">No upcoming events</div>\n</div>\n", styles: [".upcoming-events{padding:16px}.upcoming-title{font-size:16px;font-weight:600;margin-bottom:12px}.no-events{color:#9ca3af;font-size:14px}\n"] }]
5901
+ }], ctorParameters: () => [], propDecorators: { eventsChanged: [{
5902
+ type: Input
5903
+ }], config: [{
5904
+ type: Input
5905
+ }], eventClicked: [{
5906
+ type: Output
5907
+ }] } });
5908
+
5909
+ /**
5910
+ * Main calendar orchestrator component.
5911
+ *
5912
+ * Provides a toolbar with view switching (month / week / day), date navigation,
5913
+ * and an optional action button. The active view and an upcoming-events sidebar
5914
+ * are rendered inside a responsive grid layout.
5915
+ *
5916
+ * All configuration (visible hours, locale, labels, mobile breakpoint) is read
5917
+ * from the `mn-config.json5` system via {@link MN_CALENDAR_CONFIG}, falling back
5918
+ * to the legacy {@link CALENDAR_CONFIG} injection token, then to built-in defaults.
5919
+ * Date formatting is delegated to the {@link CALENDAR_DATE_FORMATTER} token.
5920
+ *
5921
+ * @example
5922
+ * ```html
5923
+ * <app-calendar-view
5924
+ * [showButton]="true"
5925
+ * [buttonTitle]="'New Event'"
5926
+ * [NewCalendarItemsEvent]="eventsEmitter"
5927
+ * (RequestNewCalendarItemsEvent)="loadEvents($event)"
5928
+ * (CalendarItemClickedEvent)="onEventClick($event)"
5929
+ * (ButtonClickedEvent)="openModal()">
5930
+ * </app-calendar-view>
5931
+ * ```
5932
+ */
5933
+ class CalendarViewComponent {
5934
+ /** Whether to show the action button in the toolbar. */
5935
+ showButton = false;
5936
+ /** Label text for the action button. */
5937
+ buttonTitle = '';
5938
+ /** Custom event renderer component type. */
5939
+ CalendarEventComponent;
5940
+ /** Observable or EventEmitter that pushes new event arrays into the calendar. */
5941
+ NewCalendarItemsEvent;
5942
+ /** Emits when the calendar needs fresh event data (e.g. after navigation). */
5943
+ RequestNewCalendarItemsEvent = new EventEmitter();
5944
+ /** Emits when a calendar event is clicked. */
5945
+ CalendarItemClickedEvent = new EventEmitter();
5946
+ /** Emits when the action button is clicked. */
5947
+ ButtonClickedEvent = new EventEmitter();
5948
+ CalendarView = CalendarView;
5949
+ currentView = CalendarView.WEEK;
5950
+ focusDay = new Date();
5951
+ dateInputValue = '';
5952
+ viewOptions = [];
5953
+ isMobileView = false;
5954
+ /** BehaviorSubject so late-subscribing child views receive the last emitted events. */
5955
+ internalEventsChanged = new BehaviorSubject([]);
5956
+ /** Subject for broadcasting focus-day changes to child views. */
5957
+ internalFocusDayChanged = new Subject();
5958
+ destroy$ = new Subject();
5959
+ formatter;
5960
+ config;
5961
+ destroyRef = inject(DestroyRef);
5962
+ lang = inject(MnLanguageService);
5963
+ constructor(formatter, mnConfig, legacyConfig) {
5964
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5965
+ // Priority: mn-config system > legacy CALENDAR_CONFIG > built-in defaults
5966
+ const raw = mnConfig ?? legacyConfig ?? undefined;
5967
+ this.config = resolveCalendarConfig(raw);
5968
+ }
5969
+ onResize() {
5970
+ this.checkMobileView();
5971
+ }
5972
+ ngOnInit() {
5973
+ this.rebuildFromConfig();
5974
+ // Re-resolve config when locale changes (supports $translate in mn-config).
5975
+ const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
5976
+ this.rebuildFromConfig();
5977
+ });
5978
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
5979
+ this.checkMobileView();
5980
+ this.updateDateInput();
5981
+ this.RequestNewCalendarItemsEvent.emit(this.focusDay);
5982
+ if (this.NewCalendarItemsEvent) {
5983
+ this.NewCalendarItemsEvent.pipe(takeUntil(this.destroy$)).subscribe(events => {
5984
+ this.internalEventsChanged.next(events);
5985
+ });
5986
+ }
5987
+ }
5988
+ ngOnDestroy() {
5989
+ this.destroy$.next();
5990
+ this.destroy$.complete();
5991
+ }
5992
+ /** Switches the active view. On mobile, forces day view. */
5993
+ switchView(view) {
5994
+ if (this.isMobileView) {
5995
+ this.currentView = CalendarView.DAY;
5996
+ return;
5997
+ }
5998
+ this.currentView = view;
5999
+ }
6000
+ /** Navigates to the previous period (month / week / day). */
6001
+ navigatePrevious() {
6002
+ const d = new Date(this.focusDay);
6003
+ switch (this.currentView) {
6004
+ case CalendarView.MONTH:
6005
+ d.setMonth(d.getMonth() - 1);
6006
+ break;
6007
+ case CalendarView.WEEK:
6008
+ d.setDate(d.getDate() - 7);
6009
+ break;
6010
+ case CalendarView.DAY:
6011
+ d.setDate(d.getDate() - 1);
6012
+ break;
6013
+ }
6014
+ this.setFocusDay(d);
6015
+ }
6016
+ /** Navigates to the next period (month / week / day). */
6017
+ navigateNext() {
6018
+ const d = new Date(this.focusDay);
6019
+ switch (this.currentView) {
6020
+ case CalendarView.MONTH:
6021
+ d.setMonth(d.getMonth() + 1);
6022
+ break;
6023
+ case CalendarView.WEEK:
6024
+ d.setDate(d.getDate() + 7);
6025
+ break;
6026
+ case CalendarView.DAY:
6027
+ d.setDate(d.getDate() + 1);
6028
+ break;
6029
+ }
6030
+ this.setFocusDay(d);
6031
+ }
6032
+ /** Navigates to today. */
6033
+ goToToday() {
6034
+ this.setFocusDay(new Date());
6035
+ }
6036
+ /** Handles the date-picker input change. */
6037
+ onDateInputChange(event) {
6038
+ const value = event.target.value;
6039
+ if (value) {
6040
+ this.setFocusDay(new Date(value));
6041
+ }
6042
+ }
6043
+ /** Handles a day click from the month view — switches to day view. */
6044
+ onMonthDayClick(date) {
6045
+ this.currentView = CalendarView.DAY;
6046
+ this.setFocusDay(date);
6047
+ }
6048
+ /** Forwards a child event click to the parent output. */
6049
+ onEventClick(event) {
6050
+ this.CalendarItemClickedEvent.emit(event);
6051
+ }
6052
+ /** trackBy for view option buttons. */
6053
+ trackByView(_index, item) {
6054
+ return item.value;
6055
+ }
6056
+ /** Rebuilds view options and labels from the current config. */
6057
+ rebuildFromConfig() {
6058
+ this.viewOptions = [
6059
+ { value: CalendarView.MONTH, label: this.config.viewLabels['MONTH'] ?? 'Month' },
6060
+ { value: CalendarView.WEEK, label: this.config.viewLabels['WEEK'] ?? 'Week' },
6061
+ { value: CalendarView.DAY, label: this.config.viewLabels['DAY'] ?? 'Day' }
6062
+ ];
6063
+ }
6064
+ checkMobileView() {
6065
+ const wasMobile = this.isMobileView;
6066
+ this.isMobileView = window.innerWidth < this.config.mobileBreakpoint;
6067
+ if (this.isMobileView && !wasMobile) {
6068
+ this.currentView = CalendarView.DAY;
6069
+ }
6070
+ }
6071
+ setFocusDay(date) {
6072
+ this.focusDay = date;
6073
+ this.updateDateInput();
6074
+ this.internalFocusDayChanged.next(date);
6075
+ this.RequestNewCalendarItemsEvent.emit(date);
6076
+ }
6077
+ updateDateInput() {
6078
+ this.dateInputValue = this.formatter.formatDateForFormControl(this.focusDay);
6079
+ }
6080
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [{ token: CALENDAR_DATE_FORMATTER, optional: true }, { token: MN_CALENDAR_CONFIG, optional: true }, { token: CALENDAR_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component });
6081
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "app-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
6082
+ provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
6083
+ ], ngImport: i0, template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">&#8249;</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">&#8250;</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: CalendarMonthComponent, selector: "app-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "app-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "app-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "app-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }] });
6084
+ }
6085
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
6086
+ type: Component,
6087
+ args: [{ selector: 'app-calendar-view', standalone: true, imports: [
6088
+ CommonModule,
6089
+ CalendarMonthComponent,
6090
+ CalendarWeekComponent,
6091
+ CalendarDayComponent,
6092
+ UpcomingEventsComponent
6093
+ ], providers: [
6094
+ provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
6095
+ ], template: "<div class=\"calendar-view\" role=\"application\" aria-label=\"Calendar\">\n <div class=\"calendar-toolbar\">\n <div class=\"toolbar-left\">\n <div class=\"view-switcher\" role=\"tablist\" aria-label=\"Calendar view\">\n <button\n *ngFor=\"let view of viewOptions; trackBy: trackByView\"\n class=\"view-btn\"\n role=\"tab\"\n [attr.aria-selected]=\"currentView === view.value\"\n [class.active]=\"currentView === view.value\"\n (click)=\"switchView(view.value)\">\n {{ view.label }}\n </button>\n </div>\n <div class=\"date-nav\">\n <button class=\"nav-btn\" (click)=\"navigatePrevious()\" aria-label=\"Previous\">&#8249;</button>\n <input type=\"date\" [value]=\"dateInputValue\" (change)=\"onDateInputChange($event)\" class=\"date-input\" aria-label=\"Select date\" />\n <button class=\"nav-btn\" (click)=\"navigateNext()\" aria-label=\"Next\">&#8250;</button>\n <button class=\"today-btn\" (click)=\"goToToday()\">{{ config.todayLabel }}</button>\n </div>\n </div>\n <div class=\"toolbar-right\">\n <button *ngIf=\"showButton\" class=\"action-btn\" (click)=\"ButtonClickedEvent.emit()\">\n {{ buttonTitle }}\n </button>\n </div>\n </div>\n\n <div class=\"calendar-content\">\n <div class=\"calendar-main\">\n <app-calendar-month\n *ngIf=\"currentView === CalendarView.MONTH\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </app-calendar-month>\n\n <app-calendar-week\n *ngIf=\"currentView === CalendarView.WEEK\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-week>\n\n <app-calendar-day\n *ngIf=\"currentView === CalendarView.DAY\"\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </app-calendar-day>\n </div>\n\n <div class=\"calendar-sidebar\">\n <app-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </app-upcoming-events>\n </div>\n </div>\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}.calendar-view{width:100%;height:100%;font-family:inherit;display:flex;flex-direction:column}.calendar-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 0;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.view-switcher{display:flex;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.view-btn{padding:6px 14px;border:none;background:#fff;cursor:pointer;font-size:13px;transition:background .15s}.view-btn:hover{background:#f3f4f6}.view-btn.active{background:#3b82f6;color:#fff}.date-nav{display:flex;align-items:center;gap:4px}.nav-btn{width:32px;height:32px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center}.nav-btn:hover{background:#f3f4f6}.date-input{padding:4px 8px;border:1px solid #e5e7eb;border-radius:6px;font-size:13px}.today-btn{padding:6px 12px;border:1px solid #e5e7eb;border-radius:6px;background:#fff;cursor:pointer;font-size:13px}.today-btn:hover{background:#f3f4f6}.action-btn{padding:8px 16px;border:none;border-radius:6px;background:#3b82f6;color:#fff;cursor:pointer;font-size:13px}.action-btn:hover{background:#2563eb}.calendar-content{display:grid;grid-template-columns:1fr 220px;gap:12px;flex:1;min-height:0}.calendar-main{min-width:0;min-height:0;overflow:hidden}.calendar-sidebar{border-left:1px solid #e5e7eb;overflow:auto}@media(max-width:767px){.calendar-toolbar{padding:8px 0}.toolbar-left{flex-direction:column;align-items:flex-start;gap:8px}.view-switcher{display:none}.calendar-content{grid-template-columns:1fr}.calendar-sidebar{display:none}.calendar-main{overflow-y:auto}}\n"] }]
6096
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
6097
+ type: Optional
6098
+ }, {
6099
+ type: Inject,
6100
+ args: [CALENDAR_DATE_FORMATTER]
6101
+ }] }, { type: undefined, decorators: [{
6102
+ type: Optional
6103
+ }, {
6104
+ type: Inject,
6105
+ args: [MN_CALENDAR_CONFIG]
6106
+ }] }, { type: undefined, decorators: [{
6107
+ type: Optional
6108
+ }, {
6109
+ type: Inject,
6110
+ args: [CALENDAR_CONFIG]
6111
+ }] }], propDecorators: { showButton: [{
6112
+ type: Input
6113
+ }], buttonTitle: [{
6114
+ type: Input
6115
+ }], CalendarEventComponent: [{
6116
+ type: Input
6117
+ }], NewCalendarItemsEvent: [{
6118
+ type: Input
6119
+ }], RequestNewCalendarItemsEvent: [{
6120
+ type: Output
6121
+ }], CalendarItemClickedEvent: [{
6122
+ type: Output
6123
+ }], ButtonClickedEvent: [{
6124
+ type: Output
6125
+ }], onResize: [{
6126
+ type: HostListener,
6127
+ args: ['window:resize']
6128
+ }] } });
6129
+
6130
+ // Main component
6131
+
4864
6132
  class MnSectionDirective {
4865
6133
  /** Section name contributed by this DOM node to the section path */
4866
6134
  mnSection;
@@ -5394,5 +6662,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
5394
6662
  * Generated bundle index. Do not edit.
5395
6663
  */
5396
6664
 
5397
- export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_MN_ALERT_CONFIG, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnComponentConfig, provideMnConfig, provideMnLanguage };
6665
+ export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_TEST_COMPONENT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnTable, MnTestComponent, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, Test, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnButtonVariants, mnCheckboxVariants, mnDatetimeVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
5398
6666
  //# sourceMappingURL=mn-angular-lib.mjs.map