@bloc-ui/date-picker 0.0.2 → 0.0.3

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,99 +1,85 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, input, signal, computed, forwardRef, HostListener, Component, NgModule } from '@angular/core';
2
+ import { input, output, signal, computed, Component, inject, ElementRef, ViewContainerRef, Renderer2, DestroyRef, forwardRef, Directive, NgModule } from '@angular/core';
3
+ import { DOCUMENT } from '@angular/common';
3
4
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
5
 
5
6
  const DAYS_OF_WEEK = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
6
- /**
7
- * Date picker component with calendar dropdown.
8
- *
9
- * ```html
10
- * <bloc-date-picker [(ngModel)]="selectedDate" />
11
- * <bloc-date-picker [formControl]="dateCtrl" placeholder="Choose date" />
12
- * ```
13
- */
14
- class BlocDatePickerComponent {
15
- _el = inject((ElementRef));
16
- /** Preset size. Defaults to `'md'`. */
17
- size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
18
- /** Placeholder text for the input. */
19
- placeholder = input('Select date', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
20
- /** Date format for display. Defaults to `'yyyy-MM-dd'`. */
21
- format = input('yyyy-MM-dd', ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
22
- /** Disable the date picker via template binding. */
23
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
7
+ const MONTHS_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
8
+ class BlocCalendarPanelComponent {
9
+ /** Currently selected date (passed in by parent). */
10
+ selectedDate = input(null, ...(ngDevMode ? [{ debugName: "selectedDate" }] : /* istanbul ignore next */ []));
24
11
  /** Minimum selectable date. */
25
12
  minDate = input(null, ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
26
13
  /** Maximum selectable date. */
27
14
  maxDate = input(null, ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
15
+ /** Panel mode: 'single' for single date, 'range' for date range. */
16
+ mode = input('single', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
17
+ /** Range start date (range mode). */
18
+ rangeStart = input(null, ...(ngDevMode ? [{ debugName: "rangeStart" }] : /* istanbul ignore next */ []));
19
+ /** Range end date (range mode). */
20
+ rangeEnd = input(null, ...(ngDevMode ? [{ debugName: "rangeEnd" }] : /* istanbul ignore next */ []));
21
+ /** Hover date for range preview (range mode). */
22
+ hoverDate = input(null, ...(ngDevMode ? [{ debugName: "hoverDate" }] : /* istanbul ignore next */ []));
23
+ /** Emitted when a day is picked or "Today" is clicked. */
24
+ dateSelect = output();
25
+ /** Emitted when "Clear" is clicked. */
26
+ cleared = output();
27
+ /** Emitted when a day is hovered (range mode). */
28
+ dateHover = output();
28
29
  weekdays = DAYS_OF_WEEK;
29
- /** Currently selected date. */
30
- _selectedDate = signal(null, ...(ngDevMode ? [{ debugName: "_selectedDate" }] : /* istanbul ignore next */ []));
31
- /** Whether the calendar dropdown is open. */
32
- isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
33
- /** The month being viewed (year + month). */
30
+ months = MONTHS_SHORT;
34
31
  _viewDate = signal(new Date(), ...(ngDevMode ? [{ debugName: "_viewDate" }] : /* istanbul ignore next */ []));
35
- _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : /* istanbul ignore next */ []));
36
- isDisabled = computed(() => this.disabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
37
- _onChange = () => { };
38
- _onTouched = () => { };
39
- displayValue = computed(() => {
40
- const d = this._selectedDate();
41
- if (!d)
42
- return '';
43
- return this._formatDate(d);
44
- }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
32
+ _view = signal('day', ...(ngDevMode ? [{ debugName: "_view" }] : /* istanbul ignore next */ []));
33
+ _yearRangeStart = signal(new Date().getFullYear() - 4, ...(ngDevMode ? [{ debugName: "_yearRangeStart" }] : /* istanbul ignore next */ []));
34
+ yearRange = computed(() => Array.from({ length: 12 }, (_, i) => this._yearRangeStart() + i), ...(ngDevMode ? [{ debugName: "yearRange" }] : /* istanbul ignore next */ []));
35
+ viewYear = computed(() => this._viewDate().getFullYear(), ...(ngDevMode ? [{ debugName: "viewYear" }] : /* istanbul ignore next */ []));
36
+ viewMonth = computed(() => this._viewDate().getMonth(), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
37
+ currentYear = new Date().getFullYear();
38
+ currentMonth = new Date().getMonth();
45
39
  monthYearLabel = computed(() => {
46
40
  const d = this._viewDate();
47
41
  return d.toLocaleString('default', { month: 'long', year: 'numeric' });
48
42
  }, ...(ngDevMode ? [{ debugName: "monthYearLabel" }] : /* istanbul ignore next */ []));
49
43
  calendarDays = computed(() => {
50
44
  const viewDate = this._viewDate();
51
- const selected = this._selectedDate();
45
+ const selected = this.selectedDate();
52
46
  const today = new Date();
53
47
  const min = this.minDate();
54
48
  const max = this.maxDate();
49
+ const rStart = this.rangeStart();
50
+ const rEnd = this.rangeEnd();
51
+ const hover = this.hoverDate();
52
+ const isRange = this.mode() === 'range';
55
53
  const year = viewDate.getFullYear();
56
54
  const month = viewDate.getMonth();
57
55
  const firstOfMonth = new Date(year, month, 1);
58
56
  const startDay = firstOfMonth.getDay();
59
57
  const daysInMonth = new Date(year, month + 1, 0).getDate();
60
- // Fill from previous month
61
58
  const prevMonthDays = new Date(year, month, 0).getDate();
62
59
  const days = [];
63
60
  for (let i = startDay - 1; i >= 0; i--) {
64
61
  const date = new Date(year, month - 1, prevMonthDays - i);
65
- days.push(this._buildDay(date, false, today, selected, min, max));
62
+ days.push(this._buildDay(date, false, today, selected, min, max, isRange, rStart, rEnd, hover));
66
63
  }
67
64
  for (let d = 1; d <= daysInMonth; d++) {
68
65
  const date = new Date(year, month, d);
69
- days.push(this._buildDay(date, true, today, selected, min, max));
66
+ days.push(this._buildDay(date, true, today, selected, min, max, isRange, rStart, rEnd, hover));
70
67
  }
71
- // Fill remaining to complete 6 rows (42 cells)
72
68
  const remaining = 42 - days.length;
73
69
  for (let d = 1; d <= remaining; d++) {
74
70
  const date = new Date(year, month + 1, d);
75
- days.push(this._buildDay(date, false, today, selected, min, max));
71
+ days.push(this._buildDay(date, false, today, selected, min, max, isRange, rStart, rEnd, hover));
76
72
  }
77
73
  return days;
78
74
  }, ...(ngDevMode ? [{ debugName: "calendarDays" }] : /* istanbul ignore next */ []));
79
- toggleCalendar() {
80
- if (this.isDisabled())
81
- return;
82
- this.isOpen.update((v) => !v);
83
- if (this.isOpen()) {
84
- const selected = this._selectedDate();
85
- if (selected) {
86
- this._viewDate.set(new Date(selected.getFullYear(), selected.getMonth(), 1));
87
- }
75
+ /** Reset the panel to day view, optionally centred on the given date. */
76
+ resetView(date) {
77
+ this._view.set('day');
78
+ if (date) {
79
+ this._viewDate.set(new Date(date.getFullYear(), date.getMonth(), 1));
88
80
  }
89
81
  }
90
- selectDay(day) {
91
- if (day.isDisabled)
92
- return;
93
- this._selectedDate.set(day.date);
94
- this._onChange(day.date);
95
- this.isOpen.set(false);
96
- }
82
+ // — Navigation —
97
83
  prevMonth() {
98
84
  const d = this._viewDate();
99
85
  this._viewDate.set(new Date(d.getFullYear(), d.getMonth() - 1, 1));
@@ -102,40 +88,58 @@ class BlocDatePickerComponent {
102
88
  const d = this._viewDate();
103
89
  this._viewDate.set(new Date(d.getFullYear(), d.getMonth() + 1, 1));
104
90
  }
105
- goToToday() {
106
- const today = new Date();
107
- this._selectedDate.set(today);
108
- this._viewDate.set(new Date(today.getFullYear(), today.getMonth(), 1));
109
- this._onChange(today);
110
- this.isOpen.set(false);
91
+ openMonthView() {
92
+ this._view.set('month');
111
93
  }
112
- clear() {
113
- this._selectedDate.set(null);
114
- this._onChange(null);
94
+ openYearView() {
95
+ const viewYear = this._viewDate().getFullYear();
96
+ this._yearRangeStart.set(viewYear - 4);
97
+ this._view.set('year');
115
98
  }
116
- _onDocClick(event) {
117
- if (!this._el.nativeElement.contains(event.target)) {
118
- this.isOpen.set(false);
119
- }
99
+ selectMonth(monthIndex) {
100
+ const d = this._viewDate();
101
+ this._viewDate.set(new Date(d.getFullYear(), monthIndex, 1));
102
+ this._view.set('day');
120
103
  }
121
- _onEsc() {
122
- this.isOpen.set(false);
104
+ selectYear(year) {
105
+ const d = this._viewDate();
106
+ this._viewDate.set(new Date(year, d.getMonth(), 1));
107
+ this._view.set('month');
123
108
  }
124
- // — ControlValueAccessor —
125
- writeValue(val) {
126
- this._selectedDate.set(val instanceof Date ? val : val ? new Date(val) : null);
109
+ prevYear() {
110
+ const d = this._viewDate();
111
+ this._viewDate.set(new Date(d.getFullYear() - 1, d.getMonth(), 1));
127
112
  }
128
- registerOnChange(fn) {
129
- this._onChange = fn;
113
+ nextYear() {
114
+ const d = this._viewDate();
115
+ this._viewDate.set(new Date(d.getFullYear() + 1, d.getMonth(), 1));
130
116
  }
131
- registerOnTouched(fn) {
132
- this._onTouched = fn;
117
+ prevYearRange() {
118
+ this._yearRangeStart.update((v) => v - 12);
133
119
  }
134
- setDisabledState(isDisabled) {
135
- this._formDisabled.set(isDisabled);
120
+ nextYearRange() {
121
+ this._yearRangeStart.update((v) => v + 12);
122
+ }
123
+ selectDay(day) {
124
+ if (day.isDisabled)
125
+ return;
126
+ this.dateSelect.emit(day.date);
127
+ }
128
+ onDayHover(day) {
129
+ if (this.mode() === 'range' && !day.isDisabled) {
130
+ this.dateHover.emit(day.date);
131
+ }
132
+ }
133
+ goToToday() {
134
+ const today = new Date();
135
+ this._viewDate.set(new Date(today.getFullYear(), today.getMonth(), 1));
136
+ this.dateSelect.emit(today);
137
+ }
138
+ clear() {
139
+ this.cleared.emit();
136
140
  }
137
141
  // — Private helpers —
138
- _buildDay(date, isCurrentMonth, today, selected, min, max) {
142
+ _buildDay(date, isCurrentMonth, today, selected, min, max, isRange = false, rangeStart = null, rangeEnd = null, hoverDate = null) {
139
143
  const isToday = date.getDate() === today.getDate() &&
140
144
  date.getMonth() === today.getMonth() &&
141
145
  date.getFullYear() === today.getFullYear();
@@ -153,197 +157,762 @@ class BlocDatePickerComponent {
153
157
  const maxDay = new Date(max.getFullYear(), max.getMonth(), max.getDate());
154
158
  isDisabled = date > maxDay;
155
159
  }
156
- return { date, day: date.getDate(), isCurrentMonth, isToday, isSelected, isDisabled };
160
+ let isRangeStart = false;
161
+ let isRangeEnd = false;
162
+ let isInRange = false;
163
+ if (isRange) {
164
+ const dt = this._stripTime(date).getTime();
165
+ const s = rangeStart ? this._stripTime(rangeStart).getTime() : null;
166
+ const e = rangeEnd ? this._stripTime(rangeEnd).getTime() : null;
167
+ const h = hoverDate ? this._stripTime(hoverDate).getTime() : null;
168
+ if (s !== null && e !== null) {
169
+ // Complete range
170
+ isRangeStart = dt === s;
171
+ isRangeEnd = dt === e;
172
+ isInRange = dt > s && dt < e;
173
+ }
174
+ else if (s !== null && h !== null) {
175
+ // Preview range (one click done, hovering)
176
+ const lo = Math.min(s, h);
177
+ const hi = Math.max(s, h);
178
+ isRangeStart = dt === lo;
179
+ isRangeEnd = dt === hi;
180
+ isInRange = dt > lo && dt < hi;
181
+ }
182
+ else if (s !== null) {
183
+ isRangeStart = dt === s;
184
+ }
185
+ }
186
+ return { date, day: date.getDate(), isCurrentMonth, isToday, isSelected, isDisabled, isRangeStart, isRangeEnd, isInRange };
157
187
  }
158
- _formatDate(d) {
159
- const yyyy = d.getFullYear();
160
- const mm = String(d.getMonth() + 1).padStart(2, '0');
161
- const dd = String(d.getDate()).padStart(2, '0');
162
- return this.format()
163
- .replace('yyyy', String(yyyy))
164
- .replace('MM', mm)
165
- .replace('dd', dd);
188
+ _stripTime(d) {
189
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
166
190
  }
167
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
168
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: BlocDatePickerComponent, isStandalone: true, selector: "bloc-date-picker", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "_onDocClick($event)", "keydown.escape": "_onEsc()" }, properties: { "class.bloc-date-picker": "true", "class.bloc-date-picker--sm": "size() === \"sm\"", "class.bloc-date-picker--lg": "size() === \"lg\"", "class.bloc-date-picker--disabled": "isDisabled()", "class.bloc-date-picker--open": "isOpen()" } }, providers: [
169
- {
170
- provide: NG_VALUE_ACCESSOR,
171
- useExisting: forwardRef(() => BlocDatePickerComponent),
172
- multi: true,
173
- },
174
- ], ngImport: i0, template: `
175
- <div class="bloc-date-picker__input-wrapper" (click)="toggleCalendar()">
176
- <input
177
- class="bloc-date-picker__input"
178
- type="text"
179
- readonly
180
- [value]="displayValue()"
181
- [placeholder]="placeholder()"
182
- [disabled]="isDisabled()"
183
- [attr.aria-expanded]="isOpen()"
184
- aria-haspopup="dialog" />
185
- <span class="bloc-date-picker__icon" aria-hidden="true">
186
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
187
- <rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.3"/>
188
- <line x1="2" y1="6" x2="14" y2="6" stroke="currentColor" stroke-width="1.3"/>
189
- <line x1="5" y1="1.5" x2="5" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
190
- <line x1="11" y1="1.5" x2="11" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
191
- </svg>
192
- </span>
191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocCalendarPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
192
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: BlocCalendarPanelComponent, isStandalone: true, selector: "bloc-calendar-panel", inputs: { selectedDate: { classPropertyName: "selectedDate", publicName: "selectedDate", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, rangeStart: { classPropertyName: "rangeStart", publicName: "rangeStart", isSignal: true, isRequired: false, transformFunction: null }, rangeEnd: { classPropertyName: "rangeEnd", publicName: "rangeEnd", isSignal: true, isRequired: false, transformFunction: null }, hoverDate: { classPropertyName: "hoverDate", publicName: "hoverDate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dateSelect: "dateSelect", cleared: "cleared", dateHover: "dateHover" }, host: { classAttribute: "bloc-calendar-panel" }, ngImport: i0, template: `
193
+ <div class="bloc-date-picker__header">
194
+ @if (_view() === 'day') {
195
+ <button type="button" class="bloc-date-picker__nav" (click)="prevMonth()" aria-label="Previous month">&#8249;</button>
196
+ <button type="button" class="bloc-date-picker__month-year-btn" (click)="openMonthView()">{{ monthYearLabel() }}</button>
197
+ <button type="button" class="bloc-date-picker__nav" (click)="nextMonth()" aria-label="Next month">&#8250;</button>
198
+ }
199
+ @if (_view() === 'month') {
200
+ <button type="button" class="bloc-date-picker__nav" (click)="prevYear()" aria-label="Previous year">&#8249;</button>
201
+ <button type="button" class="bloc-date-picker__month-year-btn" (click)="openYearView()">{{ viewYear() }}</button>
202
+ <button type="button" class="bloc-date-picker__nav" (click)="nextYear()" aria-label="Next year">&#8250;</button>
203
+ }
204
+ @if (_view() === 'year') {
205
+ <button type="button" class="bloc-date-picker__nav" (click)="prevYearRange()" aria-label="Previous years">&#8249;</button>
206
+ <span class="bloc-date-picker__month-year-label">{{ yearRange()[0] }} – {{ yearRange()[11] }}</span>
207
+ <button type="button" class="bloc-date-picker__nav" (click)="nextYearRange()" aria-label="Next years">&#8250;</button>
208
+ }
193
209
  </div>
194
210
 
195
- @if (isOpen()) {
196
- <div class="bloc-date-picker__dropdown" role="dialog" aria-label="Date picker">
197
- <div class="bloc-date-picker__header">
198
- <button type="button" class="bloc-date-picker__nav" (click)="prevMonth()" aria-label="Previous month">
199
- &#8249;
200
- </button>
201
- <span class="bloc-date-picker__month-year">
202
- {{ monthYearLabel() }}
203
- </span>
204
- <button type="button" class="bloc-date-picker__nav" (click)="nextMonth()" aria-label="Next month">
205
- &#8250;
211
+ @if (_view() === 'day') {
212
+ <div class="bloc-date-picker__weekdays">
213
+ @for (day of weekdays; track day) {
214
+ <span class="bloc-date-picker__weekday">{{ day }}</span>
215
+ }
216
+ </div>
217
+
218
+ <div class="bloc-date-picker__days">
219
+ @for (day of calendarDays(); track day.date.getTime()) {
220
+ <button
221
+ type="button"
222
+ class="bloc-date-picker__day"
223
+ [class.bloc-date-picker__day--other]="!day.isCurrentMonth"
224
+ [class.bloc-date-picker__day--today]="day.isToday"
225
+ [class.bloc-date-picker__day--selected]="day.isSelected"
226
+ [class.bloc-date-picker__day--disabled]="day.isDisabled"
227
+ [class.bloc-date-picker__day--range-start]="day.isRangeStart"
228
+ [class.bloc-date-picker__day--range-end]="day.isRangeEnd"
229
+ [class.bloc-date-picker__day--in-range]="day.isInRange"
230
+ [disabled]="day.isDisabled"
231
+ (click)="selectDay(day)"
232
+ (mouseenter)="onDayHover(day)">
233
+ {{ day.day }}
206
234
  </button>
207
- </div>
235
+ }
236
+ </div>
237
+ }
208
238
 
209
- <div class="bloc-date-picker__weekdays">
210
- @for (day of weekdays; track day) {
211
- <span class="bloc-date-picker__weekday">{{ day }}</span>
212
- }
213
- </div>
239
+ @if (_view() === 'month') {
240
+ <div class="bloc-date-picker__month-grid">
241
+ @for (m of months; track $index) {
242
+ <button
243
+ type="button"
244
+ class="bloc-date-picker__month-cell"
245
+ [class.bloc-date-picker__month-cell--selected]="selectedDate() && viewYear() === selectedDate()!.getFullYear() && $index === selectedDate()!.getMonth()"
246
+ [class.bloc-date-picker__month-cell--current]="viewYear() === currentYear && $index === currentMonth"
247
+ (click)="selectMonth($index)">
248
+ {{ m }}
249
+ </button>
250
+ }
251
+ </div>
252
+ }
214
253
 
215
- <div class="bloc-date-picker__days">
216
- @for (day of calendarDays(); track day.date.getTime()) {
217
- <button
218
- type="button"
219
- class="bloc-date-picker__day"
220
- [class.bloc-date-picker__day--other]="!day.isCurrentMonth"
221
- [class.bloc-date-picker__day--today]="day.isToday"
222
- [class.bloc-date-picker__day--selected]="day.isSelected"
223
- [class.bloc-date-picker__day--disabled]="day.isDisabled"
224
- [disabled]="day.isDisabled"
225
- (click)="selectDay(day)">
226
- {{ day.day }}
227
- </button>
228
- }
229
- </div>
254
+ @if (_view() === 'year') {
255
+ <div class="bloc-date-picker__year-grid">
256
+ @for (y of yearRange(); track y) {
257
+ <button
258
+ type="button"
259
+ class="bloc-date-picker__year-cell"
260
+ [class.bloc-date-picker__year-cell--selected]="selectedDate() && y === selectedDate()!.getFullYear()"
261
+ [class.bloc-date-picker__year-cell--current]="y === currentYear"
262
+ (click)="selectYear(y)">
263
+ {{ y }}
264
+ </button>
265
+ }
266
+ </div>
267
+ }
230
268
 
231
- <div class="bloc-date-picker__footer">
232
- <button type="button" class="bloc-date-picker__today-btn" (click)="goToToday()">
233
- Today
269
+ @if (_view() === 'day') {
270
+ <div class="bloc-date-picker__footer">
271
+ <button type="button" class="bloc-date-picker__today-btn" (click)="goToToday()">
272
+ Today
273
+ </button>
274
+ @if (selectedDate() || rangeStart()) {
275
+ <button type="button" class="bloc-date-picker__clear-btn" (click)="clear()">
276
+ Clear
234
277
  </button>
235
- @if (_selectedDate()) {
236
- <button type="button" class="bloc-date-picker__clear-btn" (click)="clear()">
237
- Clear
238
- </button>
239
- }
240
- </div>
278
+ }
241
279
  </div>
242
280
  }
243
- `, isInline: true, styles: [":host{display:inline-block;position:relative;font-family:inherit}.bloc-date-picker__input-wrapper{display:flex;align-items:center;cursor:pointer;position:relative}.bloc-date-picker__input{appearance:none;border-width:1px;border-style:solid;font-family:inherit;cursor:pointer;outline:none;width:100%;box-sizing:border-box;padding:var(--bloc-date-picker-padding, 8px 36px 8px 12px);font-size:var(--bloc-date-picker-font-size, .875rem);border-radius:var(--bloc-date-picker-radius, 6px);transition:border-color .15s ease}:where(.bloc-date-picker__input){color:var(--bloc-date-picker-color, #374151);background-color:var(--bloc-date-picker-bg, #ffffff);border-color:var(--bloc-date-picker-border, var(--bloc-border, #d1d5db))}:where(.bloc-date-picker__input:focus){border-color:var(--bloc-date-picker-focus-border, var(--bloc-primary, #6b7280));box-shadow:0 0 0 2px var(--bloc-date-picker-focus-ring, rgba(107, 114, 128, .2))}:where(.bloc-date-picker__input::placeholder){color:var(--bloc-date-picker-placeholder, #9ca3af)}.bloc-date-picker__icon{position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none;display:flex;align-items:center}:where(.bloc-date-picker__icon){color:var(--bloc-date-picker-icon-color, #9ca3af)}:host.bloc-date-picker--disabled{opacity:.5;pointer-events:none}:host.bloc-date-picker--disabled .bloc-date-picker__input{cursor:not-allowed}.bloc-date-picker__dropdown{position:absolute;top:calc(100% + 4px);left:0;z-index:var(--bloc-date-picker-z-index, 100);min-width:280px;border-width:1px;border-style:solid;border-radius:var(--bloc-date-picker-dropdown-radius, 8px);padding:12px;animation:bloc-dp-in .15s ease}:where(.bloc-date-picker__dropdown){background-color:var(--bloc-date-picker-dropdown-bg, #ffffff);border-color:var(--bloc-date-picker-dropdown-border, var(--bloc-border, #d1d5db));box-shadow:var(--bloc-date-picker-dropdown-shadow, 0 4px 16px rgba(0, 0, 0, .1))}.bloc-date-picker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.bloc-date-picker__month-year{font-weight:600;font-size:.875rem}:where(.bloc-date-picker__month-year){color:var(--bloc-date-picker-header-color, #374151)}.bloc-date-picker__nav{appearance:none;background:transparent;border:none;cursor:pointer;font-size:1.25rem;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__nav){color:var(--bloc-date-picker-nav-color, #6b7280)}:where(.bloc-date-picker__nav):hover{background-color:var(--bloc-date-picker-nav-hover-bg, #f3f4f6)}.bloc-date-picker__weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:0;margin-bottom:4px}.bloc-date-picker__weekday{font-size:.75rem;font-weight:600;text-align:center;padding:4px 0;-webkit-user-select:none;user-select:none}:where(.bloc-date-picker__weekday){color:var(--bloc-date-picker-weekday-color, #9ca3af)}.bloc-date-picker__days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.bloc-date-picker__day{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;width:32px;height:32px;display:flex;align-items:center;justify-content:center;margin:0 auto;border-radius:var(--bloc-date-picker-day-radius, 6px);transition:background-color .1s ease,color .1s ease}:where(.bloc-date-picker__day){color:var(--bloc-date-picker-day-color, #374151)}:where(.bloc-date-picker__day):hover:not(:disabled){background-color:var(--bloc-date-picker-day-hover-bg, #f3f4f6)}.bloc-date-picker__day--other{opacity:.35}.bloc-date-picker__day--today{font-weight:700;border:1px solid var(--bloc-date-picker-today-border, var(--bloc-primary, #6b7280))}.bloc-date-picker__day--selected{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600}.bloc-date-picker__day--disabled{cursor:not-allowed;opacity:.3}.bloc-date-picker__footer{display:flex;justify-content:space-between;margin-top:8px;padding-top:8px;border-top:1px solid var(--bloc-date-picker-footer-border, var(--bloc-border, #d1d5db))}.bloc-date-picker__today-btn,.bloc-date-picker__clear-btn{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;font-weight:500;padding:4px 8px;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__today-btn){color:var(--bloc-date-picker-action-color, var(--bloc-primary, #6b7280))}:where(.bloc-date-picker__today-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6)}:where(.bloc-date-picker__clear-btn){color:var(--bloc-date-picker-clear-color, #9ca3af)}:where(.bloc-date-picker__clear-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6);color:var(--bloc-date-picker-clear-hover-color, #374151)}:host.bloc-date-picker--sm .bloc-date-picker__input{padding:var(--bloc-date-picker-padding, 6px 32px 6px 10px);font-size:var(--bloc-date-picker-font-size, .8125rem)}:host.bloc-date-picker--lg .bloc-date-picker__input{padding:var(--bloc-date-picker-padding, 12px 40px 12px 14px);font-size:var(--bloc-date-picker-font-size, 1rem)}@keyframes bloc-dp-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n"] });
281
+ `, isInline: true, styles: [":host{display:block;font-family:inherit}.bloc-date-picker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.bloc-date-picker__month-year-btn,.bloc-date-picker__month-year-label{font-weight:600;font-size:.875rem}.bloc-date-picker__month-year-btn{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;flex:1;text-align:center;border-radius:4px;padding:2px 6px;transition:background-color .1s ease}.bloc-date-picker__month-year-label{flex:1;text-align:center}:where(.bloc-date-picker__month-year-btn),:where(.bloc-date-picker__month-year-label){color:var(--bloc-date-picker-header-color, #374151)}:where(.bloc-date-picker__month-year-btn:hover){background-color:var(--bloc-date-picker-nav-hover-bg, #f3f4f6)}.bloc-date-picker__nav{appearance:none;background:transparent;border:none;cursor:pointer;font-size:1.25rem;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__nav){color:var(--bloc-date-picker-nav-color, #6b7280)}:where(.bloc-date-picker__nav):hover{background-color:var(--bloc-date-picker-nav-hover-bg, #f3f4f6)}.bloc-date-picker__weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:0;margin-bottom:4px}.bloc-date-picker__weekday{font-size:.75rem;font-weight:600;text-align:center;padding:4px 0;-webkit-user-select:none;user-select:none}:where(.bloc-date-picker__weekday){color:var(--bloc-date-picker-weekday-color, #9ca3af)}.bloc-date-picker__days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.bloc-date-picker__day{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;width:32px;height:32px;display:flex;align-items:center;justify-content:center;margin:0 auto;border-radius:var(--bloc-date-picker-day-radius, 6px);transition:background-color .1s ease,color .1s ease}.bloc-date-picker__day--other{opacity:.35}.bloc-date-picker__day--today{font-weight:700;border:1px solid var(--bloc-date-picker-today-border, var(--bloc-primary, #6b7280))}.bloc-date-picker__day--selected{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600}.bloc-date-picker__day--disabled{cursor:not-allowed;opacity:.3}.bloc-date-picker__day--in-range{background-color:var(--bloc-date-picker-range-bg, #f3f4f6);border-radius:0}.bloc-date-picker__day--range-start{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600;border-radius:var(--bloc-date-picker-day-radius, 6px) 0 0 var(--bloc-date-picker-day-radius, 6px)}.bloc-date-picker__day--range-end{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600;border-radius:0 var(--bloc-date-picker-day-radius, 6px) var(--bloc-date-picker-day-radius, 6px) 0}.bloc-date-picker__day--range-start.bloc-date-picker__day--range-end{border-radius:var(--bloc-date-picker-day-radius, 6px)}:where(.bloc-date-picker__day){color:var(--bloc-date-picker-day-color, #374151)}:where(.bloc-date-picker__day):hover:not(:disabled){background-color:var(--bloc-date-picker-day-hover-bg, #f3f4f6)}.bloc-date-picker__footer{display:flex;justify-content:space-between;margin-top:8px;padding-top:8px;border-top:1px solid var(--bloc-date-picker-footer-border, var(--bloc-border, #d1d5db))}.bloc-date-picker__today-btn,.bloc-date-picker__clear-btn{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;font-weight:500;padding:4px 8px;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__today-btn){color:var(--bloc-date-picker-action-color, var(--bloc-primary, #6b7280))}:where(.bloc-date-picker__today-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6)}:where(.bloc-date-picker__clear-btn){color:var(--bloc-date-picker-clear-color, #9ca3af)}:where(.bloc-date-picker__clear-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6);color:var(--bloc-date-picker-clear-hover-color, #374151)}.bloc-date-picker__month-grid,.bloc-date-picker__year-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;padding:4px 0}.bloc-date-picker__month-cell,.bloc-date-picker__year-cell{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;height:40px;display:flex;align-items:center;justify-content:center;border-radius:var(--bloc-date-picker-day-radius, 6px);transition:background-color .1s ease,color .1s ease}.bloc-date-picker__month-cell--current,.bloc-date-picker__year-cell--current{font-weight:700;border:1px solid var(--bloc-date-picker-today-border, var(--bloc-primary, #6b7280))}.bloc-date-picker__month-cell--selected,.bloc-date-picker__year-cell--selected{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600}:where(.bloc-date-picker__month-cell),:where(.bloc-date-picker__year-cell){color:var(--bloc-date-picker-day-color, #374151)}:where(.bloc-date-picker__month-cell):hover:not(:disabled),:where(.bloc-date-picker__year-cell):hover:not(:disabled){background-color:var(--bloc-date-picker-day-hover-bg, #f3f4f6)}\n"] });
244
282
  }
245
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerComponent, decorators: [{
283
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocCalendarPanelComponent, decorators: [{
246
284
  type: Component,
247
- args: [{ selector: 'bloc-date-picker', standalone: true, template: `
248
- <div class="bloc-date-picker__input-wrapper" (click)="toggleCalendar()">
249
- <input
250
- class="bloc-date-picker__input"
251
- type="text"
252
- readonly
253
- [value]="displayValue()"
254
- [placeholder]="placeholder()"
255
- [disabled]="isDisabled()"
256
- [attr.aria-expanded]="isOpen()"
257
- aria-haspopup="dialog" />
258
- <span class="bloc-date-picker__icon" aria-hidden="true">
259
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
260
- <rect x="2" y="3" width="12" height="11" rx="1.5" stroke="currentColor" stroke-width="1.3"/>
261
- <line x1="2" y1="6" x2="14" y2="6" stroke="currentColor" stroke-width="1.3"/>
262
- <line x1="5" y1="1.5" x2="5" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
263
- <line x1="11" y1="1.5" x2="11" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
264
- </svg>
265
- </span>
285
+ args: [{ selector: 'bloc-calendar-panel', standalone: true, template: `
286
+ <div class="bloc-date-picker__header">
287
+ @if (_view() === 'day') {
288
+ <button type="button" class="bloc-date-picker__nav" (click)="prevMonth()" aria-label="Previous month">&#8249;</button>
289
+ <button type="button" class="bloc-date-picker__month-year-btn" (click)="openMonthView()">{{ monthYearLabel() }}</button>
290
+ <button type="button" class="bloc-date-picker__nav" (click)="nextMonth()" aria-label="Next month">&#8250;</button>
291
+ }
292
+ @if (_view() === 'month') {
293
+ <button type="button" class="bloc-date-picker__nav" (click)="prevYear()" aria-label="Previous year">&#8249;</button>
294
+ <button type="button" class="bloc-date-picker__month-year-btn" (click)="openYearView()">{{ viewYear() }}</button>
295
+ <button type="button" class="bloc-date-picker__nav" (click)="nextYear()" aria-label="Next year">&#8250;</button>
296
+ }
297
+ @if (_view() === 'year') {
298
+ <button type="button" class="bloc-date-picker__nav" (click)="prevYearRange()" aria-label="Previous years">&#8249;</button>
299
+ <span class="bloc-date-picker__month-year-label">{{ yearRange()[0] }} {{ yearRange()[11] }}</span>
300
+ <button type="button" class="bloc-date-picker__nav" (click)="nextYearRange()" aria-label="Next years">&#8250;</button>
301
+ }
266
302
  </div>
267
303
 
268
- @if (isOpen()) {
269
- <div class="bloc-date-picker__dropdown" role="dialog" aria-label="Date picker">
270
- <div class="bloc-date-picker__header">
271
- <button type="button" class="bloc-date-picker__nav" (click)="prevMonth()" aria-label="Previous month">
272
- &#8249;
273
- </button>
274
- <span class="bloc-date-picker__month-year">
275
- {{ monthYearLabel() }}
276
- </span>
277
- <button type="button" class="bloc-date-picker__nav" (click)="nextMonth()" aria-label="Next month">
278
- &#8250;
304
+ @if (_view() === 'day') {
305
+ <div class="bloc-date-picker__weekdays">
306
+ @for (day of weekdays; track day) {
307
+ <span class="bloc-date-picker__weekday">{{ day }}</span>
308
+ }
309
+ </div>
310
+
311
+ <div class="bloc-date-picker__days">
312
+ @for (day of calendarDays(); track day.date.getTime()) {
313
+ <button
314
+ type="button"
315
+ class="bloc-date-picker__day"
316
+ [class.bloc-date-picker__day--other]="!day.isCurrentMonth"
317
+ [class.bloc-date-picker__day--today]="day.isToday"
318
+ [class.bloc-date-picker__day--selected]="day.isSelected"
319
+ [class.bloc-date-picker__day--disabled]="day.isDisabled"
320
+ [class.bloc-date-picker__day--range-start]="day.isRangeStart"
321
+ [class.bloc-date-picker__day--range-end]="day.isRangeEnd"
322
+ [class.bloc-date-picker__day--in-range]="day.isInRange"
323
+ [disabled]="day.isDisabled"
324
+ (click)="selectDay(day)"
325
+ (mouseenter)="onDayHover(day)">
326
+ {{ day.day }}
279
327
  </button>
280
- </div>
328
+ }
329
+ </div>
330
+ }
281
331
 
282
- <div class="bloc-date-picker__weekdays">
283
- @for (day of weekdays; track day) {
284
- <span class="bloc-date-picker__weekday">{{ day }}</span>
285
- }
286
- </div>
332
+ @if (_view() === 'month') {
333
+ <div class="bloc-date-picker__month-grid">
334
+ @for (m of months; track $index) {
335
+ <button
336
+ type="button"
337
+ class="bloc-date-picker__month-cell"
338
+ [class.bloc-date-picker__month-cell--selected]="selectedDate() && viewYear() === selectedDate()!.getFullYear() && $index === selectedDate()!.getMonth()"
339
+ [class.bloc-date-picker__month-cell--current]="viewYear() === currentYear && $index === currentMonth"
340
+ (click)="selectMonth($index)">
341
+ {{ m }}
342
+ </button>
343
+ }
344
+ </div>
345
+ }
287
346
 
288
- <div class="bloc-date-picker__days">
289
- @for (day of calendarDays(); track day.date.getTime()) {
290
- <button
291
- type="button"
292
- class="bloc-date-picker__day"
293
- [class.bloc-date-picker__day--other]="!day.isCurrentMonth"
294
- [class.bloc-date-picker__day--today]="day.isToday"
295
- [class.bloc-date-picker__day--selected]="day.isSelected"
296
- [class.bloc-date-picker__day--disabled]="day.isDisabled"
297
- [disabled]="day.isDisabled"
298
- (click)="selectDay(day)">
299
- {{ day.day }}
300
- </button>
301
- }
302
- </div>
347
+ @if (_view() === 'year') {
348
+ <div class="bloc-date-picker__year-grid">
349
+ @for (y of yearRange(); track y) {
350
+ <button
351
+ type="button"
352
+ class="bloc-date-picker__year-cell"
353
+ [class.bloc-date-picker__year-cell--selected]="selectedDate() && y === selectedDate()!.getFullYear()"
354
+ [class.bloc-date-picker__year-cell--current]="y === currentYear"
355
+ (click)="selectYear(y)">
356
+ {{ y }}
357
+ </button>
358
+ }
359
+ </div>
360
+ }
303
361
 
304
- <div class="bloc-date-picker__footer">
305
- <button type="button" class="bloc-date-picker__today-btn" (click)="goToToday()">
306
- Today
362
+ @if (_view() === 'day') {
363
+ <div class="bloc-date-picker__footer">
364
+ <button type="button" class="bloc-date-picker__today-btn" (click)="goToToday()">
365
+ Today
366
+ </button>
367
+ @if (selectedDate() || rangeStart()) {
368
+ <button type="button" class="bloc-date-picker__clear-btn" (click)="clear()">
369
+ Clear
307
370
  </button>
308
- @if (_selectedDate()) {
309
- <button type="button" class="bloc-date-picker__clear-btn" (click)="clear()">
310
- Clear
311
- </button>
312
- }
313
- </div>
371
+ }
314
372
  </div>
315
373
  }
316
- `, providers: [
374
+ `, host: { class: 'bloc-calendar-panel' }, styles: [":host{display:block;font-family:inherit}.bloc-date-picker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.bloc-date-picker__month-year-btn,.bloc-date-picker__month-year-label{font-weight:600;font-size:.875rem}.bloc-date-picker__month-year-btn{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;flex:1;text-align:center;border-radius:4px;padding:2px 6px;transition:background-color .1s ease}.bloc-date-picker__month-year-label{flex:1;text-align:center}:where(.bloc-date-picker__month-year-btn),:where(.bloc-date-picker__month-year-label){color:var(--bloc-date-picker-header-color, #374151)}:where(.bloc-date-picker__month-year-btn:hover){background-color:var(--bloc-date-picker-nav-hover-bg, #f3f4f6)}.bloc-date-picker__nav{appearance:none;background:transparent;border:none;cursor:pointer;font-size:1.25rem;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__nav){color:var(--bloc-date-picker-nav-color, #6b7280)}:where(.bloc-date-picker__nav):hover{background-color:var(--bloc-date-picker-nav-hover-bg, #f3f4f6)}.bloc-date-picker__weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:0;margin-bottom:4px}.bloc-date-picker__weekday{font-size:.75rem;font-weight:600;text-align:center;padding:4px 0;-webkit-user-select:none;user-select:none}:where(.bloc-date-picker__weekday){color:var(--bloc-date-picker-weekday-color, #9ca3af)}.bloc-date-picker__days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.bloc-date-picker__day{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;width:32px;height:32px;display:flex;align-items:center;justify-content:center;margin:0 auto;border-radius:var(--bloc-date-picker-day-radius, 6px);transition:background-color .1s ease,color .1s ease}.bloc-date-picker__day--other{opacity:.35}.bloc-date-picker__day--today{font-weight:700;border:1px solid var(--bloc-date-picker-today-border, var(--bloc-primary, #6b7280))}.bloc-date-picker__day--selected{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600}.bloc-date-picker__day--disabled{cursor:not-allowed;opacity:.3}.bloc-date-picker__day--in-range{background-color:var(--bloc-date-picker-range-bg, #f3f4f6);border-radius:0}.bloc-date-picker__day--range-start{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600;border-radius:var(--bloc-date-picker-day-radius, 6px) 0 0 var(--bloc-date-picker-day-radius, 6px)}.bloc-date-picker__day--range-end{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600;border-radius:0 var(--bloc-date-picker-day-radius, 6px) var(--bloc-date-picker-day-radius, 6px) 0}.bloc-date-picker__day--range-start.bloc-date-picker__day--range-end{border-radius:var(--bloc-date-picker-day-radius, 6px)}:where(.bloc-date-picker__day){color:var(--bloc-date-picker-day-color, #374151)}:where(.bloc-date-picker__day):hover:not(:disabled){background-color:var(--bloc-date-picker-day-hover-bg, #f3f4f6)}.bloc-date-picker__footer{display:flex;justify-content:space-between;margin-top:8px;padding-top:8px;border-top:1px solid var(--bloc-date-picker-footer-border, var(--bloc-border, #d1d5db))}.bloc-date-picker__today-btn,.bloc-date-picker__clear-btn{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;font-weight:500;padding:4px 8px;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__today-btn){color:var(--bloc-date-picker-action-color, var(--bloc-primary, #6b7280))}:where(.bloc-date-picker__today-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6)}:where(.bloc-date-picker__clear-btn){color:var(--bloc-date-picker-clear-color, #9ca3af)}:where(.bloc-date-picker__clear-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6);color:var(--bloc-date-picker-clear-hover-color, #374151)}.bloc-date-picker__month-grid,.bloc-date-picker__year-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;padding:4px 0}.bloc-date-picker__month-cell,.bloc-date-picker__year-cell{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;height:40px;display:flex;align-items:center;justify-content:center;border-radius:var(--bloc-date-picker-day-radius, 6px);transition:background-color .1s ease,color .1s ease}.bloc-date-picker__month-cell--current,.bloc-date-picker__year-cell--current{font-weight:700;border:1px solid var(--bloc-date-picker-today-border, var(--bloc-primary, #6b7280))}.bloc-date-picker__month-cell--selected,.bloc-date-picker__year-cell--selected{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600}:where(.bloc-date-picker__month-cell),:where(.bloc-date-picker__year-cell){color:var(--bloc-date-picker-day-color, #374151)}:where(.bloc-date-picker__month-cell):hover:not(:disabled),:where(.bloc-date-picker__year-cell):hover:not(:disabled){background-color:var(--bloc-date-picker-day-hover-bg, #f3f4f6)}\n"] }]
375
+ }], propDecorators: { selectedDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedDate", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], rangeStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeStart", required: false }] }], rangeEnd: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeEnd", required: false }] }], hoverDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverDate", required: false }] }], dateSelect: [{ type: i0.Output, args: ["dateSelect"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], dateHover: [{ type: i0.Output, args: ["dateHover"] }] } });
376
+
377
+ // ── Injected styles ────────────────────────────────────────────────────────
378
+ const TRIGGER_CSS$1 = [
379
+ '.bloc-dp-trigger-dropdown{',
380
+ 'position:fixed;z-index:var(--bloc-date-picker-z-index,100);',
381
+ 'min-width:280px;border-width:1px;border-style:solid;',
382
+ 'border-color:var(--bloc-date-picker-dropdown-border,var(--bloc-border,#d1d5db));',
383
+ 'border-radius:var(--bloc-date-picker-dropdown-radius,8px);',
384
+ 'padding:12px;box-sizing:border-box;',
385
+ 'font-family:inherit;',
386
+ 'animation:bloc-dp-trigger-in .15s ease}',
387
+ ':where(.bloc-dp-trigger-dropdown){',
388
+ 'background-color:var(--bloc-date-picker-dropdown-bg,#ffffff);',
389
+ 'box-shadow:var(--bloc-date-picker-dropdown-shadow,0 4px 16px rgba(0,0,0,.1))}',
390
+ // Arrow — outer
391
+ '.bloc-dp-trigger-dropdown::before,',
392
+ '.bloc-dp-trigger-dropdown::after{content:"";position:absolute;width:0;height:0}',
393
+ '.bloc-dp-trigger-dropdown::before{',
394
+ 'top:-8px;left:16px;border:8px solid transparent;border-top:none;',
395
+ 'border-bottom-color:var(--bloc-date-picker-dropdown-border,var(--bloc-border,#d1d5db))}',
396
+ // Arrow — inner
397
+ '.bloc-dp-trigger-dropdown::after{',
398
+ 'top:-7px;left:17px;border:7px solid transparent;border-top:none;',
399
+ 'border-bottom-color:var(--bloc-date-picker-dropdown-bg,#ffffff)}',
400
+ // Upward variant
401
+ '.bloc-dp-trigger-dropdown--upward{animation-name:bloc-dp-trigger-in-up}',
402
+ '.bloc-dp-trigger-dropdown--upward::before{',
403
+ 'top:auto;bottom:-8px;left:16px;border:8px solid transparent;border-bottom:none;',
404
+ 'border-top-color:var(--bloc-date-picker-dropdown-border,var(--bloc-border,#d1d5db))}',
405
+ '.bloc-dp-trigger-dropdown--upward::after{',
406
+ 'top:auto;bottom:-7px;left:17px;border:7px solid transparent;border-bottom:none;',
407
+ 'border-top-color:var(--bloc-date-picker-dropdown-bg,#ffffff)}',
408
+ '@keyframes bloc-dp-trigger-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}',
409
+ '@keyframes bloc-dp-trigger-in-up{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}',
410
+ // Disabled state
411
+ '.bloc-date-picker-trigger--disabled{opacity:0.5;cursor:not-allowed;pointer-events:none}',
412
+ ].join('');
413
+ function ensureStyles$1(doc) {
414
+ if (!doc?.head || doc.getElementById('bloc-dp-trigger-styles'))
415
+ return;
416
+ const style = doc.createElement('style');
417
+ style.id = 'bloc-dp-trigger-styles';
418
+ style.textContent = TRIGGER_CSS$1;
419
+ doc.head.appendChild(style);
420
+ }
421
+ // ── Directive ──────────────────────────────────────────────────────────────
422
+ class BlocDatePickerTriggerDirective {
423
+ _el = inject((ElementRef));
424
+ _vcr = inject(ViewContainerRef);
425
+ _renderer = inject(Renderer2);
426
+ _doc = inject(DOCUMENT);
427
+ _destroyRef = inject(DestroyRef);
428
+ /** Minimum selectable date. */
429
+ minDate = input(null, ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
430
+ /** Maximum selectable date. */
431
+ maxDate = input(null, ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
432
+ /** Date format string for `displayValue`. Defaults to `'yyyy-MM-dd'`. */
433
+ format = input('yyyy-MM-dd', ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
434
+ /** Disable the trigger via template binding. */
435
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
436
+ /** Currently selected date. */
437
+ _selectedDate = signal(null, ...(ngDevMode ? [{ debugName: "_selectedDate" }] : /* istanbul ignore next */ []));
438
+ /** Whether the calendar panel is open. */
439
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
440
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : /* istanbul ignore next */ []));
441
+ isDisabled = computed(() => this.disabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
442
+ /** Formatted display value consumers can bind in their trigger template. */
443
+ displayValue = computed(() => {
444
+ const d = this._selectedDate();
445
+ if (!d)
446
+ return '';
447
+ return this._formatDate(d);
448
+ }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
449
+ _onChange = () => { };
450
+ _onTouched = () => { };
451
+ _panelRef = null;
452
+ _wrapper = null;
453
+ _docClickUnlisten = null;
454
+ _escUnlisten = null;
455
+ constructor() {
456
+ ensureStyles$1(this._doc);
457
+ }
458
+ ngOnInit() {
459
+ this._destroyRef.onDestroy(() => this._close());
460
+ }
461
+ toggle() {
462
+ if (this.isDisabled())
463
+ return;
464
+ this.isOpen() ? this._close() : this._open();
465
+ }
466
+ // ── ControlValueAccessor ────────────────────────────────────────────────
467
+ writeValue(val) {
468
+ this._selectedDate.set(val instanceof Date ? val : val ? new Date(val) : null);
469
+ }
470
+ registerOnChange(fn) {
471
+ this._onChange = fn;
472
+ }
473
+ registerOnTouched(fn) {
474
+ this._onTouched = fn;
475
+ }
476
+ setDisabledState(isDisabled) {
477
+ this._formDisabled.set(isDisabled);
478
+ }
479
+ // ── Private ─────────────────────────────────────────────────────────────
480
+ _open() {
481
+ if (this._panelRef)
482
+ return;
483
+ // Create panel via ViewContainerRef (keeps it in Angular's change-detection tree)
484
+ this._panelRef = this._vcr.createComponent(BlocCalendarPanelComponent);
485
+ const panel = this._panelRef.instance;
486
+ // Set inputs
487
+ this._panelRef.setInput('selectedDate', this._selectedDate());
488
+ this._panelRef.setInput('minDate', this.minDate());
489
+ this._panelRef.setInput('maxDate', this.maxDate());
490
+ // Listen to outputs
491
+ panel.dateSelect.subscribe((date) => {
492
+ this._selectedDate.set(date);
493
+ this._onChange(date);
494
+ this._close();
495
+ });
496
+ panel.cleared.subscribe(() => {
497
+ this._selectedDate.set(null);
498
+ this._onChange(null);
499
+ });
500
+ panel.resetView(this._selectedDate());
501
+ // Create wrapper div and append component's host element into it
502
+ this._wrapper = this._doc.createElement('div');
503
+ this._wrapper.className = 'bloc-dp-trigger-dropdown';
504
+ this._wrapper.setAttribute('role', 'dialog');
505
+ this._wrapper.setAttribute('aria-label', 'Date picker');
506
+ this._wrapper.appendChild(this._panelRef.location.nativeElement);
507
+ this._doc.body.appendChild(this._wrapper);
508
+ // Position
509
+ this._positionDropdown();
510
+ this.isOpen.set(true);
511
+ this._onTouched();
512
+ // Close on outside click (delay so current click doesn't immediately close)
513
+ requestAnimationFrame(() => {
514
+ this._docClickUnlisten = this._renderer.listen('document', 'click', (e) => {
515
+ const target = e.target;
516
+ if (!this._el.nativeElement.contains(target) &&
517
+ !this._wrapper?.contains(target)) {
518
+ this._close();
519
+ }
520
+ });
521
+ this._escUnlisten = this._renderer.listen('document', 'keydown.escape', () => {
522
+ this._close();
523
+ });
524
+ });
525
+ }
526
+ _close() {
527
+ this._docClickUnlisten?.();
528
+ this._docClickUnlisten = null;
529
+ this._escUnlisten?.();
530
+ this._escUnlisten = null;
531
+ if (this._panelRef) {
532
+ this._panelRef.destroy();
533
+ this._panelRef = null;
534
+ }
535
+ if (this._wrapper) {
536
+ this._wrapper.remove();
537
+ this._wrapper = null;
538
+ }
539
+ this.isOpen.set(false);
540
+ }
541
+ _positionDropdown() {
542
+ if (!this._wrapper)
543
+ return;
544
+ const rect = this._el.nativeElement.getBoundingClientRect();
545
+ const openUpward = window.innerHeight - rect.bottom < 320;
546
+ if (openUpward) {
547
+ this._wrapper.classList.add('bloc-dp-trigger-dropdown--upward');
548
+ this._wrapper.style.left = `${rect.left}px`;
549
+ this._wrapper.style.bottom = `${window.innerHeight - rect.top + 10}px`;
550
+ this._wrapper.style.top = 'auto';
551
+ }
552
+ else {
553
+ this._wrapper.classList.remove('bloc-dp-trigger-dropdown--upward');
554
+ this._wrapper.style.left = `${rect.left}px`;
555
+ this._wrapper.style.top = `${rect.bottom + 10}px`;
556
+ this._wrapper.style.bottom = 'auto';
557
+ }
558
+ }
559
+ _formatDate(d) {
560
+ const yyyy = d.getFullYear();
561
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
562
+ const dd = String(d.getDate()).padStart(2, '0');
563
+ return this.format()
564
+ .replace('yyyy', String(yyyy))
565
+ .replace('MM', mm)
566
+ .replace('dd', dd);
567
+ }
568
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
569
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.5", type: BlocDatePickerTriggerDirective, isStandalone: true, selector: "[blocDatePickerTrigger]", inputs: { minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "toggle()" }, properties: { "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "isOpen()", "class.bloc-date-picker-trigger--open": "isOpen()", "class.bloc-date-picker-trigger--disabled": "isDisabled()", "attr.disabled": "isDisabled() || null" } }, providers: [
570
+ {
571
+ provide: NG_VALUE_ACCESSOR,
572
+ useExisting: forwardRef(() => BlocDatePickerTriggerDirective),
573
+ multi: true,
574
+ },
575
+ ], exportAs: ["blocDatePickerTrigger"], ngImport: i0 });
576
+ }
577
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerTriggerDirective, decorators: [{
578
+ type: Directive,
579
+ args: [{
580
+ selector: '[blocDatePickerTrigger]',
581
+ standalone: true,
582
+ exportAs: 'blocDatePickerTrigger',
583
+ providers: [
317
584
  {
318
585
  provide: NG_VALUE_ACCESSOR,
319
- useExisting: forwardRef(() => BlocDatePickerComponent),
586
+ useExisting: forwardRef(() => BlocDatePickerTriggerDirective),
320
587
  multi: true,
321
588
  },
322
- ], host: {
323
- '[class.bloc-date-picker]': 'true',
324
- '[class.bloc-date-picker--sm]': 'size() === "sm"',
325
- '[class.bloc-date-picker--lg]': 'size() === "lg"',
326
- '[class.bloc-date-picker--disabled]': 'isDisabled()',
327
- '[class.bloc-date-picker--open]': 'isOpen()',
328
- }, styles: [":host{display:inline-block;position:relative;font-family:inherit}.bloc-date-picker__input-wrapper{display:flex;align-items:center;cursor:pointer;position:relative}.bloc-date-picker__input{appearance:none;border-width:1px;border-style:solid;font-family:inherit;cursor:pointer;outline:none;width:100%;box-sizing:border-box;padding:var(--bloc-date-picker-padding, 8px 36px 8px 12px);font-size:var(--bloc-date-picker-font-size, .875rem);border-radius:var(--bloc-date-picker-radius, 6px);transition:border-color .15s ease}:where(.bloc-date-picker__input){color:var(--bloc-date-picker-color, #374151);background-color:var(--bloc-date-picker-bg, #ffffff);border-color:var(--bloc-date-picker-border, var(--bloc-border, #d1d5db))}:where(.bloc-date-picker__input:focus){border-color:var(--bloc-date-picker-focus-border, var(--bloc-primary, #6b7280));box-shadow:0 0 0 2px var(--bloc-date-picker-focus-ring, rgba(107, 114, 128, .2))}:where(.bloc-date-picker__input::placeholder){color:var(--bloc-date-picker-placeholder, #9ca3af)}.bloc-date-picker__icon{position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none;display:flex;align-items:center}:where(.bloc-date-picker__icon){color:var(--bloc-date-picker-icon-color, #9ca3af)}:host.bloc-date-picker--disabled{opacity:.5;pointer-events:none}:host.bloc-date-picker--disabled .bloc-date-picker__input{cursor:not-allowed}.bloc-date-picker__dropdown{position:absolute;top:calc(100% + 4px);left:0;z-index:var(--bloc-date-picker-z-index, 100);min-width:280px;border-width:1px;border-style:solid;border-radius:var(--bloc-date-picker-dropdown-radius, 8px);padding:12px;animation:bloc-dp-in .15s ease}:where(.bloc-date-picker__dropdown){background-color:var(--bloc-date-picker-dropdown-bg, #ffffff);border-color:var(--bloc-date-picker-dropdown-border, var(--bloc-border, #d1d5db));box-shadow:var(--bloc-date-picker-dropdown-shadow, 0 4px 16px rgba(0, 0, 0, .1))}.bloc-date-picker__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.bloc-date-picker__month-year{font-weight:600;font-size:.875rem}:where(.bloc-date-picker__month-year){color:var(--bloc-date-picker-header-color, #374151)}.bloc-date-picker__nav{appearance:none;background:transparent;border:none;cursor:pointer;font-size:1.25rem;line-height:1;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__nav){color:var(--bloc-date-picker-nav-color, #6b7280)}:where(.bloc-date-picker__nav):hover{background-color:var(--bloc-date-picker-nav-hover-bg, #f3f4f6)}.bloc-date-picker__weekdays{display:grid;grid-template-columns:repeat(7,1fr);gap:0;margin-bottom:4px}.bloc-date-picker__weekday{font-size:.75rem;font-weight:600;text-align:center;padding:4px 0;-webkit-user-select:none;user-select:none}:where(.bloc-date-picker__weekday){color:var(--bloc-date-picker-weekday-color, #9ca3af)}.bloc-date-picker__days{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}.bloc-date-picker__day{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;width:32px;height:32px;display:flex;align-items:center;justify-content:center;margin:0 auto;border-radius:var(--bloc-date-picker-day-radius, 6px);transition:background-color .1s ease,color .1s ease}:where(.bloc-date-picker__day){color:var(--bloc-date-picker-day-color, #374151)}:where(.bloc-date-picker__day):hover:not(:disabled){background-color:var(--bloc-date-picker-day-hover-bg, #f3f4f6)}.bloc-date-picker__day--other{opacity:.35}.bloc-date-picker__day--today{font-weight:700;border:1px solid var(--bloc-date-picker-today-border, var(--bloc-primary, #6b7280))}.bloc-date-picker__day--selected{background-color:var(--bloc-date-picker-selected-bg, var(--bloc-primary, #6b7280))!important;color:var(--bloc-date-picker-selected-color, #ffffff)!important;font-weight:600}.bloc-date-picker__day--disabled{cursor:not-allowed;opacity:.3}.bloc-date-picker__footer{display:flex;justify-content:space-between;margin-top:8px;padding-top:8px;border-top:1px solid var(--bloc-date-picker-footer-border, var(--bloc-border, #d1d5db))}.bloc-date-picker__today-btn,.bloc-date-picker__clear-btn{appearance:none;background:transparent;border:none;cursor:pointer;font-family:inherit;font-size:.8125rem;font-weight:500;padding:4px 8px;border-radius:4px;transition:background-color .1s ease}:where(.bloc-date-picker__today-btn){color:var(--bloc-date-picker-action-color, var(--bloc-primary, #6b7280))}:where(.bloc-date-picker__today-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6)}:where(.bloc-date-picker__clear-btn){color:var(--bloc-date-picker-clear-color, #9ca3af)}:where(.bloc-date-picker__clear-btn):hover{background-color:var(--bloc-date-picker-action-hover-bg, #f3f4f6);color:var(--bloc-date-picker-clear-hover-color, #374151)}:host.bloc-date-picker--sm .bloc-date-picker__input{padding:var(--bloc-date-picker-padding, 6px 32px 6px 10px);font-size:var(--bloc-date-picker-font-size, .8125rem)}:host.bloc-date-picker--lg .bloc-date-picker__input{padding:var(--bloc-date-picker-padding, 12px 40px 12px 14px);font-size:var(--bloc-date-picker-font-size, 1rem)}@keyframes bloc-dp-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n"] }]
329
- }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], _onDocClick: [{
330
- type: HostListener,
331
- args: ['document:click', ['$event']]
332
- }], _onEsc: [{
333
- type: HostListener,
334
- args: ['keydown.escape']
335
- }] } });
589
+ ],
590
+ host: {
591
+ '(click)': 'toggle()',
592
+ '[attr.aria-haspopup]': '"dialog"',
593
+ '[attr.aria-expanded]': 'isOpen()',
594
+ '[class.bloc-date-picker-trigger--open]': 'isOpen()',
595
+ '[class.bloc-date-picker-trigger--disabled]': 'isDisabled()',
596
+ '[attr.disabled]': 'isDisabled() || null',
597
+ },
598
+ }]
599
+ }], ctorParameters: () => [], propDecorators: { minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
600
+
601
+ // ── Injected styles (reuses the same dropdown CSS id as the single trigger) ──
602
+ const TRIGGER_CSS = [
603
+ '.bloc-dp-trigger-dropdown{',
604
+ 'position:fixed;z-index:var(--bloc-date-picker-z-index,100);',
605
+ 'min-width:280px;border-width:1px;border-style:solid;',
606
+ 'border-color:var(--bloc-date-picker-dropdown-border,var(--bloc-border,#d1d5db));',
607
+ 'border-radius:var(--bloc-date-picker-dropdown-radius,8px);',
608
+ 'padding:12px;box-sizing:border-box;',
609
+ 'font-family:inherit;',
610
+ 'animation:bloc-dp-trigger-in .15s ease}',
611
+ ':where(.bloc-dp-trigger-dropdown){',
612
+ 'background-color:var(--bloc-date-picker-dropdown-bg,#ffffff);',
613
+ 'box-shadow:var(--bloc-date-picker-dropdown-shadow,0 4px 16px rgba(0,0,0,.1))}',
614
+ '.bloc-dp-trigger-dropdown::before,',
615
+ '.bloc-dp-trigger-dropdown::after{content:"";position:absolute;width:0;height:0}',
616
+ '.bloc-dp-trigger-dropdown::before{',
617
+ 'top:-8px;left:16px;border:8px solid transparent;border-top:none;',
618
+ 'border-bottom-color:var(--bloc-date-picker-dropdown-border,var(--bloc-border,#d1d5db))}',
619
+ '.bloc-dp-trigger-dropdown::after{',
620
+ 'top:-7px;left:17px;border:7px solid transparent;border-top:none;',
621
+ 'border-bottom-color:var(--bloc-date-picker-dropdown-bg,#ffffff)}',
622
+ '.bloc-dp-trigger-dropdown--upward{animation-name:bloc-dp-trigger-in-up}',
623
+ '.bloc-dp-trigger-dropdown--upward::before{',
624
+ 'top:auto;bottom:-8px;left:16px;border:8px solid transparent;border-bottom:none;',
625
+ 'border-top-color:var(--bloc-date-picker-dropdown-border,var(--bloc-border,#d1d5db))}',
626
+ '.bloc-dp-trigger-dropdown--upward::after{',
627
+ 'top:auto;bottom:-7px;left:17px;border:7px solid transparent;border-bottom:none;',
628
+ 'border-top-color:var(--bloc-date-picker-dropdown-bg,#ffffff)}',
629
+ '@keyframes bloc-dp-trigger-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}',
630
+ '@keyframes bloc-dp-trigger-in-up{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}',
631
+ '.bloc-date-picker-trigger--disabled{opacity:0.5;cursor:not-allowed;pointer-events:none}',
632
+ ].join('');
633
+ function ensureStyles(doc) {
634
+ if (!doc?.head || doc.getElementById('bloc-dp-trigger-styles'))
635
+ return;
636
+ const style = doc.createElement('style');
637
+ style.id = 'bloc-dp-trigger-styles';
638
+ style.textContent = TRIGGER_CSS;
639
+ doc.head.appendChild(style);
640
+ }
641
+ // ── Directive ──────────────────────────────────────────────────────────────
642
+ class BlocDateRangePickerTriggerDirective {
643
+ _el = inject((ElementRef));
644
+ _vcr = inject(ViewContainerRef);
645
+ _renderer = inject(Renderer2);
646
+ _doc = inject(DOCUMENT);
647
+ _destroyRef = inject(DestroyRef);
648
+ /** Minimum selectable date. */
649
+ minDate = input(null, ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
650
+ /** Maximum selectable date. */
651
+ maxDate = input(null, ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
652
+ /** Date format string. Defaults to `'yyyy-MM-dd'`. */
653
+ format = input('yyyy-MM-dd', ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
654
+ /** Disable the trigger via template binding. */
655
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
656
+ /** Optional FormGroup with `from` and `to` controls. When provided, the directive patches these controls directly. */
657
+ rangeFormGroup = input(null, ...(ngDevMode ? [{ debugName: "rangeFormGroup" }] : /* istanbul ignore next */ []));
658
+ // ── Internal state ──────────────────────────────────────────────────────
659
+ _rangeStart = signal(null, ...(ngDevMode ? [{ debugName: "_rangeStart" }] : /* istanbul ignore next */ []));
660
+ _rangeEnd = signal(null, ...(ngDevMode ? [{ debugName: "_rangeEnd" }] : /* istanbul ignore next */ []));
661
+ _hoverDate = signal(null, ...(ngDevMode ? [{ debugName: "_hoverDate" }] : /* istanbul ignore next */ []));
662
+ _pickState = signal('idle', ...(ngDevMode ? [{ debugName: "_pickState" }] : /* istanbul ignore next */ []));
663
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
664
+ _formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_formDisabled" }] : /* istanbul ignore next */ []));
665
+ isDisabled = computed(() => this.disabled() || this._formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
666
+ /** Formatted display value: "from → to" or partial. */
667
+ displayValue = computed(() => {
668
+ const from = this._rangeStart();
669
+ const to = this._rangeEnd();
670
+ if (from && to)
671
+ return `${this._formatDate(from)} → ${this._formatDate(to)}`;
672
+ if (from)
673
+ return this._formatDate(from);
674
+ return '';
675
+ }, ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
676
+ /** Formatted from date. */
677
+ displayValueFrom = computed(() => {
678
+ const d = this._rangeStart();
679
+ return d ? this._formatDate(d) : '';
680
+ }, ...(ngDevMode ? [{ debugName: "displayValueFrom" }] : /* istanbul ignore next */ []));
681
+ /** Formatted to date. */
682
+ displayValueTo = computed(() => {
683
+ const d = this._rangeEnd();
684
+ return d ? this._formatDate(d) : '';
685
+ }, ...(ngDevMode ? [{ debugName: "displayValueTo" }] : /* istanbul ignore next */ []));
686
+ _onChange = () => { };
687
+ _onTouched = () => { };
688
+ _panelRef = null;
689
+ _wrapper = null;
690
+ _docClickUnlisten = null;
691
+ _escUnlisten = null;
692
+ constructor() {
693
+ ensureStyles(this._doc);
694
+ }
695
+ ngOnInit() {
696
+ this._destroyRef.onDestroy(() => this._close());
697
+ }
698
+ toggle() {
699
+ if (this.isDisabled())
700
+ return;
701
+ this.isOpen() ? this._close() : this._open();
702
+ }
703
+ // ── ControlValueAccessor ────────────────────────────────────────────────
704
+ writeValue(val) {
705
+ if (val && typeof val === 'object' && ('from' in val || 'to' in val)) {
706
+ const v = val;
707
+ this._rangeStart.set(this._toDate(v.from));
708
+ this._rangeEnd.set(this._toDate(v.to));
709
+ }
710
+ else {
711
+ this._rangeStart.set(null);
712
+ this._rangeEnd.set(null);
713
+ }
714
+ this._pickState.set('idle');
715
+ }
716
+ registerOnChange(fn) {
717
+ this._onChange = fn;
718
+ }
719
+ registerOnTouched(fn) {
720
+ this._onTouched = fn;
721
+ }
722
+ setDisabledState(isDisabled) {
723
+ this._formDisabled.set(isDisabled);
724
+ }
725
+ // ── Private ─────────────────────────────────────────────────────────────
726
+ _open() {
727
+ if (this._panelRef)
728
+ return;
729
+ this._panelRef = this._vcr.createComponent(BlocCalendarPanelComponent);
730
+ const panel = this._panelRef.instance;
731
+ // Set inputs — range mode
732
+ this._panelRef.setInput('mode', 'range');
733
+ this._panelRef.setInput('rangeStart', this._rangeStart());
734
+ this._panelRef.setInput('rangeEnd', this._rangeEnd());
735
+ this._panelRef.setInput('hoverDate', null);
736
+ this._panelRef.setInput('minDate', this.minDate());
737
+ this._panelRef.setInput('maxDate', this.maxDate());
738
+ // Listen to outputs
739
+ panel.dateSelect.subscribe((date) => this._handleDateSelect(date));
740
+ panel.dateHover.subscribe((date) => this._handleDateHover(date));
741
+ panel.cleared.subscribe(() => this._handleClear());
742
+ // Reset view to show the range start month, or current month
743
+ panel.resetView(this._rangeStart());
744
+ // Create wrapper
745
+ this._wrapper = this._doc.createElement('div');
746
+ this._wrapper.className = 'bloc-dp-trigger-dropdown';
747
+ this._wrapper.setAttribute('role', 'dialog');
748
+ this._wrapper.setAttribute('aria-label', 'Date range picker');
749
+ this._wrapper.appendChild(this._panelRef.location.nativeElement);
750
+ this._doc.body.appendChild(this._wrapper);
751
+ this._positionDropdown();
752
+ this.isOpen.set(true);
753
+ this._onTouched();
754
+ requestAnimationFrame(() => {
755
+ this._docClickUnlisten = this._renderer.listen('document', 'click', (e) => {
756
+ const target = e.target;
757
+ if (!this._el.nativeElement.contains(target) &&
758
+ !this._wrapper?.contains(target)) {
759
+ this._close();
760
+ }
761
+ });
762
+ this._escUnlisten = this._renderer.listen('document', 'keydown.escape', () => {
763
+ this._close();
764
+ });
765
+ });
766
+ }
767
+ _handleDateSelect(date) {
768
+ if (this._pickState() === 'idle') {
769
+ // First click — set anchor
770
+ this._rangeStart.set(date);
771
+ this._rangeEnd.set(null);
772
+ this._hoverDate.set(null);
773
+ this._pickState.set('picking');
774
+ // Update panel inputs
775
+ this._panelRef?.setInput('rangeStart', date);
776
+ this._panelRef?.setInput('rangeEnd', null);
777
+ this._panelRef?.setInput('hoverDate', null);
778
+ }
779
+ else {
780
+ // Second click — complete range
781
+ const anchor = this._rangeStart();
782
+ const [from, to] = anchor <= date ? [anchor, date] : [date, anchor];
783
+ this._rangeStart.set(from);
784
+ this._rangeEnd.set(to);
785
+ this._hoverDate.set(null);
786
+ this._pickState.set('idle');
787
+ this._emitValue({ from, to });
788
+ this._close();
789
+ }
790
+ }
791
+ _handleDateHover(date) {
792
+ if (this._pickState() === 'picking') {
793
+ this._hoverDate.set(date);
794
+ this._panelRef?.setInput('hoverDate', date);
795
+ }
796
+ }
797
+ _handleClear() {
798
+ this._rangeStart.set(null);
799
+ this._rangeEnd.set(null);
800
+ this._hoverDate.set(null);
801
+ this._pickState.set('idle');
802
+ this._panelRef?.setInput('rangeStart', null);
803
+ this._panelRef?.setInput('rangeEnd', null);
804
+ this._panelRef?.setInput('hoverDate', null);
805
+ this._emitValue({ from: null, to: null });
806
+ }
807
+ _emitValue(val) {
808
+ this._onChange(val);
809
+ // Sync FormGroup if provided
810
+ const fg = this.rangeFormGroup();
811
+ if (fg) {
812
+ fg.patchValue({ from: val.from, to: val.to }, { emitEvent: false });
813
+ }
814
+ }
815
+ _close() {
816
+ this._docClickUnlisten?.();
817
+ this._docClickUnlisten = null;
818
+ this._escUnlisten?.();
819
+ this._escUnlisten = null;
820
+ if (this._panelRef) {
821
+ this._panelRef.destroy();
822
+ this._panelRef = null;
823
+ }
824
+ if (this._wrapper) {
825
+ this._wrapper.remove();
826
+ this._wrapper = null;
827
+ }
828
+ // If we were mid-pick, revert to idle and restore previous complete range
829
+ if (this._pickState() === 'picking') {
830
+ this._pickState.set('idle');
831
+ // If no completed range, clear anchor
832
+ if (!this._rangeEnd()) {
833
+ this._rangeStart.set(null);
834
+ }
835
+ }
836
+ this._hoverDate.set(null);
837
+ this.isOpen.set(false);
838
+ }
839
+ _positionDropdown() {
840
+ if (!this._wrapper)
841
+ return;
842
+ const rect = this._el.nativeElement.getBoundingClientRect();
843
+ const openUpward = window.innerHeight - rect.bottom < 320;
844
+ if (openUpward) {
845
+ this._wrapper.classList.add('bloc-dp-trigger-dropdown--upward');
846
+ this._wrapper.style.left = `${rect.left}px`;
847
+ this._wrapper.style.bottom = `${window.innerHeight - rect.top + 10}px`;
848
+ this._wrapper.style.top = 'auto';
849
+ }
850
+ else {
851
+ this._wrapper.classList.remove('bloc-dp-trigger-dropdown--upward');
852
+ this._wrapper.style.left = `${rect.left}px`;
853
+ this._wrapper.style.top = `${rect.bottom + 10}px`;
854
+ this._wrapper.style.bottom = 'auto';
855
+ }
856
+ }
857
+ _formatDate(d) {
858
+ const yyyy = d.getFullYear();
859
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
860
+ const dd = String(d.getDate()).padStart(2, '0');
861
+ return this.format()
862
+ .replace('yyyy', String(yyyy))
863
+ .replace('MM', mm)
864
+ .replace('dd', dd);
865
+ }
866
+ _toDate(val) {
867
+ if (val instanceof Date)
868
+ return val;
869
+ if (val && typeof val === 'string')
870
+ return new Date(val);
871
+ return null;
872
+ }
873
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDateRangePickerTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
874
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.5", type: BlocDateRangePickerTriggerDirective, isStandalone: true, selector: "[blocDateRangePickerTrigger]", inputs: { minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, rangeFormGroup: { classPropertyName: "rangeFormGroup", publicName: "rangeFormGroup", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "toggle()" }, properties: { "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "isOpen()", "class.bloc-date-picker-trigger--open": "isOpen()", "class.bloc-date-picker-trigger--disabled": "isDisabled()", "attr.disabled": "isDisabled() || null" } }, providers: [
875
+ {
876
+ provide: NG_VALUE_ACCESSOR,
877
+ useExisting: forwardRef(() => BlocDateRangePickerTriggerDirective),
878
+ multi: true,
879
+ },
880
+ ], exportAs: ["blocDateRangePickerTrigger"], ngImport: i0 });
881
+ }
882
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDateRangePickerTriggerDirective, decorators: [{
883
+ type: Directive,
884
+ args: [{
885
+ selector: '[blocDateRangePickerTrigger]',
886
+ standalone: true,
887
+ exportAs: 'blocDateRangePickerTrigger',
888
+ providers: [
889
+ {
890
+ provide: NG_VALUE_ACCESSOR,
891
+ useExisting: forwardRef(() => BlocDateRangePickerTriggerDirective),
892
+ multi: true,
893
+ },
894
+ ],
895
+ host: {
896
+ '(click)': 'toggle()',
897
+ '[attr.aria-haspopup]': '"dialog"',
898
+ '[attr.aria-expanded]': 'isOpen()',
899
+ '[class.bloc-date-picker-trigger--open]': 'isOpen()',
900
+ '[class.bloc-date-picker-trigger--disabled]': 'isDisabled()',
901
+ '[attr.disabled]': 'isDisabled() || null',
902
+ },
903
+ }]
904
+ }], ctorParameters: () => [], propDecorators: { minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], rangeFormGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeFormGroup", required: false }] }] } });
336
905
 
337
906
  class BlocDatePickerModule {
338
907
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
339
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerModule, imports: [BlocDatePickerComponent], exports: [BlocDatePickerComponent] });
908
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerModule, imports: [BlocDatePickerTriggerDirective, BlocDateRangePickerTriggerDirective], exports: [BlocDatePickerTriggerDirective, BlocDateRangePickerTriggerDirective] });
340
909
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerModule });
341
910
  }
342
911
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: BlocDatePickerModule, decorators: [{
343
912
  type: NgModule,
344
913
  args: [{
345
- imports: [BlocDatePickerComponent],
346
- exports: [BlocDatePickerComponent],
914
+ imports: [BlocDatePickerTriggerDirective, BlocDateRangePickerTriggerDirective],
915
+ exports: [BlocDatePickerTriggerDirective, BlocDateRangePickerTriggerDirective],
347
916
  }]
348
917
  }] });
349
918
 
@@ -351,5 +920,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
351
920
  * Generated bundle index. Do not edit.
352
921
  */
353
922
 
354
- export { BlocDatePickerComponent, BlocDatePickerModule };
923
+ export { BlocDatePickerModule, BlocDatePickerTriggerDirective, BlocDateRangePickerTriggerDirective };
355
924
  //# sourceMappingURL=bloc-ui-date-picker.mjs.map