@tolle_/tolle-ui 18.3.1 → 18.3.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.
@@ -8,8 +8,14 @@ export declare class AlertDialogDynamicComponent {
8
8
  * content panel plays its exit animation before the overlay is torn down.
9
9
  */
10
10
  closing: boolean;
11
+ /**
12
+ * Result recorded by the action/cancel buttons' `confirmed`/`cancelled`
13
+ * outputs, which fire *before* the button drives the dialog closed. The
14
+ * single close path (`onOpenChange`) then reads it, so a confirm reports
15
+ * `true` instead of racing with the button's own `false` close signal.
16
+ */
17
+ result: boolean;
11
18
  onOpenChange(open: boolean): void;
12
- close(result: boolean): void;
13
19
  static ɵfac: i0.ɵɵFactoryDeclaration<AlertDialogDynamicComponent, never>;
14
20
  static ɵcmp: i0.ɵɵComponentDeclaration<AlertDialogDynamicComponent, "tolle-alert-dialog-dynamic", never, {}, {}, never, never, true, never>;
15
21
  }
@@ -11,5 +11,7 @@ export interface AlertDialogConfig {
11
11
  export declare class AlertDialogRef {
12
12
  private readonly _afterClosed;
13
13
  afterClosed$: Observable<boolean>;
14
+ /** Guards against the dialog's several close paths racing to emit twice. */
15
+ private closed;
14
16
  close(result?: boolean): void;
15
17
  }
@@ -16,6 +16,8 @@ export declare class RangeCalendarComponent implements OnInit, OnChanges, Contro
16
16
  rangeSelect: EventEmitter<DateRange>;
17
17
  currentView: 'date' | 'month' | 'year';
18
18
  viewDate: Date;
19
+ /** Which visible month the quick pickers are editing; always 0 in the single-month view. */
20
+ pickerMonthIndex: number;
19
21
  value: DateRange;
20
22
  weekDays: string[];
21
23
  daysInMonth: Date[];
@@ -23,7 +25,6 @@ export declare class RangeCalendarComponent implements OnInit, OnChanges, Contro
23
25
  visibleMonths: {
24
26
  date: Date;
25
27
  days: Date[];
26
- label: string;
27
28
  }[];
28
29
  months: string[];
29
30
  years: number[];
@@ -36,7 +37,11 @@ export declare class RangeCalendarComponent implements OnInit, OnChanges, Contro
36
37
  ngOnChanges(changes: SimpleChanges): void;
37
38
  generateDays(): void;
38
39
  generateYears(): void;
39
- setView(view: 'date' | 'month' | 'year'): void;
40
+ /** The month the quick pickers act on — `viewDate` shifted to the clicked panel. */
41
+ get pickerDate(): Date;
42
+ /** `monthIndex` is the index of the visible month whose header was clicked. */
43
+ setView(view: 'date' | 'month' | 'year', monthIndex?: number): void;
44
+ headerBtnClass(view: 'month' | 'year'): string;
40
45
  prev(): void;
41
46
  next(): void;
42
47
  selectDate(date: Date): void;
@@ -50,6 +55,12 @@ export declare class RangeCalendarComponent implements OnInit, OnChanges, Contro
50
55
  private focusDay;
51
56
  selectMonth(monthIndex: number): void;
52
57
  selectYear(year: number): void;
58
+ /**
59
+ * Re-anchors the view so the panel the picker was opened from lands on `date`,
60
+ * then returns to the day grid. In the single-month view the offset is 0, so
61
+ * this is just "show that month".
62
+ */
63
+ private applyPickerDate;
53
64
  isSelected(date: Date): boolean;
54
65
  /** True when `date` belongs to `refMonth` (used to blank out adjacent-month cells). */
55
66
  isCurrentMonth(date: Date, refMonth: Date): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tolle_/tolle-ui",
3
- "version": "18.3.1",
3
+ "version": "18.3.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@tolle_/tolle-ui",
3
- "version": "18.3.1",
3
+ "version": "18.3.3",
4
4
  "registry": "r/{name}.json",
5
5
  "components": [
6
6
  {
@@ -15,12 +15,12 @@
15
15
  {
16
16
  "path": "alert-dialog-dynamic.component.ts",
17
17
  "target": "ui/alert-dialog-dynamic.component.ts",
18
- "content": "import { Component, Inject, inject, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport {\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent\n} from './alert-dialog.component';\nimport { ButtonComponent } from './button.component';\nimport { AlertDialogConfig, AlertDialogRef } from './alert-dialog.types';\n\n@Component({\n selector: 'tolle-alert-dialog-dynamic',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [\n CommonModule,\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent,\n ButtonComponent\n ],\n template: `\n <tolle-alert-dialog [open]=\"!closing\" (openChange)=\"onOpenChange($event)\">\n <tolle-alert-dialog-content [size]=\"config.size || 'lg'\">\n <tolle-alert-dialog-header>\n <tolle-alert-dialog-title>{{ config.title }}</tolle-alert-dialog-title>\n <tolle-alert-dialog-description>\n {{ config.description }}\n </tolle-alert-dialog-description>\n </tolle-alert-dialog-header>\n <tolle-alert-dialog-footer>\n <tolle-alert-dialog-cancel (click)=\"close(false)\">\n <tolle-button variant=\"outline\">{{ config.cancelText || 'Cancel' }}</tolle-button>\n </tolle-alert-dialog-cancel>\n <tolle-alert-dialog-action (click)=\"close(true)\">\n <tolle-button [variant]=\"config.variant === 'destructive' ? 'destructive' : 'default'\">\n {{ config.actionText || 'Continue' }}\n </tolle-button>\n </tolle-alert-dialog-action>\n </tolle-alert-dialog-footer>\n </tolle-alert-dialog-content>\n </tolle-alert-dialog>\n `\n})\nexport class AlertDialogDynamicComponent {\n config!: AlertDialogConfig;\n dialogRef!: AlertDialogRef;\n\n /**\n * Set by `AlertDialogService` once the dialog's result is ready, so the\n * content panel plays its exit animation before the overlay is torn down.\n */\n closing = false;\n\n onOpenChange(open: boolean) {\n if (!open) {\n this.close(false);\n }\n }\n\n close(result: boolean) {\n this.dialogRef.close(result);\n }\n}\n"
18
+ "content": "import { Component, Inject, inject, OnInit } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport {\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent\n} from './alert-dialog.component';\nimport { ButtonComponent } from './button.component';\nimport { AlertDialogConfig, AlertDialogRef } from './alert-dialog.types';\n\n@Component({\n selector: 'tolle-alert-dialog-dynamic',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [\n CommonModule,\n AlertDialogComponent,\n AlertDialogContentComponent,\n AlertDialogHeaderComponent,\n AlertDialogTitleComponent,\n AlertDialogDescriptionComponent,\n AlertDialogFooterComponent,\n AlertDialogCancelComponent,\n AlertDialogActionComponent,\n ButtonComponent\n ],\n template: `\n <tolle-alert-dialog [open]=\"!closing\" (openChange)=\"onOpenChange($event)\">\n <tolle-alert-dialog-content [size]=\"config.size || 'lg'\">\n <tolle-alert-dialog-header>\n <tolle-alert-dialog-title>{{ config.title }}</tolle-alert-dialog-title>\n <tolle-alert-dialog-description>\n {{ config.description }}\n </tolle-alert-dialog-description>\n </tolle-alert-dialog-header>\n <tolle-alert-dialog-footer>\n <tolle-alert-dialog-cancel (cancelled)=\"result = false\">\n <tolle-button variant=\"outline\">{{ config.cancelText || 'Cancel' }}</tolle-button>\n </tolle-alert-dialog-cancel>\n <tolle-alert-dialog-action (confirmed)=\"result = true\">\n <tolle-button [variant]=\"config.variant === 'destructive' ? 'destructive' : 'default'\">\n {{ config.actionText || 'Continue' }}\n </tolle-button>\n </tolle-alert-dialog-action>\n </tolle-alert-dialog-footer>\n </tolle-alert-dialog-content>\n </tolle-alert-dialog>\n `\n})\nexport class AlertDialogDynamicComponent {\n config!: AlertDialogConfig;\n dialogRef!: AlertDialogRef;\n\n /**\n * Set by `AlertDialogService` once the dialog's result is ready, so the\n * content panel plays its exit animation before the overlay is torn down.\n */\n closing = false;\n\n /**\n * Result recorded by the action/cancel buttons' `confirmed`/`cancelled`\n * outputs, which fire *before* the button drives the dialog closed. The\n * single close path (`onOpenChange`) then reads it, so a confirm reports\n * `true` instead of racing with the button's own `false` close signal.\n */\n result = false;\n\n onOpenChange(open: boolean) {\n if (!open) {\n this.dialogRef.close(this.result);\n }\n }\n}\n"
19
19
  },
20
20
  {
21
21
  "path": "alert-dialog.types.ts",
22
22
  "target": "ui/alert-dialog.types.ts",
23
- "content": "import { Subject, Observable } from 'rxjs';\n\nexport type AlertDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'fit';\n\nexport interface AlertDialogConfig {\n title: string;\n description: string;\n cancelText?: string;\n actionText?: string;\n variant?: 'default' | 'destructive';\n size?: AlertDialogSize;\n}\n\nexport class AlertDialogRef {\n private readonly _afterClosed = new Subject<boolean>();\n afterClosed$: Observable<boolean> = this._afterClosed.asObservable();\n\n close(result: boolean = false): void {\n this._afterClosed.next(result);\n this._afterClosed.complete();\n }\n}\n"
23
+ "content": "import { Subject, Observable } from 'rxjs';\n\nexport type AlertDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'fit';\n\nexport interface AlertDialogConfig {\n title: string;\n description: string;\n cancelText?: string;\n actionText?: string;\n variant?: 'default' | 'destructive';\n size?: AlertDialogSize;\n}\n\nexport class AlertDialogRef {\n private readonly _afterClosed = new Subject<boolean>();\n afterClosed$: Observable<boolean> = this._afterClosed.asObservable();\n\n /** Guards against the dialog's several close paths racing to emit twice. */\n private closed = false;\n\n close(result: boolean = false): void {\n if (this.closed) return;\n this.closed = true;\n this._afterClosed.next(result);\n this._afterClosed.complete();\n }\n}\n"
24
24
  },
25
25
  {
26
26
  "path": "utils/cn.ts",
@@ -15,7 +15,7 @@
15
15
  {
16
16
  "path": "alert-dialog.types.ts",
17
17
  "target": "ui/alert-dialog.types.ts",
18
- "content": "import { Subject, Observable } from 'rxjs';\n\nexport type AlertDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'fit';\n\nexport interface AlertDialogConfig {\n title: string;\n description: string;\n cancelText?: string;\n actionText?: string;\n variant?: 'default' | 'destructive';\n size?: AlertDialogSize;\n}\n\nexport class AlertDialogRef {\n private readonly _afterClosed = new Subject<boolean>();\n afterClosed$: Observable<boolean> = this._afterClosed.asObservable();\n\n close(result: boolean = false): void {\n this._afterClosed.next(result);\n this._afterClosed.complete();\n }\n}\n"
18
+ "content": "import { Subject, Observable } from 'rxjs';\n\nexport type AlertDialogSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'fit';\n\nexport interface AlertDialogConfig {\n title: string;\n description: string;\n cancelText?: string;\n actionText?: string;\n variant?: 'default' | 'destructive';\n size?: AlertDialogSize;\n}\n\nexport class AlertDialogRef {\n private readonly _afterClosed = new Subject<boolean>();\n afterClosed$: Observable<boolean> = this._afterClosed.asObservable();\n\n /** Guards against the dialog's several close paths racing to emit twice. */\n private closed = false;\n\n close(result: boolean = false): void {\n if (this.closed) return;\n this.closed = true;\n this._afterClosed.next(result);\n this._afterClosed.complete();\n }\n}\n"
19
19
  },
20
20
  {
21
21
  "path": "utils/cn.ts",
@@ -12,7 +12,7 @@
12
12
  {
13
13
  "path": "range-calendar.component.ts",
14
14
  "target": "ui/range-calendar.component.ts",
15
- "content": "import { Component, Input, OnInit, OnChanges, SimpleChanges, forwardRef, Output, EventEmitter, ChangeDetectorRef, ElementRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport {\n addDays, addMonths, subMonths, startOfMonth, endOfMonth,\n startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth,\n isSameDay, isToday, setMonth, setYear, addYears, subYears,\n isBefore, startOfDay, isWithinInterval, format\n} from 'date-fns';\nimport { cn } from './utils/cn';\nimport {DateRange} from './types/date-range';\n\n@Component({\n selector: 'tolle-range-calendar',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => RangeCalendarComponent),\n multi: true\n }\n ],\n template: `\n <div [class]=\"cn('text-popover-foreground inline-block min-w-fit', bordered ? 'p-3 border rounded-md bg-background shadow-sm' : '', class)\">\n\n <!-- ===== Multi-month view (side-by-side, shared range) ===== -->\n <div *ngIf=\"numberOfMonths > 1\" class=\"flex flex-col gap-4 sm:flex-row\">\n <div *ngFor=\"let m of visibleMonths; let idx = index\" class=\"space-y-2\">\n <div class=\"flex items-center justify-between pt-1 pb-2\">\n <button *ngIf=\"idx === 0\" type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\">\n <i class=\"ri-arrow-left-s-line text-lg\"></i>\n </button>\n <span *ngIf=\"idx !== 0\" class=\"h-7 w-7\"></span>\n\n <div class=\"text-sm font-semibold\">{{ m.label }}</div>\n\n <button *ngIf=\"idx === visibleMonths.length - 1\" type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\">\n <i class=\"ri-arrow-right-s-line text-lg\"></i>\n </button>\n <span *ngIf=\"idx !== visibleMonths.length - 1\" class=\"h-7 w-7\"></span>\n </div>\n\n <div class=\"grid grid-cols-7 gap-y-1\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">{{ day }}</span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-y-1\" (keydown)=\"onGridKeydown($event)\">\n <ng-container *ngFor=\"let date of m.days\">\n <!-- Only the month's own days are rendered; adjacent-month days are\n blank spacers so each calendar in a multi-month view is self-contained. -->\n <button\n *ngIf=\"isCurrentMonth(date, m.date); else pad\"\n type=\"button\"\n role=\"gridcell\"\n [attr.data-date]=\"fmtKey(date)\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date, m.date)\"\n >\n {{ date | date: 'd' }}\n </button>\n <ng-template #pad><span class=\"h-9 w-9\" aria-hidden=\"true\"></span></ng-template>\n </ng-container>\n </div>\n </div>\n </div>\n\n <!-- ===== Single-month view (with month / year quick pickers) ===== -->\n <ng-container *ngIf=\"numberOfMonths <= 1\">\n <div class=\"flex items-center justify-between pt-1 pb-4 gap-2\">\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" (click)=\"setView('month')\"\n [class]=\"cn('text-sm font-semibold px-2 py-1 rounded transition-colors', currentView === 'month' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground')\">\n {{ viewDate | date: 'MMMM' }}\n </button>\n <button type=\"button\" (click)=\"setView('year')\"\n [class]=\"cn('text-sm font-semibold px-2 py-1 rounded transition-colors', currentView === 'year' ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground')\">\n {{ viewDate | date: 'yyyy' }}\n </button>\n </div>\n <div class=\"flex items-center space-x-1\">\n <button type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\"><i class=\"ri-arrow-left-s-line text-lg\"></i></button>\n <button type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\"><i class=\"ri-arrow-right-s-line text-lg\"></i></button>\n </div>\n </div>\n\n <div *ngIf=\"currentView === 'date'\" class=\"space-y-2 animate-in fade-in zoom-in-95 duration-200\">\n <div class=\"grid grid-cols-7 gap-y-1 w-full\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">\n {{ day }}\n </span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-y-1 w-full\" (keydown)=\"onGridKeydown($event)\">\n <button\n *ngFor=\"let date of daysInMonth\"\n type=\"button\"\n role=\"gridcell\"\n [attr.data-date]=\"fmtKey(date)\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date)\"\n >\n {{ date | date: 'd' }}\n </button>\n </div>\n </div>\n\n <div *ngIf=\"currentView === 'month'\" class=\"grid grid-cols-3 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button *ngFor=\"let month of months; let i = index\" type=\"button\" (click)=\"selectMonth(i)\"\n [class]=\"cn('text-sm py-2.5 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors', i === viewDate.getMonth() ? 'bg-primary text-primary-foreground' : '')\">\n {{ month }}\n </button>\n </div>\n\n <div *ngIf=\"currentView === 'year'\" class=\"grid grid-cols-4 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button *ngFor=\"let year of years\" type=\"button\" (click)=\"selectYear(year)\"\n [class]=\"cn('text-sm py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors', year === viewDate.getFullYear() ? 'bg-primary text-primary-foreground' : '')\">\n {{ year }}\n </button>\n </div>\n </ng-container>\n </div>\n `\n})\nexport class RangeCalendarComponent implements OnInit, OnChanges, ControlValueAccessor {\n @Input() class = '';\n @Input() disablePastDates = false;\n /** Renders the calendar's own border/background/shadow. Set `false` inside a\n * card/popover that already provides chrome. @default true */\n @Input() bordered = true;\n /** Number of consecutive months rendered side by side. @default 1 */\n @Input() numberOfMonths = 1;\n @Output() rangeSelect = new EventEmitter<DateRange>(); // Emits whenever selection changes\n\n currentView: 'date' | 'month' | 'year' = 'date';\n viewDate: Date = new Date();\n\n // The Range Value\n value: DateRange = { start: null, end: null };\n\n weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];\n daysInMonth: Date[] = [];\n /** Grids for each visible month (multi-month view). */\n visibleMonths: { date: Date; days: Date[]; label: string }[] = [];\n months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n years: number[] = [];\n\n navBtnClass = cn('h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-input rounded-md flex items-center justify-center hover:bg-accent hover:text-accent-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring');\n\n onTouched: () => void = () => {};\n onChange: (value: DateRange) => void = () => {};\n protected cn = cn;\n\n constructor(private cdr: ChangeDetectorRef, private el: ElementRef<HTMLElement>) {}\n\n ngOnInit() {\n this.generateDays();\n this.generateYears();\n }\n\n ngOnChanges(changes: SimpleChanges) {\n // Rebuild the month grids when the visible-month count changes after init\n // (e.g. a `[numberOfMonths]` toggle), so single ⇄ multi-month is reactive.\n if (changes['numberOfMonths'] && !changes['numberOfMonths'].firstChange) {\n this.generateDays();\n }\n }\n\n // --- Date Generation Logic (Same as Calendar) ---\n generateDays() {\n const start = startOfWeek(startOfMonth(this.viewDate));\n const end = endOfWeek(endOfMonth(this.viewDate));\n this.daysInMonth = eachDayOfInterval({ start, end });\n\n // Multi-month grids: viewDate is the leftmost month, then N-1 consecutive.\n const count = Math.max(1, this.numberOfMonths);\n this.visibleMonths = Array.from({ length: count }, (_, i) => {\n const monthDate = addMonths(this.viewDate, i);\n return {\n date: monthDate,\n days: eachDayOfInterval({ start: startOfWeek(startOfMonth(monthDate)), end: endOfWeek(endOfMonth(monthDate)) }),\n label: format(monthDate, 'MMMM yyyy'),\n };\n });\n }\n\n generateYears() {\n const currentYear = this.viewDate.getFullYear();\n this.years = Array.from({ length: 16 }, (_, i) => currentYear - 6 + i);\n }\n\n setView(view: 'date' | 'month' | 'year') {\n this.currentView = view;\n if (view === 'year') this.generateYears();\n }\n\n prev() {\n if (this.currentView === 'date') {\n this.viewDate = subMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = subYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n }\n }\n\n next() {\n if (this.currentView === 'date') {\n this.viewDate = addMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = addYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n }\n }\n\n // --- Range Selection Logic ---\n\n selectDate(date: Date) {\n if (this.isDateDisabled(date)) return;\n\n const { start, end } = this.value;\n\n // 1. If start exists but end doesn't\n if (start && !end) {\n if (isBefore(date, start)) {\n // User clicked earlier date -> Reset start\n this.value = { start: date, end: null };\n } else {\n // User clicked later date -> Complete range\n this.value = { start, end: date };\n }\n }\n // 2. If neither exist OR both exist (reset)\n else {\n this.value = { start: date, end: null };\n }\n\n // Only re-anchor the view when the clicked day isn't already on a visible month\n // (so clicking the right-hand month in a 2-month view doesn't jump the calendar).\n const onVisibleMonth = this.visibleMonths.some((m) => isSameMonth(date, m.date));\n if (!onVisibleMonth) {\n this.viewDate = startOfMonth(date);\n this.generateDays();\n }\n\n this.onChange(this.value);\n this.rangeSelect.emit(this.value);\n this.onTouched();\n }\n\n // --- Grid keyboard navigation (date view only) ---\n\n /**\n * Arrow keys move by day/week, Home/End to week edges, PageUp/PageDown by\n * month. Enter/Space are left to the native day <button>. The focused day is\n * read from its `data-date` key, so navigation is independent of grid layout\n * (adjacent-month days aren't rendered in the multi-month view).\n */\n onGridKeydown(event: KeyboardEvent) {\n if (this.numberOfMonths <= 1 && this.currentView !== 'date') return;\n\n const key = (event.target as HTMLElement).getAttribute('data-date');\n if (!key) return;\n const current = this.keyToDate(key);\n\n let next: Date | null = null;\n switch (event.key) {\n case 'ArrowLeft': next = addDays(current, -1); break;\n case 'ArrowRight': next = addDays(current, 1); break;\n case 'ArrowUp': next = addDays(current, -7); break;\n case 'ArrowDown': next = addDays(current, 7); break;\n case 'Home': next = startOfWeek(current); break;\n case 'End': next = endOfWeek(current); break;\n case 'PageUp': next = subMonths(current, 1); break;\n case 'PageDown': next = addMonths(current, 1); break;\n default: return;\n }\n\n event.preventDefault();\n this.focusDay(next);\n }\n\n private focusDay(date: Date) {\n const selector = `[data-date=\"${this.fmtKey(date)}\"]`;\n let btn = this.el.nativeElement.querySelector(selector) as HTMLElement | null;\n if (btn) { btn.focus(); return; }\n\n // Target day isn't currently rendered → page the view toward it, then retry.\n if (this.numberOfMonths > 1) {\n this.viewDate = isBefore(date, this.visibleMonths[0].date)\n ? subMonths(this.viewDate, 1)\n : addMonths(this.viewDate, 1);\n } else {\n this.viewDate = startOfMonth(date);\n }\n this.generateDays();\n this.cdr.detectChanges();\n btn = this.el.nativeElement.querySelector(selector) as HTMLElement | null;\n btn?.focus();\n }\n\n selectMonth(monthIndex: number) {\n this.viewDate = setMonth(this.viewDate, monthIndex);\n this.currentView = 'date';\n this.generateDays();\n }\n\n selectYear(year: number) {\n this.viewDate = setYear(this.viewDate, year);\n this.currentView = 'date';\n this.generateDays();\n }\n\n // --- Visual Styling for Range ---\n\n isSelected(date: Date): boolean {\n const { start, end } = this.value;\n return !!((start && isSameDay(date, start)) || (end && isSameDay(date, end)));\n }\n\n /** True when `date` belongs to `refMonth` (used to blank out adjacent-month cells). */\n isCurrentMonth(date: Date, refMonth: Date): boolean {\n return isSameMonth(date, refMonth);\n }\n\n /** Stable, timezone-safe `data-date` key used to locate day buttons for focus. */\n fmtKey(date: Date): string {\n return format(date, 'yyyy-MM-dd');\n }\n\n private keyToDate(key: string): Date {\n const [y, m, d] = key.split('-').map(Number);\n return new Date(y, m - 1, d);\n }\n\n getDayClass(date: Date, refMonth: Date = this.viewDate) {\n const { start, end } = this.value;\n const isOutside = !isSameMonth(date, refMonth);\n const isDisabled = this.isDateDisabled(date);\n\n // Range Checks\n const isStart = start && isSameDay(date, start);\n const isEnd = end && isSameDay(date, end);\n const isInside = start && end && isWithinInterval(date, { start, end });\n const isTodayDate = isToday(date);\n\n return cn(\n // Base: h-9 w-9, but we remove margins/rounding for the 'strip' effect\n 'h-9 w-9 p-0 font-normal text-sm transition-all flex items-center justify-center relative z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n\n // Default State (Not selected, Not disabled)\n !isInside && !isStart && !isEnd && !isDisabled && 'hover:bg-accent hover:text-accent-foreground rounded-md',\n\n // The \"Caps\": Start and End\n (isStart || isEnd) && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',\n\n // The \"Strip\": Dates between start and end\n isInside && !isStart && !isEnd && 'bg-accent text-accent-foreground rounded-none',\n\n // Connecting the Caps to the Strip (Rectangular inside edges)\n isStart && end && 'rounded-l-md rounded-r-none',\n isEnd && start && 'rounded-r-md rounded-l-none',\n // If no end date yet, Start should be fully rounded\n isStart && !end && 'rounded-md',\n\n // Muted/Disabled logic\n !isInside && isTodayDate && !isStart && !isEnd && 'bg-accent/50 text-accent-foreground rounded-md',\n (isOutside || isDisabled) && 'text-muted-foreground opacity-50',\n isDisabled && 'cursor-not-allowed'\n );\n }\n\n isDateDisabled(date: Date): boolean {\n return this.disablePastDates ? isBefore(date, startOfDay(new Date())) : false;\n }\n\n // --- CVA Implementation ---\n writeValue(val: DateRange | null): void {\n if (val) {\n this.value = val;\n if (val.start) this.viewDate = val.start;\n this.generateDays();\n this.generateYears();\n }\n }\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n}\n"
15
+ "content": "import { Component, Input, OnInit, OnChanges, SimpleChanges, forwardRef, Output, EventEmitter, ChangeDetectorRef, ElementRef } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport {\n addDays, addMonths, subMonths, startOfMonth, endOfMonth,\n startOfWeek, endOfWeek, eachDayOfInterval, isSameMonth,\n isSameDay, isToday, setMonth, setYear, addYears, subYears,\n isBefore, startOfDay, isWithinInterval, format\n} from 'date-fns';\nimport { cn } from './utils/cn';\nimport {DateRange} from './types/date-range';\n\n@Component({\n selector: 'tolle-range-calendar',\n styles: [':host { display: block; }'],\n standalone: true,\n imports: [CommonModule],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => RangeCalendarComponent),\n multi: true\n }\n ],\n template: `\n <div [class]=\"cn('text-popover-foreground inline-block min-w-fit', bordered ? 'p-3 border rounded-md bg-background shadow-sm' : '', class)\">\n\n <!-- ===== Month / year quick pickers (shared by the single- and multi-month views) ===== -->\n <ng-container *ngIf=\"currentView !== 'date'\">\n <div class=\"flex items-center justify-between pt-1 pb-4 gap-2\">\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" (click)=\"setView('month', pickerMonthIndex)\" [class]=\"headerBtnClass('month')\">\n {{ pickerDate | date: 'MMMM' }}\n </button>\n <button type=\"button\" (click)=\"setView('year', pickerMonthIndex)\" [class]=\"headerBtnClass('year')\">\n {{ pickerDate | date: 'yyyy' }}\n </button>\n </div>\n <div class=\"flex items-center space-x-1\">\n <button type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous\"><i class=\"ri-arrow-left-s-line text-lg\"></i></button>\n <button type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next\"><i class=\"ri-arrow-right-s-line text-lg\"></i></button>\n </div>\n </div>\n\n <div *ngIf=\"currentView === 'month'\" class=\"grid grid-cols-3 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button *ngFor=\"let month of months; let i = index\" type=\"button\" (click)=\"selectMonth(i)\"\n [class]=\"cn('text-sm py-2.5 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors', i === pickerDate.getMonth() ? 'bg-primary text-primary-foreground' : '')\">\n {{ month }}\n </button>\n </div>\n\n <div *ngIf=\"currentView === 'year'\" class=\"grid grid-cols-4 gap-2 w-64 animate-in fade-in zoom-in-95 duration-200\">\n <button *ngFor=\"let year of years\" type=\"button\" (click)=\"selectYear(year)\"\n [class]=\"cn('text-sm py-2 rounded-md hover:bg-accent hover:text-accent-foreground transition-colors', year === pickerDate.getFullYear() ? 'bg-primary text-primary-foreground' : '')\">\n {{ year }}\n </button>\n </div>\n </ng-container>\n\n <!-- ===== Multi-month view (side-by-side, shared range) ===== -->\n <div *ngIf=\"currentView === 'date' && numberOfMonths > 1\" class=\"flex flex-col gap-4 sm:flex-row\">\n <div *ngFor=\"let m of visibleMonths; let idx = index\" class=\"space-y-2\">\n <div class=\"flex items-center justify-between pt-1 pb-2\">\n <button *ngIf=\"idx === 0\" type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\">\n <i class=\"ri-arrow-left-s-line text-lg\"></i>\n </button>\n <span *ngIf=\"idx !== 0\" class=\"h-7 w-7\"></span>\n\n <!-- Each panel's month and year open the quick pickers for that panel,\n so a two-month view jumps the same way the single-month one does. -->\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" (click)=\"setView('month', idx)\" [class]=\"headerBtnClass('month')\">{{ m.date | date: 'MMMM' }}</button>\n <button type=\"button\" (click)=\"setView('year', idx)\" [class]=\"headerBtnClass('year')\">{{ m.date | date: 'yyyy' }}</button>\n </div>\n\n <button *ngIf=\"idx === visibleMonths.length - 1\" type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\">\n <i class=\"ri-arrow-right-s-line text-lg\"></i>\n </button>\n <span *ngIf=\"idx !== visibleMonths.length - 1\" class=\"h-7 w-7\"></span>\n </div>\n\n <div class=\"grid grid-cols-7 gap-y-1\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">{{ day }}</span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-y-1\" (keydown)=\"onGridKeydown($event)\">\n <ng-container *ngFor=\"let date of m.days\">\n <!-- Only the month's own days are rendered; adjacent-month days are\n blank spacers so each calendar in a multi-month view is self-contained. -->\n <button\n *ngIf=\"isCurrentMonth(date, m.date); else pad\"\n type=\"button\"\n role=\"gridcell\"\n [attr.data-date]=\"fmtKey(date)\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date, m.date)\"\n >\n {{ date | date: 'd' }}\n </button>\n <ng-template #pad><span class=\"h-9 w-9\" aria-hidden=\"true\"></span></ng-template>\n </ng-container>\n </div>\n </div>\n </div>\n\n <!-- ===== Single-month view ===== -->\n <ng-container *ngIf=\"currentView === 'date' && numberOfMonths <= 1\">\n <div class=\"flex items-center justify-between pt-1 pb-4 gap-2\">\n <div class=\"flex items-center gap-1\">\n <button type=\"button\" (click)=\"setView('month')\" [class]=\"headerBtnClass('month')\">\n {{ viewDate | date: 'MMMM' }}\n </button>\n <button type=\"button\" (click)=\"setView('year')\" [class]=\"headerBtnClass('year')\">\n {{ viewDate | date: 'yyyy' }}\n </button>\n </div>\n <div class=\"flex items-center space-x-1\">\n <button type=\"button\" (click)=\"prev()\" [class]=\"navBtnClass\" aria-label=\"Previous month\"><i class=\"ri-arrow-left-s-line text-lg\"></i></button>\n <button type=\"button\" (click)=\"next()\" [class]=\"navBtnClass\" aria-label=\"Next month\"><i class=\"ri-arrow-right-s-line text-lg\"></i></button>\n </div>\n </div>\n\n <div class=\"space-y-2 animate-in fade-in zoom-in-95 duration-200\">\n <div class=\"grid grid-cols-7 gap-y-1 w-full\">\n <span *ngFor=\"let day of weekDays\" class=\"text-[0.8rem] text-muted-foreground font-normal text-center w-9\">\n {{ day }}\n </span>\n </div>\n <div role=\"grid\" class=\"grid grid-cols-7 gap-y-1 w-full\" (keydown)=\"onGridKeydown($event)\">\n <button\n *ngFor=\"let date of daysInMonth\"\n type=\"button\"\n role=\"gridcell\"\n [attr.data-date]=\"fmtKey(date)\"\n [attr.aria-selected]=\"isSelected(date)\"\n [attr.aria-label]=\"(date | date:'fullDate')\"\n (click)=\"selectDate(date)\"\n [disabled]=\"isDateDisabled(date)\"\n [class]=\"getDayClass(date)\"\n >\n {{ date | date: 'd' }}\n </button>\n </div>\n </div>\n </ng-container>\n </div>\n `\n})\nexport class RangeCalendarComponent implements OnInit, OnChanges, ControlValueAccessor {\n @Input() class = '';\n @Input() disablePastDates = false;\n /** Renders the calendar's own border/background/shadow. Set `false` inside a\n * card/popover that already provides chrome. @default true */\n @Input() bordered = true;\n /** Number of consecutive months rendered side by side. @default 1 */\n @Input() numberOfMonths = 1;\n @Output() rangeSelect = new EventEmitter<DateRange>(); // Emits whenever selection changes\n\n currentView: 'date' | 'month' | 'year' = 'date';\n viewDate: Date = new Date();\n /** Which visible month the quick pickers are editing; always 0 in the single-month view. */\n pickerMonthIndex = 0;\n\n // The Range Value\n value: DateRange = { start: null, end: null };\n\n weekDays = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];\n daysInMonth: Date[] = [];\n /** Grids for each visible month (multi-month view). */\n visibleMonths: { date: Date; days: Date[] }[] = [];\n months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n years: number[] = [];\n\n navBtnClass = cn('h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 border border-input rounded-md flex items-center justify-center hover:bg-accent hover:text-accent-foreground transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring');\n\n onTouched: () => void = () => {};\n onChange: (value: DateRange) => void = () => {};\n protected cn = cn;\n\n constructor(private cdr: ChangeDetectorRef, private el: ElementRef<HTMLElement>) {}\n\n ngOnInit() {\n this.generateDays();\n this.generateYears();\n }\n\n ngOnChanges(changes: SimpleChanges) {\n // Rebuild the month grids when the visible-month count changes after init\n // (e.g. a `[numberOfMonths]` toggle), so single ⇄ multi-month is reactive.\n if (changes['numberOfMonths'] && !changes['numberOfMonths'].firstChange) {\n this.generateDays();\n }\n }\n\n // --- Date Generation Logic (Same as Calendar) ---\n generateDays() {\n const start = startOfWeek(startOfMonth(this.viewDate));\n const end = endOfWeek(endOfMonth(this.viewDate));\n this.daysInMonth = eachDayOfInterval({ start, end });\n\n // Multi-month grids: viewDate is the leftmost month, then N-1 consecutive.\n const count = Math.max(1, this.numberOfMonths);\n this.visibleMonths = Array.from({ length: count }, (_, i) => {\n const monthDate = addMonths(this.viewDate, i);\n return {\n date: monthDate,\n days: eachDayOfInterval({ start: startOfWeek(startOfMonth(monthDate)), end: endOfWeek(endOfMonth(monthDate)) }),\n };\n });\n }\n\n generateYears() {\n const currentYear = this.pickerDate.getFullYear();\n this.years = Array.from({ length: 16 }, (_, i) => currentYear - 6 + i);\n }\n\n /** The month the quick pickers act on — `viewDate` shifted to the clicked panel. */\n get pickerDate(): Date {\n return addMonths(this.viewDate, this.pickerMonthIndex);\n }\n\n /** `monthIndex` is the index of the visible month whose header was clicked. */\n setView(view: 'date' | 'month' | 'year', monthIndex = 0) {\n this.pickerMonthIndex = view === 'date' ? 0 : monthIndex;\n this.currentView = view;\n if (view === 'year') this.generateYears();\n }\n\n headerBtnClass(view: 'month' | 'year') {\n return cn(\n 'text-sm font-semibold px-2 py-1 rounded transition-colors',\n this.currentView === view ? 'bg-secondary text-secondary-foreground' : 'hover:bg-accent hover:text-accent-foreground'\n );\n }\n\n prev() {\n if (this.currentView === 'date') {\n this.viewDate = subMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = subYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = subYears(this.viewDate, 1);\n }\n }\n\n next() {\n if (this.currentView === 'date') {\n this.viewDate = addMonths(this.viewDate, 1);\n this.generateDays();\n } else if (this.currentView === 'year') {\n this.viewDate = addYears(this.viewDate, 16);\n this.generateYears();\n } else if (this.currentView === 'month') {\n this.viewDate = addYears(this.viewDate, 1);\n }\n }\n\n // --- Range Selection Logic ---\n\n selectDate(date: Date) {\n if (this.isDateDisabled(date)) return;\n\n const { start, end } = this.value;\n\n // 1. If start exists but end doesn't\n if (start && !end) {\n if (isBefore(date, start)) {\n // User clicked earlier date -> Reset start\n this.value = { start: date, end: null };\n } else {\n // User clicked later date -> Complete range\n this.value = { start, end: date };\n }\n }\n // 2. If neither exist OR both exist (reset)\n else {\n this.value = { start: date, end: null };\n }\n\n // Only re-anchor the view when the clicked day isn't already on a visible month\n // (so clicking the right-hand month in a 2-month view doesn't jump the calendar).\n const onVisibleMonth = this.visibleMonths.some((m) => isSameMonth(date, m.date));\n if (!onVisibleMonth) {\n this.viewDate = startOfMonth(date);\n this.generateDays();\n }\n\n this.onChange(this.value);\n this.rangeSelect.emit(this.value);\n this.onTouched();\n }\n\n // --- Grid keyboard navigation (date view only) ---\n\n /**\n * Arrow keys move by day/week, Home/End to week edges, PageUp/PageDown by\n * month. Enter/Space are left to the native day <button>. The focused day is\n * read from its `data-date` key, so navigation is independent of grid layout\n * (adjacent-month days aren't rendered in the multi-month view).\n */\n onGridKeydown(event: KeyboardEvent) {\n if (this.numberOfMonths <= 1 && this.currentView !== 'date') return;\n\n const key = (event.target as HTMLElement).getAttribute('data-date');\n if (!key) return;\n const current = this.keyToDate(key);\n\n let next: Date | null = null;\n switch (event.key) {\n case 'ArrowLeft': next = addDays(current, -1); break;\n case 'ArrowRight': next = addDays(current, 1); break;\n case 'ArrowUp': next = addDays(current, -7); break;\n case 'ArrowDown': next = addDays(current, 7); break;\n case 'Home': next = startOfWeek(current); break;\n case 'End': next = endOfWeek(current); break;\n case 'PageUp': next = subMonths(current, 1); break;\n case 'PageDown': next = addMonths(current, 1); break;\n default: return;\n }\n\n event.preventDefault();\n this.focusDay(next);\n }\n\n private focusDay(date: Date) {\n const selector = `[data-date=\"${this.fmtKey(date)}\"]`;\n let btn = this.el.nativeElement.querySelector(selector) as HTMLElement | null;\n if (btn) { btn.focus(); return; }\n\n // Target day isn't currently rendered → page the view toward it, then retry.\n if (this.numberOfMonths > 1) {\n this.viewDate = isBefore(date, this.visibleMonths[0].date)\n ? subMonths(this.viewDate, 1)\n : addMonths(this.viewDate, 1);\n } else {\n this.viewDate = startOfMonth(date);\n }\n this.generateDays();\n this.cdr.detectChanges();\n btn = this.el.nativeElement.querySelector(selector) as HTMLElement | null;\n btn?.focus();\n }\n\n selectMonth(monthIndex: number) {\n this.applyPickerDate(setMonth(this.pickerDate, monthIndex));\n }\n\n selectYear(year: number) {\n this.applyPickerDate(setYear(this.pickerDate, year));\n }\n\n /**\n * Re-anchors the view so the panel the picker was opened from lands on `date`,\n * then returns to the day grid. In the single-month view the offset is 0, so\n * this is just \"show that month\".\n */\n private applyPickerDate(date: Date) {\n this.viewDate = startOfMonth(subMonths(date, this.pickerMonthIndex));\n this.pickerMonthIndex = 0;\n this.currentView = 'date';\n this.generateDays();\n }\n\n // --- Visual Styling for Range ---\n\n isSelected(date: Date): boolean {\n const { start, end } = this.value;\n return !!((start && isSameDay(date, start)) || (end && isSameDay(date, end)));\n }\n\n /** True when `date` belongs to `refMonth` (used to blank out adjacent-month cells). */\n isCurrentMonth(date: Date, refMonth: Date): boolean {\n return isSameMonth(date, refMonth);\n }\n\n /** Stable, timezone-safe `data-date` key used to locate day buttons for focus. */\n fmtKey(date: Date): string {\n return format(date, 'yyyy-MM-dd');\n }\n\n private keyToDate(key: string): Date {\n const [y, m, d] = key.split('-').map(Number);\n return new Date(y, m - 1, d);\n }\n\n getDayClass(date: Date, refMonth: Date = this.viewDate) {\n const { start, end } = this.value;\n const isOutside = !isSameMonth(date, refMonth);\n const isDisabled = this.isDateDisabled(date);\n\n // Range Checks\n const isStart = start && isSameDay(date, start);\n const isEnd = end && isSameDay(date, end);\n const isInside = start && end && isWithinInterval(date, { start, end });\n const isTodayDate = isToday(date);\n\n return cn(\n // Base: h-9 w-9, but we remove margins/rounding for the 'strip' effect\n 'h-9 w-9 p-0 font-normal text-sm transition-all flex items-center justify-center relative z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',\n\n // Default State (Not selected, Not disabled)\n !isInside && !isStart && !isEnd && !isDisabled && 'hover:bg-accent hover:text-accent-foreground rounded-md',\n\n // The \"Caps\": Start and End\n (isStart || isEnd) && 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',\n\n // The \"Strip\": Dates between start and end\n isInside && !isStart && !isEnd && 'bg-accent text-accent-foreground rounded-none',\n\n // Connecting the Caps to the Strip (Rectangular inside edges)\n isStart && end && 'rounded-l-md rounded-r-none',\n isEnd && start && 'rounded-r-md rounded-l-none',\n // If no end date yet, Start should be fully rounded\n isStart && !end && 'rounded-md',\n\n // Muted/Disabled logic\n !isInside && isTodayDate && !isStart && !isEnd && 'bg-accent/50 text-accent-foreground rounded-md',\n (isOutside || isDisabled) && 'text-muted-foreground opacity-50',\n isDisabled && 'cursor-not-allowed'\n );\n }\n\n isDateDisabled(date: Date): boolean {\n return this.disablePastDates ? isBefore(date, startOfDay(new Date())) : false;\n }\n\n // --- CVA Implementation ---\n writeValue(val: DateRange | null): void {\n if (val) {\n this.value = val;\n if (val.start) this.viewDate = val.start;\n this.generateDays();\n this.generateYears();\n }\n }\n registerOnChange(fn: any): void { this.onChange = fn; }\n registerOnTouched(fn: any): void { this.onTouched = fn; }\n}\n"
16
16
  },
17
17
  {
18
18
  "path": "types/date-range.ts",