mn-angular-lib 0.0.52 → 0.0.54

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';
@@ -606,6 +606,7 @@ class MnLanguageService {
606
606
  _translations = {};
607
607
  _locale$ = new BehaviorSubject('en');
608
608
  _urlPattern = null;
609
+ _debug = false;
609
610
  /** Observable of the current active locale. */
610
611
  locale$ = this._locale$.asObservable();
611
612
  constructor(http) {
@@ -615,11 +616,23 @@ class MnLanguageService {
615
616
  get locale() {
616
617
  return this._locale$.value;
617
618
  }
619
+ /**
620
+ * Enable or disable debug logging.
621
+ */
622
+ setDebug(enabled) {
623
+ this._debug = enabled;
624
+ if (enabled) {
625
+ console.log(`[MnLanguage] Debug mode enabled`);
626
+ }
627
+ }
618
628
  /**
619
629
  * Configure the URL pattern used to fetch translation files.
620
630
  * Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
621
631
  */
622
632
  configure(urlPattern) {
633
+ if (this._debug) {
634
+ console.log(`[MnLanguage] Configured urlPattern: ${urlPattern}`);
635
+ }
623
636
  this._urlPattern = urlPattern;
624
637
  }
625
638
  /**
@@ -634,9 +647,15 @@ class MnLanguageService {
634
647
  return;
635
648
  }
636
649
  const url = this._urlPattern.replace('{locale}', locale);
650
+ if (this._debug) {
651
+ console.log(`[MnLanguage] Loading locale "${locale}" from ${url}`);
652
+ }
637
653
  try {
638
654
  const map = await firstValueFrom(this.http.get(url));
639
655
  this._translations[locale] = map ?? {};
656
+ if (this._debug) {
657
+ console.log(`[MnLanguage] Loaded locale "${locale}"`, this._translations[locale]);
658
+ }
640
659
  }
641
660
  catch (err) {
642
661
  console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
@@ -647,6 +666,9 @@ class MnLanguageService {
647
666
  * Switch the active locale. Loads translations if not yet loaded.
648
667
  */
649
668
  async setLocale(locale) {
669
+ if (this._debug) {
670
+ console.log(`[MnLanguage] Setting locale to "${locale}"`);
671
+ }
650
672
  await this.loadLocale(locale);
651
673
  this._locale$.next(locale);
652
674
  }
@@ -667,8 +689,11 @@ class MnLanguageService {
667
689
  */
668
690
  translate(key, params) {
669
691
  const map = this._translations[this.locale] ?? {};
670
- let value = map[key];
692
+ let value = this.getValueFromMap(map, key);
671
693
  if (value === undefined) {
694
+ if (this._debug) {
695
+ console.warn(`[MnLanguage] Missing translation for key: "${key}" in locale: "${this.locale}"`);
696
+ }
672
697
  return key;
673
698
  }
674
699
  if (params) {
@@ -678,6 +703,21 @@ class MnLanguageService {
678
703
  }
679
704
  return value;
680
705
  }
706
+ /**
707
+ * Helper to retrieve a value from a potentially nested translation map using a dot-notated key.
708
+ */
709
+ getValueFromMap(map, key) {
710
+ if (map[key] !== undefined)
711
+ return map[key];
712
+ const parts = key.split('.');
713
+ let current = map;
714
+ for (const part of parts) {
715
+ if (current === null || typeof current !== 'object')
716
+ return undefined;
717
+ current = current[part];
718
+ }
719
+ return typeof current === 'string' ? current : undefined;
720
+ }
681
721
  /**
682
722
  * Shorthand alias for `translate`.
683
723
  */
@@ -739,6 +779,7 @@ class MnConfigService {
739
779
  */
740
780
  async load(url, debugMode = false) {
741
781
  this._debugMode = debugMode;
782
+ this.lang.setDebug(debugMode);
742
783
  let text;
743
784
  try {
744
785
  text = await firstValueFrom(this.http.get(url, { responseType: 'text' }));
@@ -768,9 +809,12 @@ class MnConfigService {
768
809
  const langCfg = cfg.language;
769
810
  if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
770
811
  const lc = langCfg;
812
+ if (this._debugMode) {
813
+ console.log(`[MnConfig] Applying language config from file`, lc);
814
+ }
771
815
  this.lang.configure(lc.urlPattern);
772
816
  const effectiveLocale = this.lang.resolveLocaleForDomain(lc.domainLocaleMap, lc.defaultLocale);
773
- const localesToLoad = lc.preload ?? [effectiveLocale];
817
+ const localesToLoad = Array.from(new Set([...(lc.preload ?? []), effectiveLocale]));
774
818
  await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
775
819
  await this.lang.setLocale(effectiveLocale);
776
820
  }
@@ -791,9 +835,12 @@ class MnConfigService {
791
835
  const langCfg = config['language'];
792
836
  if (isPlainObject(langCfg) && typeof langCfg['urlPattern'] === 'string') {
793
837
  const lc = langCfg;
838
+ if (this._debugMode) {
839
+ console.log(`[MnConfig] Applying language config from object`, lc);
840
+ }
794
841
  this.lang.configure(lc.urlPattern);
795
842
  const effectiveLocale = this.lang.resolveLocaleForDomain(lc.domainLocaleMap, lc.defaultLocale);
796
- const localesToLoad = lc.preload ?? [effectiveLocale];
843
+ const localesToLoad = Array.from(new Set([...(lc.preload ?? []), effectiveLocale]));
797
844
  await Promise.all(localesToLoad.map(l => this.lang.loadLocale(l)));
798
845
  await this.lang.setLocale(effectiveLocale);
799
846
  }
@@ -1278,7 +1325,7 @@ function provideMnComponentConfig(token, componentName, initial) {
1278
1325
  const cfg = resolveConfig();
1279
1326
  // Re-resolve translatable values whenever the locale changes.
1280
1327
  // skip(1) because the current locale was already used for the initial resolve.
1281
- const sub = lang.locale$.pipe(skip(1)).subscribe(() => {
1328
+ const sub = lang.locale$.subscribe(() => {
1282
1329
  const updated = resolveConfig();
1283
1330
  // Mutate the existing object in place so all template bindings pick up the new values.
1284
1331
  for (const key of Object.keys(updated)) {
@@ -4861,6 +4908,1274 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4861
4908
 
4862
4909
  // Types
4863
4910
 
4911
+ /**
4912
+ * Available calendar view modes.
4913
+ */
4914
+ var CalendarView;
4915
+ (function (CalendarView) {
4916
+ CalendarView["MONTH"] = "MONTH";
4917
+ CalendarView["WEEK"] = "WEEK";
4918
+ CalendarView["DAY"] = "DAY";
4919
+ })(CalendarView || (CalendarView = {}));
4920
+ /**
4921
+ * Builds locale-derived day name arrays from a BCP 47 locale string.
4922
+ * Uses January 1 2024 (a Monday) as the reference date.
4923
+ */
4924
+ function buildDayNames(locale) {
4925
+ const base = new Date(2024, 0, 1); // 2024-01-01 is a Monday
4926
+ const short = [];
4927
+ const long = [];
4928
+ for (let i = 0; i < 7; i++) {
4929
+ const d = new Date(base);
4930
+ d.setDate(base.getDate() + i);
4931
+ short.push(d.toLocaleDateString(locale, { weekday: 'short' }));
4932
+ long.push(d.toLocaleDateString(locale, { weekday: 'long' }));
4933
+ }
4934
+ return { short, long };
4935
+ }
4936
+ /** Default calendar configuration values. */
4937
+ const DEFAULT_CALENDAR_CONFIG = (() => {
4938
+ const locale = 'en-US';
4939
+ const names = buildDayNames(locale);
4940
+ return {
4941
+ startHour: 7,
4942
+ endHour: 22,
4943
+ locale,
4944
+ todayLabel: 'Today',
4945
+ upcomingEventsTitle: 'Upcoming events',
4946
+ viewLabels: { MONTH: 'Month', WEEK: 'Week', DAY: 'Day' },
4947
+ shortDayNames: names.short,
4948
+ longDayNames: names.long,
4949
+ mobileBreakpoint: 768,
4950
+ };
4951
+ })();
4952
+ /**
4953
+ * Injection token for the resolved calendar configuration.
4954
+ *
4955
+ * Prefer using {@link MN_CALENDAR_CONFIG} with `provideMnComponentConfig`
4956
+ * so that settings can be managed via `mn-config.json5`. This token is
4957
+ * kept for backward compatibility and manual `providers` usage.
4958
+ *
4959
+ * @example
4960
+ * ```ts
4961
+ * providers: [
4962
+ * { provide: CALENDAR_CONFIG, useValue: { startHour: 8, endHour: 20, locale: 'nl-NL' } }
4963
+ * ]
4964
+ * ```
4965
+ */
4966
+ const CALENDAR_CONFIG = new InjectionToken('CalendarConfig', {
4967
+ providedIn: 'root',
4968
+ factory: () => DEFAULT_CALENDAR_CONFIG
4969
+ });
4970
+ /**
4971
+ * Injection token resolved via `MnConfigService` (the `mn-config.json5` system).
4972
+ *
4973
+ * Use the helper {@link provideMnCalendarConfig} in the component's `providers`
4974
+ * array so that calendar settings are read from the config file and support
4975
+ * `$translate` markers, section scoping, and instance-id overrides.
4976
+ *
4977
+ * Component name in the config file: `'mn-calendar'`.
4978
+ *
4979
+ * @example
4980
+ * ```json5
4981
+ * // mn-config.json5
4982
+ * {
4983
+ * defaults: {
4984
+ * "mn-calendar": {
4985
+ * startHour: 8,
4986
+ * endHour: 20,
4987
+ * locale: "nl-NL",
4988
+ * todayLabel: { $translate: "calendar.today" }
4989
+ * }
4990
+ * }
4991
+ * }
4992
+ * ```
4993
+ */
4994
+ const MN_CALENDAR_CONFIG = new InjectionToken('MN_CALENDAR_CONFIG');
4995
+ /** Component name used to look up calendar settings in `mn-config.json5`. */
4996
+ const MN_CALENDAR_COMPONENT_NAME = 'mn-calendar';
4997
+ /**
4998
+ * Provider helper that wires the calendar into the `mn-config` system.
4999
+ *
5000
+ * Add this to the `providers` array of the component (or module) that hosts
5001
+ * `<app-calendar-view>`. It reads defaults and overrides from `mn-config.json5`
5002
+ * under the key `"mn-calendar"` and provides them via {@link MN_CALENDAR_CONFIG}.
5003
+ *
5004
+ * @param initial — optional partial defaults merged before config-file values.
5005
+ */
5006
+ function provideMnCalendarConfig(initial) {
5007
+ return provideMnComponentConfig(MN_CALENDAR_CONFIG, MN_CALENDAR_COMPONENT_NAME, initial);
5008
+ }
5009
+ /**
5010
+ * Merges a partial config with defaults, re-deriving day names from locale when needed.
5011
+ */
5012
+ function resolveCalendarConfig(partial) {
5013
+ if (!partial)
5014
+ return { ...DEFAULT_CALENDAR_CONFIG };
5015
+ const locale = partial.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
5016
+ const names = buildDayNames(locale);
5017
+ return {
5018
+ ...DEFAULT_CALENDAR_CONFIG,
5019
+ ...partial,
5020
+ locale,
5021
+ shortDayNames: partial.shortDayNames ?? names.short,
5022
+ longDayNames: partial.longDayNames ?? names.long,
5023
+ };
5024
+ }
5025
+
5026
+ /**
5027
+ * Injection token for the calendar date formatter.
5028
+ *
5029
+ * @example
5030
+ * ```ts
5031
+ * providers: [
5032
+ * { provide: CALENDAR_DATE_FORMATTER, useClass: MyCustomFormatter }
5033
+ * ]
5034
+ * ```
5035
+ */
5036
+ const CALENDAR_DATE_FORMATTER = new InjectionToken('CalendarDateFormatter');
5037
+
5038
+ /**
5039
+ * Default implementation of {@link CalendarDateFormatter} that uses the
5040
+ * browser's `Intl.DateTimeFormat` API for locale-aware formatting.
5041
+ *
5042
+ * The locale is read from the injected {@link CALENDAR_CONFIG}. If no config
5043
+ * is provided, `'en-US'` is used as the fallback.
5044
+ *
5045
+ * This service has no dependency on `@ngx-translate` or any other i18n library,
5046
+ * so the calendar library works out of the box. Consumers can replace it with
5047
+ * their own implementation via the `CALENDAR_DATE_FORMATTER` injection token.
5048
+ */
5049
+ class DefaultCalendarDateFormatter {
5050
+ locale;
5051
+ constructor(config) {
5052
+ this.locale = config?.locale ?? DEFAULT_CALENDAR_CONFIG.locale;
5053
+ }
5054
+ /** Formats an hour and minute pair into a locale time string (e.g. "09:00 AM"). */
5055
+ formatTimeI(hour, minute) {
5056
+ const date = new Date();
5057
+ date.setHours(hour, minute, 0, 0);
5058
+ return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
5059
+ }
5060
+ /** Formats the time portion of a Date (e.g. "2:30 PM"). Returns empty string for undefined. */
5061
+ formatTime(date) {
5062
+ if (!date)
5063
+ return Promise.resolve('');
5064
+ return Promise.resolve(date.toLocaleTimeString(this.locale, { hour: '2-digit', minute: '2-digit' }));
5065
+ }
5066
+ /** Formats a Date as a full date-time string (e.g. "May 15, 2026, 02:30 PM"). */
5067
+ formatDateTime(date) {
5068
+ return of(date.toLocaleString(this.locale, {
5069
+ year: 'numeric', month: 'short', day: 'numeric',
5070
+ hour: '2-digit', minute: '2-digit'
5071
+ }));
5072
+ }
5073
+ /** Formats a Date as a date-only string (e.g. "May 15, 2026"). */
5074
+ formatDate(date) {
5075
+ return of(date.toLocaleDateString(this.locale, {
5076
+ year: 'numeric', month: 'short', day: 'numeric'
5077
+ }));
5078
+ }
5079
+ /** Formats a Date as `YYYY-MM-DD` for use in `<input type="date">` controls. */
5080
+ formatDateForFormControl(date) {
5081
+ const y = date.getFullYear();
5082
+ const m = String(date.getMonth() + 1).padStart(2, '0');
5083
+ const d = String(date.getDate()).padStart(2, '0');
5084
+ return `${y}-${m}-${d}`;
5085
+ }
5086
+ /** Returns `true` if both dates fall on the same calendar day. */
5087
+ isSameDay(date1, date2) {
5088
+ return date1.getFullYear() === date2.getFullYear()
5089
+ && date1.getMonth() === date2.getMonth()
5090
+ && date1.getDate() === date2.getDate();
5091
+ }
5092
+ /** Formats a Date as "Month Year" (e.g. "January 2026"). */
5093
+ formatMonthName(date) {
5094
+ return Promise.resolve(date.toLocaleString(this.locale, { month: 'long', year: 'numeric' }));
5095
+ }
5096
+ 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 });
5097
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter });
5098
+ }
5099
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DefaultCalendarDateFormatter, decorators: [{
5100
+ type: Injectable
5101
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5102
+ type: Optional
5103
+ }, {
5104
+ type: Inject,
5105
+ args: [CALENDAR_CONFIG]
5106
+ }] }] });
5107
+
5108
+ /**
5109
+ * Month grid view showing a 7×6 grid of day cells.
5110
+ *
5111
+ * Each cell displays the day number and up to 3 coloured dots representing
5112
+ * events on that day. Clicking a cell emits `dayClicked`.
5113
+ */
5114
+ class CalendarMonthComponent {
5115
+ /** The date whose month is displayed. */
5116
+ focusDay;
5117
+ /** Observable that emits the full event list whenever it changes. */
5118
+ eventsChanged;
5119
+ /** Observable that emits when the focus day changes. */
5120
+ focusDayChanged;
5121
+ /** Resolved calendar configuration passed from the parent view. */
5122
+ config;
5123
+ /** Emits the date of a clicked day cell. */
5124
+ dayClicked = new EventEmitter();
5125
+ monthItems = [];
5126
+ longDayNames;
5127
+ events = [];
5128
+ destroy$ = new Subject();
5129
+ formatter;
5130
+ constructor() {
5131
+ this.formatter = new DefaultCalendarDateFormatter();
5132
+ this.longDayNames = DEFAULT_CALENDAR_CONFIG.longDayNames;
5133
+ }
5134
+ ngOnInit() {
5135
+ const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5136
+ this.longDayNames = resolved.longDayNames;
5137
+ this.buildMonth();
5138
+ if (this.eventsChanged) {
5139
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5140
+ this.events = events;
5141
+ this.buildMonth();
5142
+ });
5143
+ }
5144
+ if (this.focusDayChanged) {
5145
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5146
+ this.focusDay = date;
5147
+ this.buildMonth();
5148
+ });
5149
+ }
5150
+ }
5151
+ ngOnDestroy() {
5152
+ this.destroy$.next();
5153
+ this.destroy$.complete();
5154
+ }
5155
+ /** Emits the clicked day's date. */
5156
+ onDayClick(date) {
5157
+ this.dayClicked.emit(date);
5158
+ }
5159
+ /** trackBy for day name headers. */
5160
+ trackByDayName(index) {
5161
+ return index;
5162
+ }
5163
+ /** trackBy for month grid cells. */
5164
+ trackByMonthItem(_index, item) {
5165
+ return item.date.getTime();
5166
+ }
5167
+ /** trackBy for event dots. */
5168
+ trackByEventDot(_index, event) {
5169
+ return event.id;
5170
+ }
5171
+ /** Builds the 42-cell month grid (6 rows × 7 columns). */
5172
+ buildMonth() {
5173
+ if (!this.focusDay)
5174
+ return;
5175
+ const year = this.focusDay.getFullYear();
5176
+ const month = this.focusDay.getMonth();
5177
+ const firstDay = new Date(year, month, 1);
5178
+ const lastDay = new Date(year, month + 1, 0);
5179
+ let startOffset = firstDay.getDay() - 1;
5180
+ if (startOffset < 0)
5181
+ startOffset = 6;
5182
+ const today = new Date();
5183
+ this.monthItems = [];
5184
+ for (let i = startOffset - 1; i >= 0; i--) {
5185
+ const date = new Date(year, month, -i);
5186
+ this.monthItems.push(this.createMonthItem(date, false, today));
5187
+ }
5188
+ for (let d = 1; d <= lastDay.getDate(); d++) {
5189
+ const date = new Date(year, month, d);
5190
+ this.monthItems.push(this.createMonthItem(date, true, today));
5191
+ }
5192
+ const remaining = 42 - this.monthItems.length;
5193
+ for (let i = 1; i <= remaining; i++) {
5194
+ const date = new Date(year, month + 1, i);
5195
+ this.monthItems.push(this.createMonthItem(date, false, today));
5196
+ }
5197
+ }
5198
+ createMonthItem(date, isCurrentMonth, today) {
5199
+ const isToday = this.formatter.isSameDay(date, today);
5200
+ const dayEvents = this.events.filter(e => this.formatter.isSameDay(e.startTime, date) ||
5201
+ this.formatter.isSameDay(e.endTime, date) ||
5202
+ (e.startTime < date && e.endTime > date));
5203
+ return {
5204
+ date,
5205
+ dayNumber: date.getDate(),
5206
+ isCurrentMonth,
5207
+ isToday,
5208
+ events: dayEvents
5209
+ };
5210
+ }
5211
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5212
+ 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"] }] });
5213
+ }
5214
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
5215
+ type: Component,
5216
+ 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"] }]
5217
+ }], ctorParameters: () => [], propDecorators: { focusDay: [{
5218
+ type: Input
5219
+ }], eventsChanged: [{
5220
+ type: Input
5221
+ }], focusDayChanged: [{
5222
+ type: Input
5223
+ }], config: [{
5224
+ type: Input
5225
+ }], dayClicked: [{
5226
+ type: Output
5227
+ }] } });
5228
+
5229
+ /**
5230
+ * Service that computes the visual layout of calendar events within a
5231
+ * time-grid (week or day view).
5232
+ *
5233
+ * Responsibilities:
5234
+ * - Splitting multi-day events into per-day segments.
5235
+ * - Assigning non-overlapping column indices to concurrent events.
5236
+ * - Computing the width (column span) each event should occupy.
5237
+ *
5238
+ * This service is stateless — all state is passed via method parameters.
5239
+ * Provide it per-component (not root) so each view gets its own instance.
5240
+ */
5241
+ class CalendarEventLayoutService {
5242
+ /**
5243
+ * Returns `true` when two time ranges overlap (exclusive boundaries).
5244
+ */
5245
+ eventsOverlap(startA, endA, startB, endB) {
5246
+ return startA < endB && startB < endA;
5247
+ }
5248
+ /**
5249
+ * Returns all events whose time range overlaps the given `[start, end)` window.
5250
+ */
5251
+ getAllEventsOnSpecificTime(events, start, end) {
5252
+ return events.filter(e => this.eventsOverlap(e.startTime, e.endTime, start, end));
5253
+ }
5254
+ /**
5255
+ * Splits multi-day events into per-day segments that fit within the
5256
+ * visible hour range (`startHour`–`endHour`) and date range.
5257
+ *
5258
+ * Single-day events are shallow-copied as-is. Multi-day events produce
5259
+ * one segment per day with `continued` / `continuedEnd` flags set.
5260
+ */
5261
+ calculateMultiDayEvents(events, startHour, endHour, rangeStart, rangeEnd) {
5262
+ const result = [];
5263
+ for (const event of events) {
5264
+ const eventStart = new Date(event.startTime);
5265
+ const eventEnd = new Date(event.endTime);
5266
+ if (eventStart.toDateString() === eventEnd.toDateString()) {
5267
+ result.push({ ...event });
5268
+ continue;
5269
+ }
5270
+ const current = new Date(eventStart);
5271
+ let isFirst = true;
5272
+ while (current < eventEnd && current < rangeEnd) {
5273
+ if (current >= rangeStart) {
5274
+ const dayStart = new Date(current);
5275
+ const dayEnd = new Date(current);
5276
+ if (isFirst) {
5277
+ dayEnd.setHours(endHour, 0, 0, 0);
5278
+ }
5279
+ else {
5280
+ dayStart.setHours(startHour, 0, 0, 0);
5281
+ if (current.toDateString() === eventEnd.toDateString()) {
5282
+ dayEnd.setHours(eventEnd.getHours(), eventEnd.getMinutes(), 0, 0);
5283
+ }
5284
+ else {
5285
+ dayEnd.setHours(endHour, 0, 0, 0);
5286
+ }
5287
+ }
5288
+ result.push({
5289
+ ...event,
5290
+ startTime: isFirst ? eventStart : dayStart,
5291
+ endTime: current.toDateString() === eventEnd.toDateString() ? eventEnd : dayEnd,
5292
+ continued: !isFirst,
5293
+ continuedEnd: current.toDateString() !== eventEnd.toDateString()
5294
+ });
5295
+ }
5296
+ isFirst = false;
5297
+ current.setDate(current.getDate() + 1);
5298
+ current.setHours(0, 0, 0, 0);
5299
+ }
5300
+ }
5301
+ return result;
5302
+ }
5303
+ /**
5304
+ * Assigns a zero-based `column` index to each event so that overlapping
5305
+ * events occupy different columns.
5306
+ *
5307
+ * Events are processed in start-time order (longest duration first for ties).
5308
+ * Each event gets the earliest column not already occupied by an overlapping event.
5309
+ */
5310
+ assignColumnsToEvents(events) {
5311
+ const sorted = [...events].sort((a, b) => {
5312
+ const diff = a.startTime.getTime() - b.startTime.getTime();
5313
+ if (diff !== 0)
5314
+ return diff;
5315
+ return (b.endTime.getTime() - b.startTime.getTime()) - (a.endTime.getTime() - a.startTime.getTime());
5316
+ });
5317
+ for (const event of sorted) {
5318
+ event.column = this.findEarliestPossibleColumn(event, sorted);
5319
+ }
5320
+ }
5321
+ /**
5322
+ * Assigns a `width` (column span) to each event, expanding it to fill
5323
+ * unused columns to its right within the overlapping group.
5324
+ */
5325
+ assignWidthsToEvents(events, scanStart, scanEnd) {
5326
+ for (const event of events) {
5327
+ const overlapping = this.getAllEventsOnSpecificTime(events, event.startTime, event.endTime);
5328
+ const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
5329
+ const biggestPossible = this.findBiggestPossibleWidth(event, events, scanStart, scanEnd);
5330
+ event.width = Math.max(1, biggestPossible);
5331
+ if ((event.column ?? 0) + event.width > maxCol + 1) {
5332
+ event.width = maxCol + 1 - (event.column ?? 0);
5333
+ }
5334
+ if (event.width < 1)
5335
+ event.width = 1;
5336
+ }
5337
+ }
5338
+ /** Finds the lowest column index not occupied by any overlapping event. */
5339
+ findEarliestPossibleColumn(event, allEvents) {
5340
+ const overlapping = allEvents.filter(e => e !== event
5341
+ && e.column !== undefined
5342
+ && this.eventsOverlap(e.startTime, e.endTime, event.startTime, event.endTime));
5343
+ let column = 0;
5344
+ while (overlapping.some(e => e.column === column)) {
5345
+ column++;
5346
+ }
5347
+ return column;
5348
+ }
5349
+ /** Computes the maximum width an event can span without overlapping a neighbour to its right. */
5350
+ findBiggestPossibleWidth(event, allEvents, _scanStart, _scanEnd) {
5351
+ const overlapping = this.getAllEventsOnSpecificTime(allEvents, event.startTime, event.endTime);
5352
+ const maxCol = Math.max(...overlapping.map(e => e.column ?? 0));
5353
+ const totalColumns = maxCol + 1;
5354
+ const occupiedCols = overlapping
5355
+ .filter(e => e !== event && (e.column ?? 0) > (event.column ?? 0))
5356
+ .map(e => e.column ?? 0);
5357
+ if (occupiedCols.length === 0) {
5358
+ return totalColumns - (event.column ?? 0);
5359
+ }
5360
+ return Math.min(...occupiedCols) - (event.column ?? 0);
5361
+ }
5362
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5363
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService });
5364
+ }
5365
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventLayoutService, decorators: [{
5366
+ type: Injectable
5367
+ }] });
5368
+
5369
+ /**
5370
+ * Static utility methods for calendar grid positioning.
5371
+ */
5372
+ class CalendarUtility {
5373
+ /**
5374
+ * Converts a weekday (from `Date.getDay()`) to a 1-based Monday-first column index.
5375
+ * Monday = 1, Tuesday = 2, …, Sunday = 7.
5376
+ */
5377
+ static getCorrectColumn(date) {
5378
+ const day = date.getDay();
5379
+ return day === 0 ? 7 : day;
5380
+ }
5381
+ /**
5382
+ * Converts an hour + minute pair to a 1-based CSS grid row index
5383
+ * within a half-hour grid starting at `startHour`.
5384
+ *
5385
+ * Each hour occupies two rows (one per 30-minute slot).
5386
+ * Formula: `(hour - startHour) * 2 + (minute >= 30 ? 1 : 0) + 1`
5387
+ *
5388
+ * @returns Grid row number (minimum 1).
5389
+ */
5390
+ static getCorrectRow(hour, minute, startHour) {
5391
+ const hourOffset = hour - startHour;
5392
+ const row = hourOffset * 2 + (minute >= 30 ? 1 : 0) + 1;
5393
+ return Math.max(1, row);
5394
+ }
5395
+ }
5396
+
5397
+ /**
5398
+ * Default event renderer used when no custom component is provided.
5399
+ *
5400
+ * Displays the event title, formatted time range, and optional description
5401
+ * with the event's colour scheme applied as background and left-border accent.
5402
+ */
5403
+ class CalendarEventDefaultComponent {
5404
+ /** The event to render. Set by {@link CalendarEventComponent} after creation. */
5405
+ event;
5406
+ formattedTime = '';
5407
+ formatter;
5408
+ constructor(formatter) {
5409
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5410
+ }
5411
+ async ngOnInit() {
5412
+ if (this.event) {
5413
+ const start = await this.formatter.formatTime(this.event.startTime);
5414
+ const end = await this.formatter.formatTime(this.event.endTime);
5415
+ this.formattedTime = `${start} - ${end}`;
5416
+ }
5417
+ }
5418
+ 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 });
5419
+ 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"] }] });
5420
+ }
5421
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventDefaultComponent, decorators: [{
5422
+ type: Component,
5423
+ 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"] }]
5424
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5425
+ type: Optional
5426
+ }, {
5427
+ type: Inject,
5428
+ args: [CALENDAR_DATE_FORMATTER]
5429
+ }] }] });
5430
+
5431
+ /**
5432
+ * Dynamic event renderer that injects a custom or default event component
5433
+ * into its view container.
5434
+ *
5435
+ * The component to render is resolved in this order:
5436
+ * 1. `customComponent` input (set on the parent week/day view)
5437
+ * 2. `event.component` (per-event override)
5438
+ * 3. {@link CalendarEventDefaultComponent} (library default)
5439
+ */
5440
+ class CalendarEventComponent {
5441
+ /** The event data to render. */
5442
+ event;
5443
+ /** Optional custom component type that overrides the default renderer. */
5444
+ customComponent;
5445
+ /** Emits when the rendered event is clicked. */
5446
+ eventClicked = new EventEmitter();
5447
+ eventContainer;
5448
+ rendered = false;
5449
+ ngAfterViewInit() {
5450
+ this.renderComponent();
5451
+ }
5452
+ ngOnChanges(changes) {
5453
+ if (this.rendered && (changes['event'] || changes['customComponent'])) {
5454
+ this.renderComponent();
5455
+ }
5456
+ }
5457
+ /** Emits the event click. */
5458
+ onEventClick() {
5459
+ this.eventClicked.emit(this.event);
5460
+ }
5461
+ /** Creates the event component dynamically and sets its `event` property. */
5462
+ renderComponent() {
5463
+ if (!this.eventContainer)
5464
+ return;
5465
+ this.eventContainer.clear();
5466
+ const component = this.customComponent ?? this.event?.component ?? CalendarEventDefaultComponent;
5467
+ const ref = this.eventContainer.createComponent(component);
5468
+ ref.instance.event = this.event;
5469
+ ref.changeDetectorRef.detectChanges();
5470
+ this.rendered = true;
5471
+ }
5472
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5473
+ 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 }] });
5474
+ }
5475
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarEventComponent, decorators: [{
5476
+ type: Component,
5477
+ 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"] }]
5478
+ }], propDecorators: { event: [{
5479
+ type: Input
5480
+ }], customComponent: [{
5481
+ type: Input
5482
+ }], eventClicked: [{
5483
+ type: Output
5484
+ }], eventContainer: [{
5485
+ type: ViewChild,
5486
+ args: ['eventContainer', { read: ViewContainerRef, static: true }]
5487
+ }] } });
5488
+
5489
+ /**
5490
+ * Week grid view showing 7 day columns with half-hour time slots.
5491
+ *
5492
+ * Overlapping events within the same day are laid out in sub-columns
5493
+ * so they appear side-by-side rather than stacked.
5494
+ */
5495
+ class CalendarWeekComponent {
5496
+ layoutService;
5497
+ /** The date around which the week is centred. */
5498
+ focusDay;
5499
+ /** Observable that emits the full event list whenever it changes. */
5500
+ eventsChanged;
5501
+ /** Observable that emits when the focus day changes. */
5502
+ focusDayChanged;
5503
+ /** Resolved calendar configuration passed from the parent view. */
5504
+ config;
5505
+ /** Optional custom event renderer component. */
5506
+ calendarEventComponent;
5507
+ /** Emits when a calendar event is clicked. */
5508
+ eventClicked = new EventEmitter();
5509
+ columns = [];
5510
+ hourRows = [];
5511
+ displayEvents = [];
5512
+ totalRows = 0;
5513
+ currentTimeRow = 0;
5514
+ currentTimeCol = '';
5515
+ gridTemplateColumns = 'repeat(7, 1fr)';
5516
+ dayColumnMap = [];
5517
+ events = [];
5518
+ destroy$ = new Subject();
5519
+ formatter;
5520
+ resolvedConfig;
5521
+ currentTimeInterval;
5522
+ constructor(layoutService) {
5523
+ this.layoutService = layoutService;
5524
+ this.formatter = new DefaultCalendarDateFormatter();
5525
+ }
5526
+ ngOnInit() {
5527
+ this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5528
+ this.buildHourRows();
5529
+ this.buildColumns();
5530
+ this.updateCurrentTime();
5531
+ this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
5532
+ if (this.eventsChanged) {
5533
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5534
+ this.events = events;
5535
+ this.refreshEvents();
5536
+ });
5537
+ }
5538
+ if (this.focusDayChanged) {
5539
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5540
+ this.focusDay = date;
5541
+ this.buildColumns();
5542
+ this.refreshEvents();
5543
+ this.updateCurrentTime();
5544
+ });
5545
+ }
5546
+ }
5547
+ ngOnDestroy() {
5548
+ this.destroy$.next();
5549
+ this.destroy$.complete();
5550
+ if (this.currentTimeInterval)
5551
+ clearInterval(this.currentTimeInterval);
5552
+ }
5553
+ /** Returns the CSS `grid-row` value for an event based on its start/end times. */
5554
+ getEventRow(event) {
5555
+ const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
5556
+ const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
5557
+ return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
5558
+ }
5559
+ /** Returns the CSS `grid-column` span for a day header, accounting for sub-columns. */
5560
+ getHeaderColumn(dayIndex) {
5561
+ if (!this.dayColumnMap.length)
5562
+ return `${dayIndex + 2} / span 1`;
5563
+ const dayInfo = this.dayColumnMap[dayIndex];
5564
+ return `${dayInfo.startCol + 1} / span ${dayInfo.subColumns}`;
5565
+ }
5566
+ /** Returns the CSS `grid-column` value for an event within its day's sub-columns. */
5567
+ getEventColumn(event) {
5568
+ const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, event.startTime));
5569
+ if (dayIdx < 0)
5570
+ return '1 / span 1';
5571
+ const dayInfo = this.dayColumnMap[dayIdx];
5572
+ const subCol = (event.column ?? 0) + dayInfo.startCol;
5573
+ const width = event.width ?? 1;
5574
+ return `${subCol} / span ${width}`;
5575
+ }
5576
+ /** Forwards event click to parent. */
5577
+ onEventClick(event) {
5578
+ this.eventClicked.emit(event);
5579
+ }
5580
+ /** trackBy for hour rows. */
5581
+ trackByHour(_index, row) {
5582
+ return row.hour;
5583
+ }
5584
+ /** trackBy for day columns. */
5585
+ trackByColumn(_index, col) {
5586
+ return col.date.getTime();
5587
+ }
5588
+ /** trackBy for events. */
5589
+ trackByEvent(_index, event) {
5590
+ return event.id;
5591
+ }
5592
+ async buildHourRows() {
5593
+ this.hourRows = [];
5594
+ const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
5595
+ this.totalRows = hours * 2;
5596
+ for (let i = 0; i < hours; i++) {
5597
+ const hour = this.resolvedConfig.startHour + i;
5598
+ const label = await this.formatter.formatTimeI(hour, 0);
5599
+ this.hourRows.push({
5600
+ hour,
5601
+ topRow: i * 2 + 1,
5602
+ bottomRow: i * 2 + 3,
5603
+ hourLabel: label
5604
+ });
5605
+ }
5606
+ }
5607
+ /** Builds the 7 day columns for the current week (Monday–Sunday). */
5608
+ buildColumns() {
5609
+ if (!this.focusDay)
5610
+ return;
5611
+ const shortNames = this.resolvedConfig.shortDayNames;
5612
+ const today = new Date();
5613
+ const day = this.focusDay.getDay();
5614
+ const mondayOffset = day === 0 ? -6 : 1 - day;
5615
+ const monday = new Date(this.focusDay);
5616
+ monday.setDate(this.focusDay.getDate() + mondayOffset);
5617
+ this.columns = [];
5618
+ for (let i = 0; i < 7; i++) {
5619
+ const date = new Date(monday);
5620
+ date.setDate(monday.getDate() + i);
5621
+ this.columns.push({
5622
+ date,
5623
+ dayName: shortNames[i],
5624
+ dayNumber: date.getDate(),
5625
+ isToday: this.formatter.isSameDay(date, today)
5626
+ });
5627
+ }
5628
+ }
5629
+ /** Filters, splits, and lays out events for the current week. */
5630
+ refreshEvents() {
5631
+ if (!this.columns.length)
5632
+ return;
5633
+ const rangeStart = this.columns[0].date;
5634
+ const rangeEnd = new Date(this.columns[6].date);
5635
+ rangeEnd.setHours(23, 59, 59, 999);
5636
+ const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
5637
+ this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
5638
+ // Assign columns per day so overlapping events within a day get sub-columns
5639
+ for (let i = 0; i < 7; i++) {
5640
+ const dayStart = new Date(this.columns[i].date);
5641
+ dayStart.setHours(0, 0, 0, 0);
5642
+ const dayEnd = new Date(this.columns[i].date);
5643
+ dayEnd.setHours(23, 59, 59, 999);
5644
+ const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
5645
+ this.layoutService.assignColumnsToEvents(dayEvents);
5646
+ this.layoutService.assignWidthsToEvents(dayEvents, dayStart, dayEnd);
5647
+ }
5648
+ this.buildGridColumns();
5649
+ this.updateCurrentTime();
5650
+ }
5651
+ /** Computes the CSS grid-template-columns string based on per-day sub-column counts. */
5652
+ buildGridColumns() {
5653
+ this.dayColumnMap = [];
5654
+ let currentCol = 1;
5655
+ for (let i = 0; i < 7; i++) {
5656
+ const dayEvents = this.displayEvents.filter(e => this.formatter.isSameDay(e.startTime, this.columns[i].date));
5657
+ let maxSubCols = 1;
5658
+ for (const e of dayEvents) {
5659
+ maxSubCols = Math.max(maxSubCols, (e.column ?? 0) + (e.width ?? 1));
5660
+ }
5661
+ this.dayColumnMap.push({ subColumns: maxSubCols, startCol: currentCol });
5662
+ currentCol += maxSubCols;
5663
+ }
5664
+ const parts = [];
5665
+ for (const day of this.dayColumnMap) {
5666
+ for (let j = 0; j < day.subColumns; j++) {
5667
+ parts.push(`${1 / day.subColumns}fr`);
5668
+ }
5669
+ }
5670
+ this.gridTemplateColumns = parts.join(' ');
5671
+ }
5672
+ /** Updates the current-time red line position. */
5673
+ updateCurrentTime() {
5674
+ const now = new Date();
5675
+ const dayIdx = this.columns.findIndex(c => this.formatter.isSameDay(c.date, now));
5676
+ if (dayIdx >= 0 && this.dayColumnMap.length > 0) {
5677
+ const dayInfo = this.dayColumnMap[dayIdx];
5678
+ this.currentTimeCol = `${dayInfo.startCol} / span ${dayInfo.subColumns}`;
5679
+ this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
5680
+ }
5681
+ else {
5682
+ this.currentTimeCol = '';
5683
+ this.currentTimeRow = 0;
5684
+ }
5685
+ }
5686
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
5687
+ 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"] }] });
5688
+ }
5689
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
5690
+ type: Component,
5691
+ 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"] }]
5692
+ }], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
5693
+ type: Input
5694
+ }], eventsChanged: [{
5695
+ type: Input
5696
+ }], focusDayChanged: [{
5697
+ type: Input
5698
+ }], config: [{
5699
+ type: Input
5700
+ }], calendarEventComponent: [{
5701
+ type: Input
5702
+ }], eventClicked: [{
5703
+ type: Output
5704
+ }] } });
5705
+
5706
+ /**
5707
+ * Day grid view showing a single day with half-hour time slots.
5708
+ *
5709
+ * Shares the same layout algorithm as the week view via
5710
+ * {@link CalendarEventLayoutService}.
5711
+ */
5712
+ class CalendarDayComponent {
5713
+ layoutService;
5714
+ /** The date to display. */
5715
+ focusDay;
5716
+ /** Observable that emits the full event list whenever it changes. */
5717
+ eventsChanged;
5718
+ /** Observable that emits when the focus day changes. */
5719
+ focusDayChanged;
5720
+ /** Resolved calendar configuration passed from the parent view. */
5721
+ config;
5722
+ /** Optional custom event renderer component. */
5723
+ calendarEventComponent;
5724
+ /** Emits when a calendar event is clicked. */
5725
+ eventClicked = new EventEmitter();
5726
+ hourRows = [];
5727
+ displayEvents = [];
5728
+ totalRows = 0;
5729
+ totalColumns = 1;
5730
+ currentTimeRow = 0;
5731
+ isToday = false;
5732
+ dayName = '';
5733
+ events = [];
5734
+ destroy$ = new Subject();
5735
+ formatter;
5736
+ resolvedConfig;
5737
+ currentTimeInterval;
5738
+ constructor(layoutService) {
5739
+ this.layoutService = layoutService;
5740
+ this.formatter = new DefaultCalendarDateFormatter();
5741
+ }
5742
+ ngOnInit() {
5743
+ this.resolvedConfig = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5744
+ this.buildHourRows();
5745
+ this.updateDayInfo();
5746
+ this.updateCurrentTime();
5747
+ this.currentTimeInterval = setInterval(() => this.updateCurrentTime(), 60000);
5748
+ if (this.eventsChanged) {
5749
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5750
+ this.events = events;
5751
+ this.refreshEvents();
5752
+ });
5753
+ }
5754
+ if (this.focusDayChanged) {
5755
+ this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
5756
+ this.focusDay = date;
5757
+ this.updateDayInfo();
5758
+ this.refreshEvents();
5759
+ this.updateCurrentTime();
5760
+ });
5761
+ }
5762
+ }
5763
+ ngOnDestroy() {
5764
+ this.destroy$.next();
5765
+ this.destroy$.complete();
5766
+ if (this.currentTimeInterval)
5767
+ clearInterval(this.currentTimeInterval);
5768
+ }
5769
+ /** Returns the CSS `grid-row` value for an event. */
5770
+ getEventRow(event) {
5771
+ const startRow = CalendarUtility.getCorrectRow(event.startTime.getHours(), event.startTime.getMinutes(), this.resolvedConfig.startHour);
5772
+ const endRow = CalendarUtility.getCorrectRow(event.endTime.getHours(), event.endTime.getMinutes(), this.resolvedConfig.startHour);
5773
+ return `${startRow} / ${Math.max(endRow, startRow + 1)}`;
5774
+ }
5775
+ /** Returns the CSS `grid-column` value for an event within its sub-columns. */
5776
+ getEventColumn(event) {
5777
+ const col = (event.column ?? 0) + 1;
5778
+ const width = event.width ?? 1;
5779
+ return `${col} / span ${width}`;
5780
+ }
5781
+ /** Forwards event click to parent. */
5782
+ onEventClick(event) {
5783
+ this.eventClicked.emit(event);
5784
+ }
5785
+ /** trackBy for hour rows. */
5786
+ trackByHour(_index, row) {
5787
+ return row.hour;
5788
+ }
5789
+ /** trackBy for events. */
5790
+ trackByEvent(_index, event) {
5791
+ return event.id;
5792
+ }
5793
+ async buildHourRows() {
5794
+ this.hourRows = [];
5795
+ const hours = this.resolvedConfig.endHour - this.resolvedConfig.startHour;
5796
+ this.totalRows = hours * 2;
5797
+ for (let i = 0; i < hours; i++) {
5798
+ const hour = this.resolvedConfig.startHour + i;
5799
+ const label = await this.formatter.formatTimeI(hour, 0);
5800
+ this.hourRows.push({
5801
+ hour,
5802
+ topRow: i * 2 + 1,
5803
+ bottomRow: i * 2 + 3,
5804
+ hourLabel: label
5805
+ });
5806
+ }
5807
+ }
5808
+ /** Updates the day name and isToday flag. */
5809
+ updateDayInfo() {
5810
+ if (!this.focusDay)
5811
+ return;
5812
+ const today = new Date();
5813
+ this.isToday = this.formatter.isSameDay(this.focusDay, today);
5814
+ const longNames = this.resolvedConfig.longDayNames;
5815
+ const dayIdx = this.focusDay.getDay();
5816
+ const mondayIdx = dayIdx === 0 ? 6 : dayIdx - 1;
5817
+ this.dayName = longNames[mondayIdx];
5818
+ }
5819
+ /** Filters, splits, and lays out events for the focus day. */
5820
+ refreshEvents() {
5821
+ if (!this.focusDay)
5822
+ return;
5823
+ const rangeStart = new Date(this.focusDay);
5824
+ rangeStart.setHours(0, 0, 0, 0);
5825
+ const rangeEnd = new Date(this.focusDay);
5826
+ rangeEnd.setHours(23, 59, 59, 999);
5827
+ const filtered = this.events.filter(e => this.layoutService.eventsOverlap(e.startTime, e.endTime, rangeStart, rangeEnd));
5828
+ this.displayEvents = this.layoutService.calculateMultiDayEvents(filtered, this.resolvedConfig.startHour, this.resolvedConfig.endHour, rangeStart, rangeEnd);
5829
+ this.layoutService.assignColumnsToEvents(this.displayEvents);
5830
+ this.layoutService.assignWidthsToEvents(this.displayEvents, rangeStart, rangeEnd);
5831
+ const maxCol = this.displayEvents.reduce((max, e) => Math.max(max, (e.column ?? 0) + (e.width ?? 1)), 1);
5832
+ this.totalColumns = maxCol;
5833
+ }
5834
+ /** Updates the current-time red line position. */
5835
+ updateCurrentTime() {
5836
+ const now = new Date();
5837
+ if (this.focusDay && this.formatter.isSameDay(this.focusDay, now)) {
5838
+ this.currentTimeRow = CalendarUtility.getCorrectRow(now.getHours(), now.getMinutes(), this.resolvedConfig.startHour);
5839
+ this.isToday = true;
5840
+ }
5841
+ else {
5842
+ this.currentTimeRow = 0;
5843
+ this.isToday = false;
5844
+ }
5845
+ }
5846
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [{ token: CalendarEventLayoutService }], target: i0.ɵɵFactoryTarget.Component });
5847
+ 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"] }] });
5848
+ }
5849
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
5850
+ type: Component,
5851
+ 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"] }]
5852
+ }], ctorParameters: () => [{ type: CalendarEventLayoutService }], propDecorators: { focusDay: [{
5853
+ type: Input
5854
+ }], eventsChanged: [{
5855
+ type: Input
5856
+ }], focusDayChanged: [{
5857
+ type: Input
5858
+ }], config: [{
5859
+ type: Input
5860
+ }], calendarEventComponent: [{
5861
+ type: Input
5862
+ }], eventClicked: [{
5863
+ type: Output
5864
+ }] } });
5865
+
5866
+ /**
5867
+ * Renders a single row in the upcoming-events sidebar.
5868
+ * Shows the event title, formatted date/time, and optional description.
5869
+ */
5870
+ class UpcomingEventRowComponent {
5871
+ /** The event to display. */
5872
+ event;
5873
+ /** Emits the event when this row is clicked. */
5874
+ eventClicked = new EventEmitter();
5875
+ formattedDate = '';
5876
+ formatter;
5877
+ constructor(formatter) {
5878
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
5879
+ }
5880
+ async ngOnInit() {
5881
+ if (this.event) {
5882
+ const start = await this.formatter.formatTime(this.event.startTime);
5883
+ const end = await this.formatter.formatTime(this.event.endTime);
5884
+ this.formattedDate = `${start} - ${end}`;
5885
+ }
5886
+ }
5887
+ 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 });
5888
+ 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"] }] });
5889
+ }
5890
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventRowComponent, decorators: [{
5891
+ type: Component,
5892
+ 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"] }]
5893
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
5894
+ type: Optional
5895
+ }, {
5896
+ type: Inject,
5897
+ args: [CALENDAR_DATE_FORMATTER]
5898
+ }] }], propDecorators: { event: [{
5899
+ type: Input
5900
+ }], eventClicked: [{
5901
+ type: Output
5902
+ }] } });
5903
+
5904
+ /**
5905
+ * Sidebar component that lists the next 10 upcoming events
5906
+ * (events whose end time is in the future), sorted by start time.
5907
+ */
5908
+ class UpcomingEventsComponent {
5909
+ /** Observable that emits the full event list whenever it changes. */
5910
+ eventsChanged;
5911
+ /** Resolved calendar configuration passed from the parent view. */
5912
+ config;
5913
+ /** Emits when an upcoming event row is clicked. */
5914
+ eventClicked = new EventEmitter();
5915
+ upcomingEvents = [];
5916
+ title;
5917
+ destroy$ = new Subject();
5918
+ constructor() {
5919
+ this.title = DEFAULT_CALENDAR_CONFIG.upcomingEventsTitle;
5920
+ }
5921
+ ngOnInit() {
5922
+ const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
5923
+ this.title = resolved.upcomingEventsTitle;
5924
+ if (this.eventsChanged) {
5925
+ this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
5926
+ const now = new Date();
5927
+ this.upcomingEvents = events
5928
+ .filter(e => e.endTime > now)
5929
+ .sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
5930
+ .slice(0, 10);
5931
+ });
5932
+ }
5933
+ }
5934
+ ngOnDestroy() {
5935
+ this.destroy$.next();
5936
+ this.destroy$.complete();
5937
+ }
5938
+ /** trackBy for upcoming event rows. */
5939
+ trackByEvent(_index, event) {
5940
+ return event.id;
5941
+ }
5942
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5943
+ 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"] }] });
5944
+ }
5945
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
5946
+ type: Component,
5947
+ 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"] }]
5948
+ }], ctorParameters: () => [], propDecorators: { eventsChanged: [{
5949
+ type: Input
5950
+ }], config: [{
5951
+ type: Input
5952
+ }], eventClicked: [{
5953
+ type: Output
5954
+ }] } });
5955
+
5956
+ /**
5957
+ * Main calendar orchestrator component.
5958
+ *
5959
+ * Provides a toolbar with view switching (month / week / day), date navigation,
5960
+ * and an optional action button. The active view and an upcoming-events sidebar
5961
+ * are rendered inside a responsive grid layout.
5962
+ *
5963
+ * All configuration (visible hours, locale, labels, mobile breakpoint) is read
5964
+ * from the `mn-config.json5` system via {@link MN_CALENDAR_CONFIG}, falling back
5965
+ * to the legacy {@link CALENDAR_CONFIG} injection token, then to built-in defaults.
5966
+ * Date formatting is delegated to the {@link CALENDAR_DATE_FORMATTER} token.
5967
+ *
5968
+ * @example
5969
+ * ```html
5970
+ * <app-calendar-view
5971
+ * [showButton]="true"
5972
+ * [buttonTitle]="'New Event'"
5973
+ * [NewCalendarItemsEvent]="eventsEmitter"
5974
+ * (RequestNewCalendarItemsEvent)="loadEvents($event)"
5975
+ * (CalendarItemClickedEvent)="onEventClick($event)"
5976
+ * (ButtonClickedEvent)="openModal()">
5977
+ * </app-calendar-view>
5978
+ * ```
5979
+ */
5980
+ class CalendarViewComponent {
5981
+ /** Whether to show the action button in the toolbar. */
5982
+ showButton = false;
5983
+ /** Label text for the action button. */
5984
+ buttonTitle = '';
5985
+ /** Custom event renderer component type. */
5986
+ CalendarEventComponent;
5987
+ /** Observable or EventEmitter that pushes new event arrays into the calendar. */
5988
+ NewCalendarItemsEvent;
5989
+ /** Emits when the calendar needs fresh event data (e.g. after navigation). */
5990
+ RequestNewCalendarItemsEvent = new EventEmitter();
5991
+ /** Emits when a calendar event is clicked. */
5992
+ CalendarItemClickedEvent = new EventEmitter();
5993
+ /** Emits when the action button is clicked. */
5994
+ ButtonClickedEvent = new EventEmitter();
5995
+ CalendarView = CalendarView;
5996
+ currentView = CalendarView.WEEK;
5997
+ focusDay = new Date();
5998
+ dateInputValue = '';
5999
+ viewOptions = [];
6000
+ isMobileView = false;
6001
+ /** BehaviorSubject so late-subscribing child views receive the last emitted events. */
6002
+ internalEventsChanged = new BehaviorSubject([]);
6003
+ /** Subject for broadcasting focus-day changes to child views. */
6004
+ internalFocusDayChanged = new Subject();
6005
+ destroy$ = new Subject();
6006
+ formatter;
6007
+ config;
6008
+ destroyRef = inject(DestroyRef);
6009
+ lang = inject(MnLanguageService);
6010
+ constructor(formatter, mnConfig, legacyConfig) {
6011
+ this.formatter = formatter ?? new DefaultCalendarDateFormatter();
6012
+ // Priority: mn-config system > legacy CALENDAR_CONFIG > built-in defaults
6013
+ const raw = mnConfig ?? legacyConfig ?? undefined;
6014
+ this.config = resolveCalendarConfig(raw);
6015
+ }
6016
+ onResize() {
6017
+ this.checkMobileView();
6018
+ }
6019
+ ngOnInit() {
6020
+ this.rebuildFromConfig();
6021
+ // Re-resolve config when locale changes (supports $translate in mn-config).
6022
+ const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
6023
+ this.rebuildFromConfig();
6024
+ });
6025
+ this.destroyRef.onDestroy(() => sub.unsubscribe());
6026
+ this.checkMobileView();
6027
+ this.updateDateInput();
6028
+ this.RequestNewCalendarItemsEvent.emit(this.focusDay);
6029
+ if (this.NewCalendarItemsEvent) {
6030
+ this.NewCalendarItemsEvent.pipe(takeUntil(this.destroy$)).subscribe(events => {
6031
+ this.internalEventsChanged.next(events);
6032
+ });
6033
+ }
6034
+ }
6035
+ ngOnDestroy() {
6036
+ this.destroy$.next();
6037
+ this.destroy$.complete();
6038
+ }
6039
+ /** Switches the active view. On mobile, forces day view. */
6040
+ switchView(view) {
6041
+ if (this.isMobileView) {
6042
+ this.currentView = CalendarView.DAY;
6043
+ return;
6044
+ }
6045
+ this.currentView = view;
6046
+ }
6047
+ /** Navigates to the previous period (month / week / day). */
6048
+ navigatePrevious() {
6049
+ const d = new Date(this.focusDay);
6050
+ switch (this.currentView) {
6051
+ case CalendarView.MONTH:
6052
+ d.setMonth(d.getMonth() - 1);
6053
+ break;
6054
+ case CalendarView.WEEK:
6055
+ d.setDate(d.getDate() - 7);
6056
+ break;
6057
+ case CalendarView.DAY:
6058
+ d.setDate(d.getDate() - 1);
6059
+ break;
6060
+ }
6061
+ this.setFocusDay(d);
6062
+ }
6063
+ /** Navigates to the next period (month / week / day). */
6064
+ navigateNext() {
6065
+ const d = new Date(this.focusDay);
6066
+ switch (this.currentView) {
6067
+ case CalendarView.MONTH:
6068
+ d.setMonth(d.getMonth() + 1);
6069
+ break;
6070
+ case CalendarView.WEEK:
6071
+ d.setDate(d.getDate() + 7);
6072
+ break;
6073
+ case CalendarView.DAY:
6074
+ d.setDate(d.getDate() + 1);
6075
+ break;
6076
+ }
6077
+ this.setFocusDay(d);
6078
+ }
6079
+ /** Navigates to today. */
6080
+ goToToday() {
6081
+ this.setFocusDay(new Date());
6082
+ }
6083
+ /** Handles the date-picker input change. */
6084
+ onDateInputChange(event) {
6085
+ const value = event.target.value;
6086
+ if (value) {
6087
+ this.setFocusDay(new Date(value));
6088
+ }
6089
+ }
6090
+ /** Handles a day click from the month view — switches to day view. */
6091
+ onMonthDayClick(date) {
6092
+ this.currentView = CalendarView.DAY;
6093
+ this.setFocusDay(date);
6094
+ }
6095
+ /** Forwards a child event click to the parent output. */
6096
+ onEventClick(event) {
6097
+ this.CalendarItemClickedEvent.emit(event);
6098
+ }
6099
+ /** trackBy for view option buttons. */
6100
+ trackByView(_index, item) {
6101
+ return item.value;
6102
+ }
6103
+ /** Rebuilds view options and labels from the current config. */
6104
+ rebuildFromConfig() {
6105
+ this.viewOptions = [
6106
+ { value: CalendarView.MONTH, label: this.config.viewLabels['MONTH'] ?? 'Month' },
6107
+ { value: CalendarView.WEEK, label: this.config.viewLabels['WEEK'] ?? 'Week' },
6108
+ { value: CalendarView.DAY, label: this.config.viewLabels['DAY'] ?? 'Day' }
6109
+ ];
6110
+ }
6111
+ checkMobileView() {
6112
+ const wasMobile = this.isMobileView;
6113
+ this.isMobileView = window.innerWidth < this.config.mobileBreakpoint;
6114
+ if (this.isMobileView && !wasMobile) {
6115
+ this.currentView = CalendarView.DAY;
6116
+ }
6117
+ }
6118
+ setFocusDay(date) {
6119
+ this.focusDay = date;
6120
+ this.updateDateInput();
6121
+ this.internalFocusDayChanged.next(date);
6122
+ this.RequestNewCalendarItemsEvent.emit(date);
6123
+ }
6124
+ updateDateInput() {
6125
+ this.dateInputValue = this.formatter.formatDateForFormControl(this.focusDay);
6126
+ }
6127
+ 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 });
6128
+ 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: [
6129
+ provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
6130
+ ], 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"] }] });
6131
+ }
6132
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
6133
+ type: Component,
6134
+ args: [{ selector: 'app-calendar-view', standalone: true, imports: [
6135
+ CommonModule,
6136
+ CalendarMonthComponent,
6137
+ CalendarWeekComponent,
6138
+ CalendarDayComponent,
6139
+ UpcomingEventsComponent
6140
+ ], providers: [
6141
+ provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
6142
+ ], 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"] }]
6143
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
6144
+ type: Optional
6145
+ }, {
6146
+ type: Inject,
6147
+ args: [CALENDAR_DATE_FORMATTER]
6148
+ }] }, { type: undefined, decorators: [{
6149
+ type: Optional
6150
+ }, {
6151
+ type: Inject,
6152
+ args: [MN_CALENDAR_CONFIG]
6153
+ }] }, { type: undefined, decorators: [{
6154
+ type: Optional
6155
+ }, {
6156
+ type: Inject,
6157
+ args: [CALENDAR_CONFIG]
6158
+ }] }], propDecorators: { showButton: [{
6159
+ type: Input
6160
+ }], buttonTitle: [{
6161
+ type: Input
6162
+ }], CalendarEventComponent: [{
6163
+ type: Input
6164
+ }], NewCalendarItemsEvent: [{
6165
+ type: Input
6166
+ }], RequestNewCalendarItemsEvent: [{
6167
+ type: Output
6168
+ }], CalendarItemClickedEvent: [{
6169
+ type: Output
6170
+ }], ButtonClickedEvent: [{
6171
+ type: Output
6172
+ }], onResize: [{
6173
+ type: HostListener,
6174
+ args: ['window:resize']
6175
+ }] } });
6176
+
6177
+ // Main component
6178
+
4864
6179
  class MnSectionDirective {
4865
6180
  /** Section name contributed by this DOM node to the section path */
4866
6181
  mnSection;
@@ -5310,6 +6625,9 @@ function provideMnLanguage(config) {
5310
6625
  provide: APP_INITIALIZER,
5311
6626
  multi: true,
5312
6627
  useFactory: (svc) => async () => {
6628
+ if (config.debug) {
6629
+ svc.setDebug(true);
6630
+ }
5313
6631
  svc.configure(config.urlPattern);
5314
6632
  const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
5315
6633
  const localesToLoad = config.preload ?? [effectiveLocale];
@@ -5394,5 +6712,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
5394
6712
  * Generated bundle index. Do not edit.
5395
6713
  */
5396
6714
 
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 };
6715
+ 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
6716
  //# sourceMappingURL=mn-angular-lib.mjs.map