@truenas/ui-components 0.4.4 → 0.4.5

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,19 +1,19 @@
1
1
  import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
2
2
  import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
4
+ import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, LOCALE_ID, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
5
5
  import * as i1$3 from '@angular/forms';
6
6
  import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
7
- import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
7
+ import { ComponentHarness, HarnessPredicate, TestKey, parallel } from '@angular/cdk/testing';
8
8
  import * as i1 from '@angular/cdk/a11y';
9
- import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
9
+ import { A11yModule, FocusMonitor, _IdGenerator } from '@angular/cdk/a11y';
10
10
  import * as i1$2 from '@angular/common';
11
11
  import { DOCUMENT, NgTemplateOutlet, CommonModule, AsyncPipe, isPlatformBrowser } from '@angular/common';
12
12
  import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose } from '@mdi/js';
13
13
  import * as i1$1 from '@angular/platform-browser';
14
14
  import { DomSanitizer } from '@angular/platform-browser';
15
15
  import { HttpClient } from '@angular/common/http';
16
- import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, Subject, take } from 'rxjs';
16
+ import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, take, Subject } from 'rxjs';
17
17
  import { RouterLink } from '@angular/router';
18
18
  import { filesize } from 'filesize';
19
19
  import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
@@ -13830,43 +13830,251 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13830
13830
  }, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <defs>\n <linearGradient id=\"fadeToPrimary\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n <stop offset=\"0%\" stop-color=\"transparent\" />\n <stop stop-color=\"transparent\" [attr.offset]=\"gradientTransitionStart() + '%'\" />\n <stop offset=\"100%\" [attr.stop-color]=\"progressBarColor()\" />\n </linearGradient>\n </defs>\n\n <!-- Background bar -->\n <rect x=\"50\" fill=\"rgba(0,0,0,0.1)\" [attr.y]=\"height() / 2 - height() / 4\" [attr.width]=\"width() - 100\" [attr.height]=\"height() / 2\" [attr.rx]=\"height() / 4\" />\n\n <!-- Fill bar -->\n <rect\n x=\"50\"\n fill=\"url(#fadeToPrimary)\"\n [attr.y]=\"height() / 2 - height() / 4\"\n [attr.width]=\"fill()\"\n [attr.height]=\"height() / 2\"\n [attr.rx]=\"height() / 4\"\n />\n\n <!-- Particle canvas -->\n <foreignObject x=\"0\" y=\"0\" [attr.width]=\"width()\" [attr.height]=\"height()\">\n <canvas #canvas style=\"pointer-events: none;\" [attr.width]=\"width()\" [attr.height]=\"height()\"></canvas>\n </foreignObject>\n</svg>\n", styles: [":host{display:block}\n"] }]
13831
13831
  }], propDecorators: { speed: [{ type: i0.Input, args: [{ isSignal: true, alias: "speed", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], canvasRef: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }] } });
13832
13832
 
13833
+ /**
13834
+ * Day-granularity date helpers shared by the calendar views.
13835
+ *
13836
+ * The calendar deals in calendar days, but the dates a caller binds are ordinary
13837
+ * `Date`s and routinely carry a time of day — `new Date()` for a default, or a value
13838
+ * round-tripped through an API. Comparing those against the midnight dates the grid
13839
+ * builds gives answers that are off by up to a day, so every comparison the calendar
13840
+ * makes goes through here.
13841
+ */
13842
+ /**
13843
+ * Orders two dates by calendar day: negative when `a` falls on an earlier day than `b`,
13844
+ * zero on the same day, positive when later. Time of day is ignored on both sides.
13845
+ */
13846
+ function compareDays(a, b) {
13847
+ return (a.getFullYear() - b.getFullYear()
13848
+ || a.getMonth() - b.getMonth()
13849
+ || a.getDate() - b.getDate());
13850
+ }
13851
+ /** Whether two dates fall on the same calendar day, ignoring time of day. */
13852
+ function isSameDay(a, b) {
13853
+ return compareDays(a, b) === 0;
13854
+ }
13855
+ /** Stable per-day key, for lookups and change tracking. */
13856
+ function dateKey(date) {
13857
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
13858
+ }
13859
+ /**
13860
+ * `YYYY-MM-DD` for the day this date falls on, built from its local parts.
13861
+ *
13862
+ * Rendered onto each day cell as `data-tn-date`, which gives tests a handle on a day
13863
+ * that survives translation — cell text is formatted for the locale, right down to the
13864
+ * numerals, so matching on it only works in English.
13865
+ *
13866
+ * Deliberately not `toISOString()`: that converts to UTC first, so anywhere east or west
13867
+ * of Greenwich it can name the day before or after the one shown.
13868
+ */
13869
+ function isoDateString(date) {
13870
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
13871
+ const day = `${date.getDate()}`.padStart(2, '0');
13872
+ return `${date.getFullYear()}-${month}-${day}`;
13873
+ }
13874
+ /**
13875
+ * Adds whole months, clamping to the last day when the target month is shorter, so
13876
+ * navigating never silently lands in the month after the one asked for. `new Date` rolls
13877
+ * 31 January + 1 month over into early March; this returns 28 (or 29) February.
13878
+ */
13879
+ function addMonths(date, delta) {
13880
+ return atMonthAndDay(date, date.getFullYear(), date.getMonth() + delta);
13881
+ }
13882
+ /**
13883
+ * The same month and day in another year, clamped the same way — 29 February in a
13884
+ * non-leap year is 28 February, not 1 March. Without the clamp, paging the year grid off
13885
+ * a leap day quietly changes the month as well as the year.
13886
+ */
13887
+ function withYear(date, year) {
13888
+ return atMonthAndDay(date, year, date.getMonth());
13889
+ }
13890
+ /** Keeps `date`'s day-of-month where the target month is long enough to hold it. */
13891
+ function atMonthAndDay(date, year, month) {
13892
+ const lastDay = new Date(year, month + 1, 0).getDate();
13893
+ return new Date(year, month, Math.min(date.getDate(), lastDay));
13894
+ }
13895
+ /** How many years one page of the year grid shows. */
13896
+ const YEARS_PER_PAGE = 24;
13897
+ /**
13898
+ * The first year of the page containing `year`.
13899
+ *
13900
+ * Pages are fixed blocks counted from year 0, not windows centred on the year asked
13901
+ * about — 2020 sits near the top of 2016–2039 rather than in the middle of 2008–2031.
13902
+ * The year view and the header both need this answer and must agree on it, or the
13903
+ * header labels a span the grid isn't showing.
13904
+ */
13905
+ function yearPageStart(year) {
13906
+ return Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
13907
+ }
13908
+ /**
13909
+ * The day a week starts on in the given locale, as a `Date.getDay()` index — 0 for
13910
+ * Sunday through 6 for Saturday. Sunday in the US, Monday across most of Europe,
13911
+ * Saturday in much of the Middle East.
13912
+ *
13913
+ * Read from `Intl`, which reports it ISO-style (1 Monday … 7 Sunday). Not every engine
13914
+ * exposes it — Firefox notably lagged — so anything that cannot answer falls back to
13915
+ * Sunday, which is what the grid did for every locale before.
13916
+ */
13917
+ function firstDayOfWeek(locale) {
13918
+ try {
13919
+ const info = new Intl.Locale(locale);
13920
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay;
13921
+ // ISO counts Sunday as 7; `Date.getDay()` counts it as 0.
13922
+ return firstDay === undefined ? 0 : firstDay % 7;
13923
+ }
13924
+ catch {
13925
+ // A malformed locale tag shouldn't take the calendar down with it.
13926
+ return 0;
13927
+ }
13928
+ }
13929
+
13930
+ /** The cell holding the grid's roving tabindex — the one cell focus belongs on. */
13931
+ const ACTIVE_CELL = '.tn-calendar-body-cell[tabindex="0"]';
13932
+ /**
13933
+ * Follows the roving tabindex with real focus, once the grid it names has rendered.
13934
+ *
13935
+ * Two situations need it, and both leave focus somewhere wrong until it runs. Arrowing
13936
+ * within a grid re-renders cells that persist as elements, so the browser keeps focus on
13937
+ * the cell just left. Switching between the day and year grids destroys the cell that
13938
+ * was focused outright, which drops focus to `<body>` and restarts the tab order.
13939
+ *
13940
+ * Tied to the render rather than a zero timeout: the target cell doesn't exist until the
13941
+ * new grid is painted.
13942
+ *
13943
+ * Does nothing when no cell can take focus — a month or page with every day disabled has
13944
+ * nothing worth focusing.
13945
+ */
13946
+ function focusActiveCellAfterRender(host, injector) {
13947
+ afterNextRender(() => {
13948
+ host.nativeElement.querySelector(ACTIVE_CELL)?.focus();
13949
+ }, { injector });
13950
+ }
13951
+
13952
+ /** The wording used when an app provides none of its own. */
13953
+ const TN_CALENDAR_INTL_DEFAULTS = {
13954
+ marked: '(marked)',
13955
+ rangeStart: '(range start)',
13956
+ rangeEnd: '(range end)',
13957
+ inRange: '(in range)',
13958
+ currentYear: '(current year)',
13959
+ monthGridLabel: (period) => period,
13960
+ yearGridLabel: (startYear, endYear) => `Years ${startYear} to ${endYear}`,
13961
+ chooseMonthAndYear: 'Choose month and year',
13962
+ previousMonth: 'Previous month',
13963
+ nextMonth: 'Next month',
13964
+ previousYears: `Previous ${YEARS_PER_PAGE} years`,
13965
+ nextYears: `Next ${YEARS_PER_PAGE} years`,
13966
+ };
13967
+ /**
13968
+ * Injection token for the calendar's wording.
13969
+ *
13970
+ * Because the library ships no localized strings, this is the recommended hook for
13971
+ * wiring a translation service so every `tn-calendar` reads the same way. Anything left
13972
+ * out falls back to {@link TN_CALENDAR_INTL_DEFAULTS}, so an app can translate one
13973
+ * string without restating the rest.
13974
+ *
13975
+ * Dates themselves are not here — they follow `LOCALE_ID`, or the calendar's `locale`
13976
+ * input. See `TnCalendarComponent.locale`.
13977
+ *
13978
+ * @example
13979
+ * ```ts
13980
+ * // Fixed for the lifetime of the app — the language is chosen at build or bootstrap.
13981
+ * providers: [
13982
+ * {
13983
+ * provide: TN_CALENDAR_INTL,
13984
+ * useFactory: (translate: TranslateService): TnCalendarIntlInput => ({
13985
+ * marked: translate.instant('calendar.marked'),
13986
+ * previousMonth: translate.instant('calendar.previousMonth'),
13987
+ * yearGridLabel: (from, to) => translate.instant('calendar.years', { from, to }),
13988
+ * }),
13989
+ * deps: [TranslateService],
13990
+ * },
13991
+ * ];
13992
+ * ```
13993
+ *
13994
+ * @example
13995
+ * ```ts
13996
+ * // Switchable at runtime: a signal, so calendars already on screen follow along.
13997
+ * providers: [
13998
+ * {
13999
+ * provide: TN_CALENDAR_INTL,
14000
+ * useFactory: (): TnCalendarIntlInput => {
14001
+ * const translate = inject(TranslateService);
14002
+ * const lang = toSignal(translate.onLangChange, { initialValue: null });
14003
+ * return computed(() => {
14004
+ * lang(); // Re-read every string when the language changes.
14005
+ * return { marked: translate.instant('calendar.marked') };
14006
+ * });
14007
+ * },
14008
+ * },
14009
+ * ];
14010
+ * ```
14011
+ */
14012
+ const TN_CALENDAR_INTL = new InjectionToken('TN_CALENDAR_INTL');
14013
+ /**
14014
+ * Resolves the wording for a calendar component: whatever the app provided, laid over
14015
+ * the built-in defaults.
14016
+ *
14017
+ * Returns a signal rather than a plain object so a language switch reaches calendars that
14018
+ * are already on screen. Read it inside a `computed` — as every caller here does — and
14019
+ * the labels recompute on their own.
14020
+ */
14021
+ function injectTnCalendarIntl() {
14022
+ const provided = inject(TN_CALENDAR_INTL, { optional: true });
14023
+ return computed(() => {
14024
+ // Signals are functions; a plain object of wording is not.
14025
+ const wording = typeof provided === 'function' ? provided() : provided;
14026
+ return { ...TN_CALENDAR_INTL_DEFAULTS, ...(wording ?? {}) };
14027
+ });
14028
+ }
14029
+
13833
14030
  class TnCalendarHeaderComponent {
13834
14031
  currentDate = input(new Date(), ...(ngDevMode ? [{ debugName: "currentDate" }] : []));
13835
14032
  currentView = input('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
13836
- monthSelected = output();
13837
- yearSelected = output();
14033
+ /**
14034
+ * Locale for the month abbreviation. Falls back to the app's `LOCALE_ID`;
14035
+ * `tn-calendar` binds it from its own `locale` input.
14036
+ */
14037
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
13838
14038
  viewChanged = output();
13839
14039
  previousClicked = output();
13840
14040
  nextClicked = output();
13841
- months = [
13842
- 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
13843
- 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'
13844
- ];
13845
- periodLabelId = `tn-calendar-period-label-${Math.floor(Math.random() * 10000)}`;
14041
+ appLocale = inject(LOCALE_ID);
14042
+ intl = injectTnCalendarIntl();
14043
+ resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
14044
+ /**
14045
+ * Ties the period button to its live region through `aria-describedby`. Counted rather
14046
+ * than drawn at random: two calendars on one page — a start and an end picker, say —
14047
+ * had a real chance of minting the same id out of only 10,000, and a duplicate id
14048
+ * silently unhooks the association with nothing to show for it.
14049
+ */
14050
+ periodLabelId = inject(_IdGenerator).getId('tn-calendar-period-label-');
13846
14051
  periodLabel = computed(() => {
13847
14052
  const date = this.currentDate();
13848
14053
  if (!date) {
13849
14054
  return '';
13850
14055
  }
14056
+ const locale = this.resolvedLocale();
13851
14057
  if (this.currentView() === 'month') {
13852
- const month = this.months[date.getMonth()];
13853
- const year = date.getFullYear();
13854
- return `${month} ${year}`;
14058
+ // Formatted as one unit so the locale decides the order and the numerals, not a
14059
+ // template string. Upper-cased after the fact: no locale offers a short month name
14060
+ // already capitalised the way this header wants it.
14061
+ return date.toLocaleDateString(locale, { month: 'short', year: 'numeric' }).toUpperCase();
13855
14062
  }
13856
14063
  else {
13857
- // For year view, show the year range (24 years like Material)
13858
- const currentYear = date.getFullYear();
13859
- const startYear = Math.floor(currentYear / 24) * 24;
13860
- const endYear = startYear + 23;
13861
- return `${startYear} – ${endYear}`;
14064
+ // The span the year grid is showing. Shared with the view so the two cannot
14065
+ // disagree about which page is on screen.
14066
+ const startYear = yearPageStart(date.getFullYear());
14067
+ const yearFormat = new Intl.NumberFormat(locale, { useGrouping: false });
14068
+ return `${yearFormat.format(startYear)} – ${yearFormat.format(startYear + YEARS_PER_PAGE - 1)}`;
13862
14069
  }
13863
14070
  }, ...(ngDevMode ? [{ debugName: "periodLabel" }] : []));
13864
14071
  previousLabel = computed(() => {
13865
- return this.currentView() === 'month' ? 'Previous month' : 'Previous 24 years';
14072
+ return this.currentView() === 'month' ? this.intl().previousMonth : this.intl().previousYears;
13866
14073
  }, ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
13867
14074
  nextLabel = computed(() => {
13868
- return this.currentView() === 'month' ? 'Next month' : 'Next 24 years';
14075
+ return this.currentView() === 'month' ? this.intl().nextMonth : this.intl().nextYears;
13869
14076
  }, ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
14077
+ periodButtonLabel = computed(() => this.intl().chooseMonthAndYear, ...(ngDevMode ? [{ debugName: "periodButtonLabel" }] : []));
13870
14078
  toggleView() {
13871
14079
  const newView = this.currentView() === 'month' ? 'year' : 'month';
13872
14080
  this.viewChanged.emit(newView);
@@ -13878,12 +14086,12 @@ class TnCalendarHeaderComponent {
13878
14086
  this.nextClicked.emit();
13879
14087
  }
13880
14088
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13881
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnCalendarHeaderComponent, isStandalone: true, selector: "tn-calendar-header", inputs: { currentDate: { classPropertyName: "currentDate", publicName: "currentDate", isSignal: true, isRequired: false, transformFunction: null }, currentView: { classPropertyName: "currentView", publicName: "currentView", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { monthSelected: "monthSelected", yearSelected: "yearSelected", viewChanged: "viewChanged", previousClicked: "previousClicked", nextClicked: "nextClicked" }, ngImport: i0, template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"'Choose month and year'\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] });
14089
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnCalendarHeaderComponent, isStandalone: true, selector: "tn-calendar-header", inputs: { currentDate: { classPropertyName: "currentDate", publicName: "currentDate", isSignal: true, isRequired: false, transformFunction: null }, currentView: { classPropertyName: "currentView", publicName: "currentView", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChanged: "viewChanged", previousClicked: "previousClicked", nextClicked: "nextClicked" }, ngImport: i0, template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"periodButtonLabel()\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] });
13882
14090
  }
13883
14091
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, decorators: [{
13884
14092
  type: Component,
13885
- args: [{ selector: 'tn-calendar-header', standalone: true, imports: [], template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"'Choose month and year'\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] }]
13886
- }], propDecorators: { currentDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentDate", required: false }] }], currentView: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentView", required: false }] }], monthSelected: [{ type: i0.Output, args: ["monthSelected"] }], yearSelected: [{ type: i0.Output, args: ["yearSelected"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], previousClicked: [{ type: i0.Output, args: ["previousClicked"] }], nextClicked: [{ type: i0.Output, args: ["nextClicked"] }] } });
14093
+ args: [{ selector: 'tn-calendar-header', standalone: true, imports: [], template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"periodButtonLabel()\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] }]
14094
+ }], propDecorators: { currentDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentDate", required: false }] }], currentView: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentView", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], previousClicked: [{ type: i0.Output, args: ["previousClicked"] }], nextClicked: [{ type: i0.Output, args: ["nextClicked"] }] } });
13887
14095
 
13888
14096
  class TnMonthViewComponent {
13889
14097
  activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
@@ -13891,25 +14099,114 @@ class TnMonthViewComponent {
13891
14099
  minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
13892
14100
  maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
13893
14101
  dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
14102
+ /**
14103
+ * Dates to flag as noteworthy — days a task runs, days with events, and the like.
14104
+ * Order and time-of-day are ignored; only the calendar day is compared. The
14105
+ * calendar owns how a marked day looks, so callers pass dates rather than styles.
14106
+ */
14107
+ markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
13894
14108
  // Range mode inputs
13895
14109
  rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
13896
14110
  selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
13897
14111
  selectedChange = output();
13898
14112
  activeDateChange = output();
13899
- weekdays = [
13900
- { long: 'Sunday', short: 'S' },
13901
- { long: 'Monday', short: 'M' },
13902
- { long: 'Tuesday', short: 'T' },
13903
- { long: 'Wednesday', short: 'W' },
13904
- { long: 'Thursday', short: 'T' },
13905
- { long: 'Friday', short: 'F' },
13906
- { long: 'Saturday', short: 'S' },
13907
- ];
14113
+ /**
14114
+ * Locale for dates, weekday names and the first day of the week. Falls back to the
14115
+ * app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
14116
+ */
14117
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
14118
+ host = inject(ElementRef);
14119
+ injector = inject(Injector);
14120
+ appLocale = inject(LOCALE_ID);
14121
+ intl = injectTnCalendarIntl();
14122
+ resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
14123
+ /**
14124
+ * Which day the week starts on in this locale, as a `Date.getDay()` index. Everything
14125
+ * about the grid's shape hangs off it: the order of the headings and how many blank
14126
+ * cells precede the 1st.
14127
+ */
14128
+ weekStart = computed(() => firstDayOfWeek(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "weekStart" }] : []));
14129
+ /** Day numbers in the locale's own numerals, without a thousands separator. */
14130
+ dayFormat = computed(() => {
14131
+ return new Intl.NumberFormat(this.resolvedLocale(), { useGrouping: false });
14132
+ }, ...(ngDevMode ? [{ debugName: "dayFormat" }] : []));
14133
+ /**
14134
+ * Column headings, named and ordered by the locale — Sunday first in the US, Monday
14135
+ * across most of Europe. The dates are an arbitrary week, used only to name the days.
14136
+ */
14137
+ weekdays = computed(() => {
14138
+ const locale = this.resolvedLocale();
14139
+ const long = new Intl.DateTimeFormat(locale, { weekday: 'long' });
14140
+ const narrow = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
14141
+ const weekStart = this.weekStart();
14142
+ return Array.from({ length: 7 }, (_, column) => {
14143
+ // 7 Jan 2024 was a Sunday, so adding a getDay() index lands on that weekday.
14144
+ const date = new Date(2024, 0, 7 + ((weekStart + column) % 7));
14145
+ return { long: long.format(date), short: narrow.format(date) };
14146
+ });
14147
+ }, ...(ngDevMode ? [{ debugName: "weekdays" }] : []));
13908
14148
  // Cell sizing now controlled via CSS custom properties in the SCSS file
14149
+ markedDateKeys = computed(() => {
14150
+ return new Set((this.markedDates() ?? []).map((date) => dateKey(date)));
14151
+ }, ...(ngDevMode ? [{ debugName: "markedDateKeys" }] : []));
14152
+ /**
14153
+ * Names the grid after the month it shows. Without it a screen-reader user arrowing
14154
+ * around hears each day but never which month they are in — the period only appears in
14155
+ * the header, outside the grid.
14156
+ */
14157
+ gridLabel = computed(() => {
14158
+ const period = this.activeDate()
14159
+ .toLocaleDateString(this.resolvedLocale(), { month: 'long', year: 'numeric' });
14160
+ return this.intl().monthGridLabel(period);
14161
+ }, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
14162
+ /**
14163
+ * The day of the month that carries `tabindex="0"` — the grid's single keyboard entry
14164
+ * point, per the roving tabindex pattern.
14165
+ *
14166
+ * It follows `activeDate`, but falls back to the nearest enabled day when that day
14167
+ * can't take focus. Without the fallback a month whose active day is disabled (say,
14168
+ * anything before `minDate`) would have no tabbable cell at all, leaving the grid
14169
+ * unreachable by keyboard. `null` only when the whole month is disabled, where there
14170
+ * is nothing worth focusing anyway.
14171
+ *
14172
+ * Derived from the month rather than from `calendarRows()`, which would be circular.
14173
+ */
14174
+ activeDay = computed(() => {
14175
+ const activeDate = this.activeDate();
14176
+ if (!activeDate) {
14177
+ return null;
14178
+ }
14179
+ const year = activeDate.getFullYear();
14180
+ const month = activeDate.getMonth();
14181
+ const lastDay = new Date(year, month + 1, 0).getDate();
14182
+ const isEnabled = (day) => this.isDateEnabled(new Date(year, month, day));
14183
+ // Always within the rendered month: both come off the same Date, and `Date` has
14184
+ // already rolled over any overflow the caller handed us.
14185
+ const wanted = activeDate.getDate();
14186
+ if (isEnabled(wanted)) {
14187
+ return wanted;
14188
+ }
14189
+ // Spiral outwards from the day we wanted, forward first.
14190
+ for (let offset = 1; offset < lastDay; offset++) {
14191
+ if (wanted + offset <= lastDay && isEnabled(wanted + offset)) {
14192
+ return wanted + offset;
14193
+ }
14194
+ if (wanted - offset >= 1 && isEnabled(wanted - offset)) {
14195
+ return wanted - offset;
14196
+ }
14197
+ }
14198
+ return null;
14199
+ }, ...(ngDevMode ? [{ debugName: "activeDay" }] : []));
14200
+ activeCellDate = computed(() => {
14201
+ const day = this.activeDay();
14202
+ if (day === null) {
14203
+ return null;
14204
+ }
14205
+ const activeDate = this.activeDate();
14206
+ return new Date(activeDate.getFullYear(), activeDate.getMonth(), day);
14207
+ }, ...(ngDevMode ? [{ debugName: "activeCellDate" }] : []));
13909
14208
  calendarRows = computed(() => {
13910
14209
  const activeDate = this.activeDate();
13911
- // Track selectedRange signal so computed recalculates when range changes
13912
- this.selectedRange();
13913
14210
  if (!activeDate) {
13914
14211
  return [];
13915
14212
  }
@@ -13917,7 +14214,13 @@ class TnMonthViewComponent {
13917
14214
  const month = activeDate.getMonth();
13918
14215
  const firstDate = new Date(year, month, 1);
13919
14216
  const lastDate = new Date(year, month + 1, 0);
13920
- const startDayOfWeek = firstDate.getDay(); // 0 = Sunday, 1 = Monday, etc.
14217
+ // How far into the week the 1st falls, counted from wherever this locale's week
14218
+ // begins — that many blanks lead the grid.
14219
+ const startDayOfWeek = (firstDate.getDay() - this.weekStart() + 7) % 7;
14220
+ // Read once for the whole grid rather than per cell, so every day is measured
14221
+ // against the same instant as well as allocating a fortieth as much.
14222
+ const today = new Date();
14223
+ const dayFormat = this.dayFormat();
13921
14224
  const rows = [];
13922
14225
  let currentRow = [];
13923
14226
  // Add empty cells for days before month starts
@@ -13927,7 +14230,7 @@ class TnMonthViewComponent {
13927
14230
  // Add all days of the month
13928
14231
  for (let day = 1; day <= lastDate.getDate(); day++) {
13929
14232
  const date = new Date(year, month, day);
13930
- currentRow.push(this.createCell(date, day));
14233
+ currentRow.push(this.createCell(date, day, today, dayFormat));
13931
14234
  // If we have 7 cells, complete the row
13932
14235
  if (currentRow.length === 7) {
13933
14236
  rows.push(currentRow);
@@ -13943,10 +14246,10 @@ class TnMonthViewComponent {
13943
14246
  }
13944
14247
  return rows;
13945
14248
  }, ...(ngDevMode ? [{ debugName: "calendarRows" }] : []));
13946
- createCell(date, value) {
13947
- const today = new Date();
13948
- const isToday = this.isSameDate(date, today);
13949
- const isSelected = this.selected() ? this.isSameDate(date, this.selected()) : false;
14249
+ createCell(date, value, today, dayFormat) {
14250
+ const isToday = isSameDay(date, today);
14251
+ const isSelected = this.selected() ? isSameDay(date, this.selected()) : false;
14252
+ const isMarked = this.markedDateKeys().has(dateKey(date));
13950
14253
  const enabled = this.isDateEnabled(date);
13951
14254
  // Range mode calculations
13952
14255
  let rangeStart = false;
@@ -13955,24 +14258,36 @@ class TnMonthViewComponent {
13955
14258
  const currentRange = this.selectedRange();
13956
14259
  if (this.rangeMode() && currentRange) {
13957
14260
  const { start, end } = currentRange;
13958
- if (start && this.isSameDate(date, start)) {
14261
+ if (start && isSameDay(date, start)) {
13959
14262
  rangeStart = true;
13960
14263
  }
13961
- if (end && this.isSameDate(date, end)) {
14264
+ if (end && isSameDay(date, end)) {
13962
14265
  rangeEnd = true;
13963
14266
  }
13964
- if (start && end && date > start && date < end) {
14267
+ // Strictly between the ends. Compared by day so a start or end carrying a time of
14268
+ // day can't come out as both a cap and an in-between day.
14269
+ if (start && end && compareDays(date, start) > 0 && compareDays(date, end) < 0) {
13965
14270
  inRange = true;
13966
14271
  }
13967
14272
  }
14273
+ // Selection and the range caps share one solid fill; marking sits below them and
14274
+ // shows only where neither applies. A cap is a selection too, so `today` must not
14275
+ // paint its outline over it.
14276
+ const isPrimary = isSelected || rangeStart || rangeEnd;
14277
+ const fill = isPrimary ? 'primary' : isMarked ? 'marked' : 'none';
13968
14278
  return {
13969
14279
  value,
13970
14280
  date: new Date(date),
13971
- label: date.getDate().toString(),
13972
- ariaLabel: this.formatAriaLabel(date, isSelected, isToday, rangeStart, rangeEnd, inRange),
14281
+ isoDate: isoDateString(date),
14282
+ label: dayFormat.format(value),
14283
+ ariaLabel: this.formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange),
13973
14284
  enabled,
13974
14285
  selected: isSelected,
13975
14286
  today: isToday,
14287
+ marked: isMarked,
14288
+ inSelection: isSelected || rangeStart || rangeEnd || inRange,
14289
+ fill,
14290
+ todayOutline: isToday && !isPrimary,
13976
14291
  rangeStart,
13977
14292
  rangeEnd,
13978
14293
  inRange,
@@ -13982,21 +14297,28 @@ class TnMonthViewComponent {
13982
14297
  return {
13983
14298
  value: 0,
13984
14299
  date: new Date(),
14300
+ isoDate: '',
13985
14301
  label: '',
13986
14302
  ariaLabel: '',
13987
14303
  enabled: false,
13988
14304
  selected: false,
13989
14305
  today: false,
14306
+ marked: false,
14307
+ inSelection: false,
14308
+ fill: 'none',
14309
+ todayOutline: false,
13990
14310
  };
13991
14311
  }
13992
14312
  isDateEnabled(date) {
13993
14313
  const minDate = this.minDate();
13994
14314
  const maxDate = this.maxDate();
13995
14315
  const dateFilter = this.dateFilter();
13996
- if (minDate && date < minDate) {
14316
+ // Bounds are inclusive of their own day: a `minDate` of "today, 14:30" means today
14317
+ // is selectable, not that today is half past the deadline.
14318
+ if (minDate && compareDays(date, minDate) < 0) {
13997
14319
  return false;
13998
14320
  }
13999
- if (maxDate && date > maxDate) {
14321
+ if (maxDate && compareDays(date, maxDate) > 0) {
14000
14322
  return false;
14001
14323
  }
14002
14324
  if (dateFilter && !dateFilter(date)) {
@@ -14004,32 +14326,31 @@ class TnMonthViewComponent {
14004
14326
  }
14005
14327
  return true;
14006
14328
  }
14007
- isSameDate(date1, date2) {
14008
- return date1.getFullYear() === date2.getFullYear() &&
14009
- date1.getMonth() === date2.getMonth() &&
14010
- date1.getDate() === date2.getDate();
14011
- }
14012
- formatAriaLabel(date, isSelected, isToday, rangeStart, rangeEnd, inRange) {
14013
- let label = date.toLocaleDateString('en', {
14329
+ /**
14330
+ * Spells out the state a day is in, but only the parts no ARIA attribute already
14331
+ * carries. Selection lives on the button's `aria-pressed` and today on `aria-current`,
14332
+ * so repeating them here had screen readers announce each one twice. Marking and the
14333
+ * range positions have no attribute equivalent, and would otherwise be conveyed by
14334
+ * background colour alone.
14335
+ */
14336
+ formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange) {
14337
+ let label = date.toLocaleDateString(this.resolvedLocale(), {
14014
14338
  weekday: 'long',
14015
14339
  year: 'numeric',
14016
14340
  month: 'long',
14017
14341
  day: 'numeric'
14018
14342
  });
14019
- if (isSelected) {
14020
- label += ' (selected)';
14021
- }
14022
- if (isToday) {
14023
- label += ' (today)';
14343
+ if (isMarked) {
14344
+ label += ` ${this.intl().marked}`;
14024
14345
  }
14025
14346
  if (rangeStart) {
14026
- label += ' (range start)';
14347
+ label += ` ${this.intl().rangeStart}`;
14027
14348
  }
14028
14349
  if (rangeEnd) {
14029
- label += ' (range end)';
14350
+ label += ` ${this.intl().rangeEnd}`;
14030
14351
  }
14031
14352
  if (inRange) {
14032
- label += ' (in range)';
14353
+ label += ` ${this.intl().inRange}`;
14033
14354
  }
14034
14355
  return label;
14035
14356
  }
@@ -14037,23 +14358,107 @@ class TnMonthViewComponent {
14037
14358
  if (cell.value === 0) {
14038
14359
  return `empty-${index}`;
14039
14360
  }
14040
- return `${cell.date.getFullYear()}-${cell.date.getMonth()}-${cell.date.getDate()}`;
14361
+ return dateKey(cell.date);
14041
14362
  }
14042
14363
  trackByRow(index) {
14043
14364
  return index;
14044
14365
  }
14045
14366
  onCellClicked(cell) {
14046
- if (cell.enabled && cell.value > 0) {
14047
- this.selectedChange.emit(cell.date);
14367
+ if (!cell.enabled || cell.value === 0) {
14368
+ return;
14369
+ }
14370
+ // The click already moved browser focus here, so the roving tabindex has to come
14371
+ // along or the two diverge: arrowing on from a clicked day would carry on from
14372
+ // wherever the active day happened to be, jumping the focus somewhere unrelated.
14373
+ this.activeDateChange.emit(cell.date);
14374
+ this.selectedChange.emit(cell.date);
14375
+ }
14376
+ /**
14377
+ * Moves the roving tabindex. Selection stays on click/Enter/Space, which the cells
14378
+ * are already buttons for, so this only ever changes which day is active.
14379
+ *
14380
+ * Moves that leave the displayed month emit an `activeDateChange` like any other
14381
+ * navigation; the grid re-renders on the new month with the target day active.
14382
+ */
14383
+ onKeydown(event) {
14384
+ const from = this.activeCellDate();
14385
+ if (!from) {
14386
+ return;
14387
+ }
14388
+ const move = this.targetForKey(event, from);
14389
+ if (!move) {
14390
+ return;
14391
+ }
14392
+ // Claim the key before the browser scrolls the page with it.
14393
+ event.preventDefault();
14394
+ const landing = this.nearestEnabled(move.date, move.search);
14395
+ if (!landing || isSameDay(landing, from)) {
14396
+ return;
14397
+ }
14398
+ this.activeDateChange.emit(landing);
14399
+ focusActiveCellAfterRender(this.host, this.injector);
14400
+ }
14401
+ /**
14402
+ * The day a key aims at, and which way to look from there when that day is disabled.
14403
+ *
14404
+ * The search direction belongs to the key, not to where the target happens to fall
14405
+ * relative to the current day. Home and End mean "the ends of *this* month", so they
14406
+ * search inward: looking outward would step straight into the neighbouring month over
14407
+ * a single filtered-out boundary day and page the view, which is not what either key
14408
+ * promises. The arrows and the paging keys keep travelling the way they were already
14409
+ * headed, and crossing into the next month is the point of those.
14410
+ */
14411
+ targetForKey(event, from) {
14412
+ const year = from.getFullYear();
14413
+ const month = from.getMonth();
14414
+ const day = from.getDate();
14415
+ // Overshooting a month's length is fine — the Date constructor rolls it over, which
14416
+ // is exactly what arrowing off the end of a month should do.
14417
+ switch (event.key) {
14418
+ case 'ArrowLeft': return { date: new Date(year, month, day - 1), search: -1 };
14419
+ case 'ArrowRight': return { date: new Date(year, month, day + 1), search: 1 };
14420
+ case 'ArrowUp': return { date: new Date(year, month, day - 7), search: -1 };
14421
+ case 'ArrowDown': return { date: new Date(year, month, day + 7), search: 1 };
14422
+ case 'Home': return { date: new Date(year, month, 1), search: 1 };
14423
+ case 'End': return { date: new Date(year, month + 1, 0), search: -1 };
14424
+ // Shift pages by a year, matching Material and the wider grid convention.
14425
+ case 'PageUp': return { date: addMonths(from, event.shiftKey ? -12 : -1), search: -1 };
14426
+ case 'PageDown': return { date: addMonths(from, event.shiftKey ? 12 : 1), search: 1 };
14427
+ default: return null;
14428
+ }
14429
+ }
14430
+ /**
14431
+ * Steps past days the caller has disabled, so a gap in `dateFilter` or the far side of
14432
+ * `minDate`/`maxDate` doesn't park focus somewhere unusable.
14433
+ *
14434
+ * Searches the way the key asked for (see `targetForKey`), then doubles back if that
14435
+ * finds nothing. The fallback is for the ends of the allowed range: arrowing right at
14436
+ * `maxDate` finds nothing ahead, doubles back onto the day we started from, and the
14437
+ * caller reads that as "don't move".
14438
+ */
14439
+ nearestEnabled(target, preferred) {
14440
+ return this.scanForEnabled(target, preferred)
14441
+ ?? this.scanForEnabled(target, preferred === 1 ? -1 : 1);
14442
+ }
14443
+ /** Bounded, so an everything-disabled filter stops the move instead of spinning. */
14444
+ scanForEnabled(from, direction) {
14445
+ const maxSteps = 62;
14446
+ const candidate = new Date(from);
14447
+ for (let step = 0; step <= maxSteps; step++) {
14448
+ if (this.isDateEnabled(candidate)) {
14449
+ return new Date(candidate);
14450
+ }
14451
+ candidate.setDate(candidate.getDate() + direction);
14048
14452
  }
14453
+ return null;
14049
14454
  }
14050
14455
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14051
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMonthViewComponent, isStandalone: true, selector: "tn-month-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays; track day) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}.tn-calendar-table{width:calc(7 * var(--calendar-cell-size));border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;margin:auto;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333);width:var(--calendar-cell-size);height:var(--calendar-cell-size)}.tn-calendar-body-cell:not(:disabled):hover:not(.tn-calendar-body-selected):not(.tn-calendar-body-range-start):not(.tn-calendar-body-range-end):not(.tn-calendar-body-in-range){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-calendar-body-cell:focus .tn-calendar-body-cell-content.tn-focus-indicator{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected,.tn-calendar-body-cell.tn-calendar-body-range-start,.tn-calendar-body-cell.tn-calendar-body-range-end,.tn-calendar-body-cell.tn-calendar-body-in-range{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;inset:0;background:transparent}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}\n"] });
14456
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMonthViewComponent, isStandalone: true, selector: "tn-month-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, markedDates: { classPropertyName: "markedDates", publicName: "markedDates", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays(); track day.long) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-date]=\"cell.isoDate\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-marked]=\"cell.marked\"\n [class.tn-calendar-body-active]=\"cell.value === activeDay()\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.value === activeDay() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.inSelection\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\"\n [class.tn-calendar-body-fill-primary]=\"cell.fill === 'primary'\"\n [class.tn-calendar-body-fill-marked]=\"cell.fill === 'marked'\"\n [class.tn-calendar-body-today-outline]=\"cell.todayOutline\">\n {{ cell.label }}\n </span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{display:block;--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px;--calendar-indicator-size: var(--calendar-cell-size);--calendar-radius: 4px}.tn-calendar-table{width:100%;min-width:min(100%,7 * var(--calendar-cell-size));table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header th:first-child{border-start-start-radius:var(--calendar-radius);border-end-start-radius:var(--calendar-radius)}.tn-calendar-table-header th:last-child{border-start-end-radius:var(--calendar-radius);border-end-end-radius:var(--calendar-radius)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333)}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-in-range:before,.tn-calendar-body-cell.tn-calendar-body-range-start:before,.tn-calendar-body-cell.tn-calendar-body-range-end:before{content:\"\";position:absolute;top:50%;transform:translateY(-50%);height:var(--calendar-indicator-size);background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-in-range:before{inset-inline:0}.tn-calendar-body-cell.tn-calendar-body-range-start:before{inset-inline-start:50%;inset-inline-end:0}.tn-calendar-body-cell.tn-calendar-body-range-end:before{inset-inline-start:0;inset-inline-end:50%}.tn-calendar-body-cell.tn-calendar-body-range-start.tn-calendar-body-range-end:before{content:none}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-indicator-size);height:var(--calendar-indicator-size);max-width:100%;border-radius:var(--calendar-radius);font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):hover .tn-calendar-body-cell-content:not(.tn-calendar-body-fill-primary){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell-content.tn-calendar-body-fill-marked{background:var(--tn-alt-bg1, #f0f0f0)}.tn-calendar-body-cell-content.tn-calendar-body-fill-primary{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}.tn-calendar-body-cell-content.tn-calendar-body-today-outline{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] });
14052
14457
  }
14053
14458
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, decorators: [{
14054
14459
  type: Component,
14055
- args: [{ selector: 'tn-month-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays; track day) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}.tn-calendar-table{width:calc(7 * var(--calendar-cell-size));border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;margin:auto;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333);width:var(--calendar-cell-size);height:var(--calendar-cell-size)}.tn-calendar-body-cell:not(:disabled):hover:not(.tn-calendar-body-selected):not(.tn-calendar-body-range-start):not(.tn-calendar-body-range-end):not(.tn-calendar-body-in-range){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-calendar-body-cell:focus .tn-calendar-body-cell-content.tn-focus-indicator{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected,.tn-calendar-body-cell.tn-calendar-body-range-start,.tn-calendar-body-cell.tn-calendar-body-range-end,.tn-calendar-body-cell.tn-calendar-body-in-range{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;inset:0;background:transparent}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}\n"] }]
14056
- }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
14460
+ args: [{ selector: 'tn-month-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays(); track day.long) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-date]=\"cell.isoDate\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-marked]=\"cell.marked\"\n [class.tn-calendar-body-active]=\"cell.value === activeDay()\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.value === activeDay() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.inSelection\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\"\n [class.tn-calendar-body-fill-primary]=\"cell.fill === 'primary'\"\n [class.tn-calendar-body-fill-marked]=\"cell.fill === 'marked'\"\n [class.tn-calendar-body-today-outline]=\"cell.todayOutline\">\n {{ cell.label }}\n </span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{display:block;--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px;--calendar-indicator-size: var(--calendar-cell-size);--calendar-radius: 4px}.tn-calendar-table{width:100%;min-width:min(100%,7 * var(--calendar-cell-size));table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header th:first-child{border-start-start-radius:var(--calendar-radius);border-end-start-radius:var(--calendar-radius)}.tn-calendar-table-header th:last-child{border-start-end-radius:var(--calendar-radius);border-end-end-radius:var(--calendar-radius)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333)}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-in-range:before,.tn-calendar-body-cell.tn-calendar-body-range-start:before,.tn-calendar-body-cell.tn-calendar-body-range-end:before{content:\"\";position:absolute;top:50%;transform:translateY(-50%);height:var(--calendar-indicator-size);background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-in-range:before{inset-inline:0}.tn-calendar-body-cell.tn-calendar-body-range-start:before{inset-inline-start:50%;inset-inline-end:0}.tn-calendar-body-cell.tn-calendar-body-range-end:before{inset-inline-start:0;inset-inline-end:50%}.tn-calendar-body-cell.tn-calendar-body-range-start.tn-calendar-body-range-end:before{content:none}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-indicator-size);height:var(--calendar-indicator-size);max-width:100%;border-radius:var(--calendar-radius);font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):hover .tn-calendar-body-cell-content:not(.tn-calendar-body-fill-primary){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell-content.tn-calendar-body-fill-marked{background:var(--tn-alt-bg1, #f0f0f0)}.tn-calendar-body-cell-content.tn-calendar-body-fill-primary{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}.tn-calendar-body-cell-content.tn-calendar-body-today-outline{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
14461
+ }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], markedDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "markedDates", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }] } });
14057
14462
 
14058
14463
  class TnMultiYearViewComponent {
14059
14464
  activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
@@ -14061,46 +14466,91 @@ class TnMultiYearViewComponent {
14061
14466
  minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
14062
14467
  maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
14063
14468
  dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
14469
+ /**
14470
+ * Locale for the year numerals — Arabic-Indic digits in `ar`, Western in `en`. Falls
14471
+ * back to the app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
14472
+ */
14473
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
14064
14474
  selectedChange = output();
14065
14475
  activeDateChange = output();
14066
- cellWidth = 25; // 100/4 for 4 columns
14067
- cellAspectRatio = 7.14286; // Same as Material
14476
+ host = inject(ElementRef);
14477
+ injector = inject(Injector);
14478
+ appLocale = inject(LOCALE_ID);
14479
+ intl = injectTnCalendarIntl();
14480
+ /** Years read out in the locale's own numerals, without a thousands separator. */
14481
+ yearFormat = computed(() => {
14482
+ return new Intl.NumberFormat(this.locale() ?? this.appLocale, { useGrouping: false });
14483
+ }, ...(ngDevMode ? [{ debugName: "yearFormat" }] : []));
14068
14484
  yearsPerRow = 4;
14069
- yearRowCount = 6; // Shows 24 years total (6 rows x 4 columns)
14070
- // Calculate the year range to display
14485
+ yearRowCount = YEARS_PER_PAGE / this.yearsPerRow;
14486
+ /**
14487
+ * The page of years on screen. Pages are fixed blocks, so the active year sits
14488
+ * wherever it falls within one rather than in the middle — see `yearPageStart`.
14489
+ */
14071
14490
  yearRange = computed(() => {
14072
- const activeDate = this.activeDate();
14073
- const currentYear = activeDate.getFullYear();
14074
- // Calculate the starting year for a 24-year range
14075
- // We want the active year to be roughly in the middle
14076
- const startYear = Math.floor(currentYear / 24) * 24;
14077
- return { start: startYear, end: startYear + 23 };
14491
+ const start = yearPageStart(this.activeDate().getFullYear());
14492
+ return { start, end: start + YEARS_PER_PAGE - 1 };
14078
14493
  }, ...(ngDevMode ? [{ debugName: "yearRange" }] : []));
14494
+ /**
14495
+ * Names the grid after the span of years it shows, so a screen-reader user arrowing
14496
+ * around knows which page they are on rather than just hearing bare years.
14497
+ */
14498
+ gridLabel = computed(() => {
14499
+ const range = this.yearRange();
14500
+ return this.intl().yearGridLabel(range.start, range.end);
14501
+ }, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
14079
14502
  yearRows = computed(() => {
14080
14503
  const range = this.yearRange();
14504
+ const format = this.yearFormat();
14505
+ const formatYear = (year) => format.format(year);
14506
+ // Read once for the whole grid rather than per cell, matching the month view: every
14507
+ // year is then measured against the same instant, and 24 allocations become one.
14508
+ const thisYear = new Date().getFullYear();
14081
14509
  const rows = [];
14082
14510
  for (let row = 0; row < this.yearRowCount; row++) {
14083
14511
  const yearRow = [];
14084
14512
  for (let col = 0; col < this.yearsPerRow; col++) {
14085
14513
  const year = range.start + (row * this.yearsPerRow) + col;
14086
- yearRow.push(this.createYearCell(year));
14514
+ yearRow.push(this.createYearCell(year, formatYear, thisYear));
14087
14515
  }
14088
14516
  rows.push(yearRow);
14089
14517
  }
14090
14518
  return rows;
14091
14519
  }, ...(ngDevMode ? [{ debugName: "yearRows" }] : []));
14092
- createYearCell(year) {
14093
- const today = new Date();
14094
- const currentYear = today.getFullYear();
14520
+ /**
14521
+ * The year that carries `tabindex="0"` — the grid's single keyboard entry point, per
14522
+ * the roving tabindex pattern. Follows `activeDate`, falling back to the nearest
14523
+ * enabled year in the page so a range whose active year is disabled still has a
14524
+ * tabbable cell. `null` only when every year on the page is disabled.
14525
+ */
14526
+ activeYear = computed(() => {
14527
+ const range = this.yearRange();
14528
+ // Always inside the page: the range is derived from this same year.
14529
+ const wanted = this.activeDate().getFullYear();
14530
+ if (this.isYearEnabled(wanted)) {
14531
+ return wanted;
14532
+ }
14533
+ // Spiral outwards from the year we wanted, forward first.
14534
+ const span = range.end - range.start;
14535
+ for (let offset = 1; offset <= span; offset++) {
14536
+ if (wanted + offset <= range.end && this.isYearEnabled(wanted + offset)) {
14537
+ return wanted + offset;
14538
+ }
14539
+ if (wanted - offset >= range.start && this.isYearEnabled(wanted - offset)) {
14540
+ return wanted - offset;
14541
+ }
14542
+ }
14543
+ return null;
14544
+ }, ...(ngDevMode ? [{ debugName: "activeYear" }] : []));
14545
+ createYearCell(year, formatYear, thisYear) {
14095
14546
  const selectedYear = this.selected()?.getFullYear();
14096
- const isToday = year === currentYear;
14547
+ const isToday = year === thisYear;
14097
14548
  const isSelected = year === selectedYear;
14098
14549
  const enabled = this.isYearEnabled(year);
14099
14550
  return {
14100
- value: year,
14101
- year: year,
14102
- label: year.toString(),
14103
- ariaLabel: this.formatYearAriaLabel(year, isSelected, isToday),
14551
+ year,
14552
+ label: formatYear(year),
14553
+ ariaLabel: this.formatYearAriaLabel(formatYear(year), isToday),
14104
14554
  enabled,
14105
14555
  selected: isSelected,
14106
14556
  today: isToday,
@@ -14116,7 +14566,11 @@ class TnMultiYearViewComponent {
14116
14566
  if (maxDate && year > maxDate.getFullYear()) {
14117
14567
  return false;
14118
14568
  }
14119
- // If we have a date filter, test January 1st of that year
14569
+ // Deliberately coarse: a year is a single cell, so it gets a single verdict, and
14570
+ // January 1st stands in for the whole year. A filter that rules out January 1st
14571
+ // while allowing the rest of the year therefore disables the year outright. Material
14572
+ // approximates the same way; testing all 365 days per cell is not worth it, and the
14573
+ // month view applies the filter exactly once the user drills in.
14120
14574
  if (dateFilter) {
14121
14575
  const testDate = new Date(year, 0, 1);
14122
14576
  if (!dateFilter(testDate)) {
@@ -14125,13 +14579,15 @@ class TnMultiYearViewComponent {
14125
14579
  }
14126
14580
  return true;
14127
14581
  }
14128
- formatYearAriaLabel(year, isSelected, isToday) {
14129
- let label = year.toString();
14130
- if (isSelected) {
14131
- label += ' (selected)';
14132
- }
14582
+ /**
14583
+ * Selection is carried by the button's `aria-pressed`, so it is deliberately not
14584
+ * repeated here. "Current year" stays: `aria-current="date"` on a year cell is vague
14585
+ * enough that spelling it out earns its place.
14586
+ */
14587
+ formatYearAriaLabel(year, isToday) {
14588
+ let label = year;
14133
14589
  if (isToday) {
14134
- label += ' (current year)';
14590
+ label += ` ${this.intl().currentYear}`;
14135
14591
  }
14136
14592
  return label;
14137
14593
  }
@@ -14143,19 +14599,92 @@ class TnMultiYearViewComponent {
14143
14599
  }
14144
14600
  onYearClicked(cell) {
14145
14601
  if (cell.enabled) {
14146
- // Create a new date with the selected year, keeping current month and day
14147
- const currentDate = this.activeDate();
14148
- const newDate = new Date(cell.year, currentDate.getMonth(), currentDate.getDate());
14149
- this.selectedChange.emit(newDate);
14602
+ // Keeps the month and day, clamped: moving off 29 February into a non-leap year
14603
+ // would otherwise roll over into March and change the month too.
14604
+ this.selectedChange.emit(withYear(this.activeDate(), cell.year));
14605
+ }
14606
+ }
14607
+ /**
14608
+ * Moves the roving tabindex across the year grid. Picking a year stays on
14609
+ * click/Enter/Space, which the cells are already buttons for.
14610
+ *
14611
+ * Moves past either end of the 24-year page emit an `activeDateChange` like the
14612
+ * header's paging buttons do, and the grid re-renders on the neighbouring page.
14613
+ */
14614
+ onKeydown(event) {
14615
+ const from = this.activeYear();
14616
+ if (from === null) {
14617
+ return;
14618
+ }
14619
+ const move = this.targetForKey(event, from);
14620
+ if (move === null) {
14621
+ return;
14622
+ }
14623
+ // Claim the key before the browser scrolls the page with it.
14624
+ event.preventDefault();
14625
+ const landing = this.nearestEnabledYear(move.year, move.search);
14626
+ if (landing === null || landing === from) {
14627
+ return;
14628
+ }
14629
+ this.activeDateChange.emit(withYear(this.activeDate(), landing));
14630
+ focusActiveCellAfterRender(this.host, this.injector);
14631
+ }
14632
+ /**
14633
+ * The year a key aims at, and which way to look from there when that year is disabled.
14634
+ *
14635
+ * The search direction belongs to the key, not to where the target happens to fall
14636
+ * relative to the current year. Home and End mean "the ends of *this* page", so they
14637
+ * search inward: looking outward would step onto the neighbouring 24-year page over a
14638
+ * single ruled-out boundary year and turn the page, which is not what either key
14639
+ * promises. The arrows and the paging keys keep travelling the way they were already
14640
+ * headed, and moving onto the next page is the point of those.
14641
+ */
14642
+ targetForKey(event, from) {
14643
+ const range = this.yearRange();
14644
+ const yearsPerPage = range.end - range.start + 1;
14645
+ switch (event.key) {
14646
+ case 'ArrowLeft': return { year: from - 1, search: -1 };
14647
+ case 'ArrowRight': return { year: from + 1, search: 1 };
14648
+ case 'ArrowUp': return { year: from - this.yearsPerRow, search: -1 };
14649
+ case 'ArrowDown': return { year: from + this.yearsPerRow, search: 1 };
14650
+ case 'Home': return { year: range.start, search: 1 };
14651
+ case 'End': return { year: range.end, search: -1 };
14652
+ case 'PageUp': return { year: from - yearsPerPage, search: -1 };
14653
+ case 'PageDown': return { year: from + yearsPerPage, search: 1 };
14654
+ default: return null;
14655
+ }
14656
+ }
14657
+ /**
14658
+ * Steps past years the caller has disabled, so `minDate`/`maxDate` don't park focus
14659
+ * somewhere unusable.
14660
+ *
14661
+ * Searches the way the key asked for (see `targetForKey`), then doubles back if that
14662
+ * finds nothing. The fallback is for the ends of the allowed range: arrowing right at
14663
+ * `maxDate` finds nothing ahead, doubles back onto the year we started from, and the
14664
+ * caller reads that as "don't move".
14665
+ */
14666
+ nearestEnabledYear(target, preferred) {
14667
+ return this.scanForEnabledYear(target, preferred)
14668
+ ?? this.scanForEnabledYear(target, preferred === 1 ? -1 : 1);
14669
+ }
14670
+ /** Bounded, so an everything-disabled range stops the move instead of spinning. */
14671
+ scanForEnabledYear(from, direction) {
14672
+ const maxSteps = 48;
14673
+ for (let step = 0; step <= maxSteps; step++) {
14674
+ const candidate = from + (step * direction);
14675
+ if (this.isYearEnabled(candidate)) {
14676
+ return candidate;
14677
+ }
14150
14678
  }
14679
+ return null;
14151
14680
  }
14152
14681
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14153
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMultiYearViewComponent, isStandalone: true, selector: "tn-multi-year-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\"\n [style.width.%]=\"cellWidth\"\n [style.padding-top.%]=\"cellAspectRatio\"\n [style.padding-bottom.%]=\"cellAspectRatio\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [".tn-calendar-table{width:100%;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body{min-width:224px}.tn-calendar-body-cell-container{position:relative;border:none}.tn-calendar-body-cell{position:absolute;top:5%;left:5%;width:90%;height:90%;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500;transition:background-color .2s ease}.tn-calendar-body-cell:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}\n"] });
14682
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMultiYearViewComponent, isStandalone: true, selector: "tn-multi-year-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-year]=\"cell.year\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.year === activeYear()\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.year === activeYear() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\">\n {{ cell.label }}\n </span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-radius: 4px;--calendar-year-row-height: 48px;--calendar-year-indicator-width: 72px;--calendar-year-indicator-height: 36px}.tn-calendar-table{width:100%;min-width:min(100%,224px);table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body-cell-container{position:relative;border:none;height:var(--calendar-year-row-height);width:25%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-year-indicator-width);height:var(--calendar-year-indicator-height);max-width:100%;border-radius:var(--calendar-radius);transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):not(.tn-calendar-body-selected):hover .tn-calendar-body-cell-content{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected) .tn-calendar-body-cell-content{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected .tn-calendar-body-cell-content{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}\n"] });
14154
14683
  }
14155
14684
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, decorators: [{
14156
14685
  type: Component,
14157
- args: [{ selector: 'tn-multi-year-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\"\n [style.width.%]=\"cellWidth\"\n [style.padding-top.%]=\"cellAspectRatio\"\n [style.padding-bottom.%]=\"cellAspectRatio\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [".tn-calendar-table{width:100%;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body{min-width:224px}.tn-calendar-body-cell-container{position:relative;border:none}.tn-calendar-body-cell{position:absolute;top:5%;left:5%;width:90%;height:90%;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500;transition:background-color .2s ease}.tn-calendar-body-cell:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}\n"] }]
14158
- }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
14686
+ args: [{ selector: 'tn-multi-year-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-year]=\"cell.year\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.year === activeYear()\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.year === activeYear() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\">\n {{ cell.label }}\n </span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-radius: 4px;--calendar-year-row-height: 48px;--calendar-year-indicator-width: 72px;--calendar-year-indicator-height: 36px}.tn-calendar-table{width:100%;min-width:min(100%,224px);table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body-cell-container{position:relative;border:none;height:var(--calendar-year-row-height);width:25%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-year-indicator-width);height:var(--calendar-year-indicator-height);max-width:100%;border-radius:var(--calendar-radius);transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):not(.tn-calendar-body-selected):hover .tn-calendar-body-cell-content{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected) .tn-calendar-body-cell-content{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected .tn-calendar-body-cell-content{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}\n"] }]
14687
+ }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
14159
14688
 
14160
14689
  class TnCalendarComponent {
14161
14690
  startView = input('month', ...(ngDevMode ? [{ debugName: "startView" }] : []));
@@ -14163,116 +14692,112 @@ class TnCalendarComponent {
14163
14692
  minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
14164
14693
  maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
14165
14694
  dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
14695
+ /**
14696
+ * Dates to flag as noteworthy — days a task runs, days with events, and the like.
14697
+ * Order and time-of-day are ignored; only the calendar day is compared. The
14698
+ * calendar owns how a marked day looks, so callers pass dates rather than styles.
14699
+ * Marking is independent of `selected`/`selectedRange`, which stay authoritative
14700
+ * where they overlap.
14701
+ */
14702
+ markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
14703
+ /**
14704
+ * Which month — or which page of years, in the year view — is on screen.
14705
+ *
14706
+ * Optional. Left unbound, the calendar opens on the month the bound value lives in and
14707
+ * drives navigation itself. Bind it, alongside `activeDateChange`, to drive the view
14708
+ * from outside: useful for a calendar that stays mounted while its value jumps to
14709
+ * another month, which the open-on-init behaviour alone won't follow.
14710
+ *
14711
+ * Unlike `selected`, this isn't strictly controlled — the calendar still navigates on
14712
+ * its own when the user pages or arrows around, so binding a constant here won't
14713
+ * freeze the view. Changing what you bind always wins.
14714
+ */
14715
+ activeDate = input(undefined, ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
14716
+ /**
14717
+ * Locale for dates, month and weekday names, and which day the week starts on.
14718
+ *
14719
+ * Defaults to the app's `LOCALE_ID`, which is where an Angular app already declares
14720
+ * its locale — so a `ng build --localize` app is right without touching this. Bind it
14721
+ * only when one calendar needs a different locale from the rest of the app.
14722
+ *
14723
+ * Nothing here reads the browser's language on its own: doing so would quietly
14724
+ * disagree with `DatePipe` and every other locale-aware part of the app. An app that
14725
+ * wants that behaviour opts into it once, at bootstrap:
14726
+ * `{ provide: LOCALE_ID, useValue: navigator.language }`.
14727
+ *
14728
+ * Wording that isn't a date — "(marked)", the header button labels — comes from
14729
+ * `TN_CALENDAR_INTL` instead.
14730
+ */
14731
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
14732
+ appLocale = inject(LOCALE_ID);
14733
+ host = inject(ElementRef);
14734
+ injector = inject(Injector);
14735
+ /** The locale actually in force: the input when bound, the app's otherwise. */
14736
+ resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
14166
14737
  // Range mode inputs
14167
14738
  rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
14168
14739
  selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
14169
14740
  selectedChange = output();
14741
+ /**
14742
+ * The date the calendar is now sitting on — the month it shows, and the day holding
14743
+ * the grid's roving tabindex.
14744
+ *
14745
+ * This is navigation, not selection: it fires on paging, on arrow keys, and on
14746
+ * clicking a day (a click moves the active cell as well as choosing the date, so the
14747
+ * two never drift apart). Treat it as "the view moved", not "the user picked a date" —
14748
+ * `selectedChange` is the one that means a choice was made.
14749
+ */
14170
14750
  activeDateChange = output();
14171
14751
  viewChanged = output();
14172
14752
  // Range mode outputs
14173
14753
  selectedRangeChange = output();
14174
- currentDate = signal(new Date(), ...(ngDevMode ? [{ debugName: "currentDate" }] : []));
14754
+ /**
14755
+ * Which month/year grid is on screen. Navigation state, not selection state.
14756
+ *
14757
+ * Follows the `activeDate` input whenever the caller changes it, but stays writable so
14758
+ * paging and arrow keys keep working with no echo back from the caller.
14759
+ */
14760
+ currentDate = linkedSignal({ ...(ngDevMode ? { debugName: "currentDate" } : {}), source: () => this.activeDate(),
14761
+ computation: (activeDate, previous) => activeDate ?? previous?.value ?? new Date() });
14175
14762
  currentView = signal('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
14176
- // Range selection state - this is the authoritative source for calendar display
14177
- rangeState = signal({
14178
- start: null,
14179
- end: null,
14180
- selecting: 'start'
14181
- }, ...(ngDevMode ? [{ debugName: "rangeState" }] : []));
14182
- // Track if user has interacted with calendar - once true, ignore external selectedRange
14183
- userHasInteracted = false;
14184
- constructor() {
14185
- // Watch for changes to selectedRange input
14186
- effect(() => {
14187
- // Track signals to re-run effect when they change
14188
- this.selectedRange();
14189
- const rangeMode = this.rangeMode();
14190
- // Only update range state from external selectedRange if user hasn't interacted yet
14191
- if (!this.userHasInteracted && rangeMode) {
14192
- this.initializeRangeState();
14193
- }
14194
- });
14195
- }
14196
14763
  ngOnInit() {
14197
14764
  this.currentView.set(this.startView());
14198
- // Initialize range state if in range mode (this also handles currentDate)
14199
- if (this.rangeMode()) {
14200
- this.initializeRangeState();
14765
+ // A bound activeDate already says what should be on screen.
14766
+ if (this.activeDate()) {
14767
+ return;
14201
14768
  }
14202
- else {
14203
- const selected = this.selected();
14204
- if (selected) {
14205
- // For single date mode, navigate to the selected date's month
14206
- this.currentDate.set(new Date(selected));
14207
- }
14769
+ // Otherwise open on the month the caller's value lives in.
14770
+ const initialDate = this.rangeMode()
14771
+ ? (this.selectedRange()?.start ?? this.selectedRange()?.end ?? null)
14772
+ : (this.selected() ?? null);
14773
+ if (initialDate) {
14774
+ this.currentDate.set(new Date(initialDate));
14208
14775
  }
14209
14776
  }
14210
- initializeRangeState() {
14211
- if (this.rangeMode()) {
14212
- const selectedRange = this.selectedRange();
14213
- if (selectedRange) {
14214
- this.rangeState.set({
14215
- start: selectedRange.start,
14216
- end: selectedRange.end,
14217
- selecting: selectedRange.start && selectedRange.end ? 'start' :
14218
- selectedRange.start ? 'end' : 'start'
14219
- });
14220
- // Navigate to the month of the selected start date, or end date if no start date
14221
- const dateToShow = selectedRange.start || selectedRange.end;
14222
- if (dateToShow) {
14223
- this.currentDate.set(new Date(dateToShow));
14224
- }
14225
- }
14226
- else {
14227
- // No selected range - initialize empty range state
14228
- this.rangeState.set({
14229
- start: null,
14230
- end: null,
14231
- selecting: 'start'
14232
- });
14233
- }
14234
- }
14235
- }
14236
- onMonthSelected(month) {
14237
- const newDate = new Date(this.currentDate());
14238
- newDate.setMonth(month);
14239
- this.currentDate.set(newDate);
14240
- this.currentView.set('month');
14241
- this.viewChanged.emit('month');
14242
- }
14243
- onYearSelected(year) {
14244
- const newDate = new Date(this.currentDate());
14245
- newDate.setFullYear(year);
14246
- this.currentDate.set(newDate);
14247
- this.activeDateChange.emit(newDate);
14248
- }
14249
14777
  onViewChanged(view) {
14250
14778
  this.currentView.set(view);
14251
14779
  this.viewChanged.emit(view);
14252
14780
  }
14253
14781
  onPreviousClicked() {
14254
- const current = this.currentDate();
14255
- let newDate;
14256
- if (this.currentView() === 'month') {
14257
- newDate = new Date(current.getFullYear(), current.getMonth() - 1, 1);
14258
- }
14259
- else {
14260
- // For year view, navigate by 24-year ranges (like Material)
14261
- newDate = new Date(current.getFullYear() - 24, current.getMonth(), 1);
14262
- }
14263
- this.currentDate.set(newDate);
14264
- this.activeDateChange.emit(newDate);
14782
+ this.page(-1);
14265
14783
  }
14266
14784
  onNextClicked() {
14785
+ this.page(1);
14786
+ }
14787
+ /**
14788
+ * Steps the view one page in `direction`: a month in the day view, a page of years in
14789
+ * the year view — the same spans `PageUp`/`PageDown` move, and the same the header
14790
+ * labels.
14791
+ *
14792
+ * Carries the day across, clamped, rather than dropping to the 1st. The keyboard has
14793
+ * always done this, so resetting here left the roving tabindex somewhere different
14794
+ * depending on whether the user clicked the arrow or pressed the key.
14795
+ */
14796
+ page(direction) {
14267
14797
  const current = this.currentDate();
14268
- let newDate;
14269
- if (this.currentView() === 'month') {
14270
- newDate = new Date(current.getFullYear(), current.getMonth() + 1, 1);
14271
- }
14272
- else {
14273
- // For year view, navigate by 24-year ranges (like Material)
14274
- newDate = new Date(current.getFullYear() + 24, current.getMonth(), 1);
14275
- }
14798
+ const newDate = this.currentView() === 'month'
14799
+ ? addMonths(current, direction)
14800
+ : withYear(current, current.getFullYear() + (direction * YEARS_PER_PAGE));
14276
14801
  this.currentDate.set(newDate);
14277
14802
  this.activeDateChange.emit(newDate);
14278
14803
  }
@@ -14284,83 +14809,346 @@ class TnCalendarComponent {
14284
14809
  this.selectedChange.emit(date);
14285
14810
  }
14286
14811
  }
14812
+ /**
14813
+ * Works out the next range from the one the caller currently owns and emits it. The
14814
+ * calendar keeps no copy — what it renders next is whatever the caller binds back.
14815
+ */
14287
14816
  handleRangeSelection(date) {
14288
- // Mark that user has interacted - calendar is now authoritative
14289
- this.userHasInteracted = true;
14290
- const currentRange = this.rangeState();
14291
- // If we already have a complete range (both start and end), clear and start fresh
14292
- if (currentRange.start && currentRange.end && currentRange.selecting === 'start') {
14293
- const newRangeState = {
14294
- start: date,
14295
- end: null,
14296
- selecting: 'end'
14297
- };
14298
- this.rangeState.set(newRangeState);
14817
+ const current = this.selectedRange();
14818
+ const start = current?.start ?? null;
14819
+ const end = current?.end ?? null;
14820
+ // Nothing pending, or the range is already complete: begin a new one.
14821
+ if (!start || end) {
14299
14822
  this.selectedRangeChange.emit({ start: date, end: null });
14300
14823
  return;
14301
14824
  }
14302
- if (currentRange.selecting === 'start' || !currentRange.start) {
14303
- // First click or selecting start date - clear any previous range immediately
14304
- const newRangeState = {
14305
- start: date,
14306
- end: null,
14307
- selecting: 'end'
14308
- };
14309
- this.rangeState.set(newRangeState);
14825
+ // A second pick before the first restarts the range rather than inverting it.
14826
+ // Compared by day: the clicked date is midnight, but the caller's start may carry a
14827
+ // time of day, which would otherwise make re-picking the same day restart the range.
14828
+ if (compareDays(date, start) < 0) {
14310
14829
  this.selectedRangeChange.emit({ start: date, end: null });
14830
+ return;
14311
14831
  }
14312
- else {
14313
- // Setting end date
14314
- const start = currentRange.start;
14315
- // If second date is earlier than first, treat it as new start date
14316
- if (date < start) {
14317
- const newRangeState = {
14318
- start: date,
14319
- end: null,
14320
- selecting: 'end'
14321
- };
14322
- this.rangeState.set(newRangeState);
14323
- this.selectedRangeChange.emit({ start: date, end: null });
14324
- }
14325
- else {
14326
- // Valid end date - complete the range
14327
- const newRangeState = {
14328
- start: start,
14329
- end: date,
14330
- selecting: 'start'
14331
- };
14332
- this.rangeState.set(newRangeState);
14333
- this.selectedRangeChange.emit({ start: start, end: date });
14334
- }
14335
- }
14832
+ this.selectedRangeChange.emit({ start, end: date });
14336
14833
  }
14337
14834
  onActiveDateChange(date) {
14338
14835
  this.currentDate.set(date);
14339
14836
  this.activeDateChange.emit(date);
14340
14837
  }
14838
+ /**
14839
+ * A year picked in the year grid moves the view to that year and returns to the day
14840
+ * grid. It is navigation, not selection — no `selectedChange` — so the user lands on
14841
+ * the month they asked for and picks a day from there.
14842
+ */
14341
14843
  onYearSelectedFromView(date) {
14342
- // When a year is selected from the multi-year view, update the current date
14343
- // and switch back to month view
14344
14844
  this.currentDate.set(date);
14345
14845
  this.currentView.set('month');
14346
14846
  this.viewChanged.emit('month');
14347
14847
  this.activeDateChange.emit(date);
14348
- }
14349
- /**
14350
- * Reset the calendar to accept external range values - called when calendar reopens
14351
- */
14352
- resetInteractionState() {
14353
- this.userHasInteracted = false;
14354
- // Reinitialize range state from selectedRange if provided
14355
- this.initializeRangeState();
14848
+ // Switching grids destroys the year cell the user just activated, which drops focus
14849
+ // to `<body>` and restarts the tab order. Carry it over to the day now active.
14850
+ focusActiveCellAfterRender(this.host, this.injector);
14356
14851
  }
14357
14852
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14358
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCalendarComponent, isStandalone: true, selector: "tn-calendar", inputs: { startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", viewChanged: "viewChanged", selectedRangeChange: "selectedRangeChange" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [currentView]=\"currentView()\"\n (monthSelected)=\"onMonthSelected($event)\"\n (yearSelected)=\"onYearSelected($event)\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\" cdkMonitorSubtreeFocus tabindex=\"-1\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? rangeState() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"], dependencies: [{ kind: "component", type: TnCalendarHeaderComponent, selector: "tn-calendar-header", inputs: ["currentDate", "currentView"], outputs: ["monthSelected", "yearSelected", "viewChanged", "previousClicked", "nextClicked"] }, { kind: "component", type: TnMonthViewComponent, selector: "tn-month-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange"] }, { kind: "component", type: TnMultiYearViewComponent, selector: "tn-multi-year-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter"], outputs: ["selectedChange", "activeDateChange"] }] });
14853
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCalendarComponent, isStandalone: true, selector: "tn-calendar", inputs: { startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", 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 }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, markedDates: { classPropertyName: "markedDates", publicName: "markedDates", isSignal: true, isRequired: false, transformFunction: null }, activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", viewChanged: "viewChanged", selectedRangeChange: "selectedRangeChange" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [currentView]=\"currentView()\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [markedDates]=\"markedDates()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? selectedRange() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"], dependencies: [{ kind: "component", type: TnCalendarHeaderComponent, selector: "tn-calendar-header", inputs: ["currentDate", "currentView", "locale"], outputs: ["viewChanged", "previousClicked", "nextClicked"] }, { kind: "component", type: TnMonthViewComponent, selector: "tn-month-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "rangeMode", "selectedRange", "locale"], outputs: ["selectedChange", "activeDateChange"] }, { kind: "component", type: TnMultiYearViewComponent, selector: "tn-multi-year-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "locale"], outputs: ["selectedChange", "activeDateChange"] }] });
14359
14854
  }
14360
14855
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, decorators: [{
14361
14856
  type: Component,
14362
- args: [{ selector: 'tn-calendar', standalone: true, imports: [TnCalendarHeaderComponent, TnMonthViewComponent, TnMultiYearViewComponent], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [currentView]=\"currentView()\"\n (monthSelected)=\"onMonthSelected($event)\"\n (yearSelected)=\"onYearSelected($event)\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\" cdkMonitorSubtreeFocus tabindex=\"-1\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? rangeState() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"] }]
14363
- }], ctorParameters: () => [], propDecorators: { startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], selectedRangeChange: [{ type: i0.Output, args: ["selectedRangeChange"] }] } });
14857
+ args: [{ selector: 'tn-calendar', standalone: true, imports: [TnCalendarHeaderComponent, TnMonthViewComponent, TnMultiYearViewComponent], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [currentView]=\"currentView()\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [markedDates]=\"markedDates()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? selectedRange() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"] }]
14858
+ }], propDecorators: { startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], markedDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "markedDates", required: false }] }], activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], selectedRangeChange: [{ type: i0.Output, args: ["selectedRangeChange"] }] } });
14859
+
14860
+ /**
14861
+ * Harness for interacting with a single day (or year) cell inside `tn-calendar`.
14862
+ *
14863
+ * Modelled on Angular Material's `MatCalendarCellHarness` so specs can move across
14864
+ * with the method names intact. Three differences worth knowing:
14865
+ *
14866
+ * - `isMarked()` is new — it reports the `markedDates` state, which Material has no
14867
+ * equivalent for.
14868
+ * - `getDate()` and the `date` filter are new, and are the reliable way to name a cell:
14869
+ * the text is formatted for the app's locale, so `'14'` misses a calendar rendering
14870
+ * `١٤`.
14871
+ * - There are no comparison-range or preview-range methods (`isComparisonRangeStart()`
14872
+ * and friends); `tn-calendar` has no comparison range to report.
14873
+ *
14874
+ * @example
14875
+ * ```typescript
14876
+ * const calendar = await loader.getHarness(TnCalendarHarness);
14877
+ * const [first] = await calendar.getCells({ date: new Date(2031, 4, 14) });
14878
+ * await first.select();
14879
+ * expect(await first.isSelected()).toBe(true);
14880
+ * ```
14881
+ */
14882
+ class TnCalendarCellHarness extends ComponentHarness {
14883
+ /** The selector for a calendar cell. Padding cells render no button, so only real days match. */
14884
+ static hostSelector = '.tn-calendar-body-cell';
14885
+ _content = this.locatorFor('.tn-calendar-body-cell-content');
14886
+ /**
14887
+ * Gets a `HarnessPredicate` that can be used to search for a cell with specific attributes.
14888
+ *
14889
+ * @param options Options for filtering which cell instances are considered a match.
14890
+ * @returns A `HarnessPredicate` configured with the given options.
14891
+ *
14892
+ * @example
14893
+ * ```typescript
14894
+ * // Every day the schedule runs on
14895
+ * const runDays = await calendar.getCells({ marked: true });
14896
+ *
14897
+ * // The disabled days before minDate
14898
+ * const blocked = await calendar.getCells({ disabled: true });
14899
+ * ```
14900
+ */
14901
+ static with(options = {}) {
14902
+ return new HarnessPredicate(TnCalendarCellHarness, options)
14903
+ .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
14904
+ .addOption('date', options.date, async (harness, date) => (await harness.getDateStamp()) === isoDateString(date))
14905
+ .addOption('year', options.year, async (harness, year) => (await harness.getYear()) === year)
14906
+ .addOption('selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected)
14907
+ .addOption('active', options.active, async (harness, active) => (await harness.isActive()) === active)
14908
+ .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled)
14909
+ .addOption('today', options.today, async (harness, today) => (await harness.isToday()) === today)
14910
+ .addOption('inRange', options.inRange, async (harness, inRange) => (await harness.isInRange()) === inRange)
14911
+ .addOption('marked', options.marked, async (harness, marked) => (await harness.isMarked()) === marked);
14912
+ }
14913
+ /** Gets the text of the calendar cell. */
14914
+ async getText() {
14915
+ return (await this._content()).text();
14916
+ }
14917
+ /** Gets the aria-label of the calendar cell. */
14918
+ async getAriaLabel() {
14919
+ return (await this.host()).getAttribute('aria-label').then((label) => label ?? '');
14920
+ }
14921
+ /**
14922
+ * Gets the calendar day this cell represents, independent of how it is rendered —
14923
+ * `getText()` is formatted for the locale, so it reads `١٤` in Arabic while this still
14924
+ * answers the 14th. Midnight local time. `null` on a year cell, which has no day.
14925
+ */
14926
+ async getDate() {
14927
+ const stamp = await this.getDateStamp();
14928
+ if (!stamp) {
14929
+ return null;
14930
+ }
14931
+ const [year, month, day] = stamp.split('-').map(Number);
14932
+ return new Date(year, month - 1, day);
14933
+ }
14934
+ /**
14935
+ * Gets the year this cell represents, independent of how it is rendered. Works on year
14936
+ * cells; `null` on a day cell.
14937
+ */
14938
+ async getYear() {
14939
+ const year = await (await this.host()).getAttribute('data-tn-year');
14940
+ return year === null ? null : Number(year);
14941
+ }
14942
+ /** The raw `YYYY-MM-DD` stamp, for comparing without rebuilding a `Date`. */
14943
+ async getDateStamp() {
14944
+ return (await this.host()).getAttribute('data-tn-date');
14945
+ }
14946
+ /** Whether the cell is selected. */
14947
+ async isSelected() {
14948
+ return (await this.host()).hasClass('tn-calendar-body-selected');
14949
+ }
14950
+ /** Whether the cell is disabled. */
14951
+ async isDisabled() {
14952
+ return (await this.host()).getProperty('disabled');
14953
+ }
14954
+ /**
14955
+ * Whether the cell holds the grid's roving tabindex — the one cell reachable with Tab,
14956
+ * which the arrow keys move. Independent of selection: arrowing around changes which
14957
+ * cell is active without selecting anything.
14958
+ */
14959
+ async isActive() {
14960
+ return (await this.host()).hasClass('tn-calendar-body-active');
14961
+ }
14962
+ /** Whether the cell represents today's date. */
14963
+ async isToday() {
14964
+ return (await this.host()).hasClass('tn-calendar-body-today');
14965
+ }
14966
+ /** Whether the cell was flagged through the calendar's `markedDates` input. */
14967
+ async isMarked() {
14968
+ return (await this.host()).hasClass('tn-calendar-body-marked');
14969
+ }
14970
+ /** Whether the cell is the start of the selected range. */
14971
+ async isRangeStart() {
14972
+ return (await this.host()).hasClass('tn-calendar-body-range-start');
14973
+ }
14974
+ /** Whether the cell is the end of the selected range. */
14975
+ async isRangeEnd() {
14976
+ return (await this.host()).hasClass('tn-calendar-body-range-end');
14977
+ }
14978
+ /**
14979
+ * Whether the cell is part of the selected range, ends included — matching Material's
14980
+ * `isInRange()`. Use `isRangeStart()`/`isRangeEnd()` to single out the ends.
14981
+ */
14982
+ async isInRange() {
14983
+ const host = await this.host();
14984
+ const [between, start, end] = await parallel(() => [
14985
+ host.hasClass('tn-calendar-body-in-range'),
14986
+ host.hasClass('tn-calendar-body-range-start'),
14987
+ host.hasClass('tn-calendar-body-range-end'),
14988
+ ]);
14989
+ return between || start || end;
14990
+ }
14991
+ /** Selects the calendar cell. Won't do anything if the cell is disabled. */
14992
+ async select() {
14993
+ return (await this.host()).click();
14994
+ }
14995
+ /**
14996
+ * Presses a key on the cell. The grid handles its keys on the cells themselves, so
14997
+ * this is how the roving tabindex is driven — arrows, Home/End and PageUp/PageDown
14998
+ * move the active cell, while Enter and Space select.
14999
+ *
15000
+ * @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
15001
+ * @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
15002
+ *
15003
+ * @example
15004
+ * ```typescript
15005
+ * const [cell] = await calendar.getCells({ active: true });
15006
+ * await cell.press(TestKey.PAGE_DOWN, { shift: true }); // a year on
15007
+ * ```
15008
+ */
15009
+ async press(key, modifiers) {
15010
+ const host = await this.host();
15011
+ return modifiers ? host.sendKeys(modifiers, key) : host.sendKeys(key);
15012
+ }
15013
+ /** Hovers over the calendar cell. */
15014
+ async hover() {
15015
+ return (await this.host()).hover();
15016
+ }
15017
+ /** Moves the mouse away from the calendar cell. */
15018
+ async mouseAway() {
15019
+ return (await this.host()).mouseAway();
15020
+ }
15021
+ /** Focuses the calendar cell. */
15022
+ async focus() {
15023
+ return (await this.host()).focus();
15024
+ }
15025
+ /** Removes focus from the calendar cell. */
15026
+ async blur() {
15027
+ return (await this.host()).blur();
15028
+ }
15029
+ }
15030
+ /**
15031
+ * Harness for interacting with `tn-calendar` in tests.
15032
+ *
15033
+ * The method names mirror Angular Material's `MatCalendarHarness`, so a spec written
15034
+ * against the Material calendar moves over unchanged. Differences to know:
15035
+ *
15036
+ * - `getCurrentView()` resolves to `'month' | 'year'`. `tn-calendar` has no separate
15037
+ * multi-year view — its year view *is* the multi-year grid.
15038
+ * - Cell filters gain `marked` for the `markedDates` state and `date`/`year` for naming
15039
+ * a cell without going through its rendered text, and drop Material's
15040
+ * `inComparisonRange`, which has no equivalent here.
15041
+ *
15042
+ * @example
15043
+ * ```typescript
15044
+ * const calendar = await loader.getHarness(TnCalendarHarness);
15045
+ *
15046
+ * // Which days does the schedule run on this month?
15047
+ * const marked = await calendar.getCells({ marked: true });
15048
+ * expect(await parallel(() => marked.map((cell) => cell.getText()))).toEqual(['24', '25']);
15049
+ *
15050
+ * // Page forward a month
15051
+ * await calendar.next();
15052
+ * expect(await calendar.getCurrentViewLabel()).toBe('MAR 2022');
15053
+ *
15054
+ * // Pick a day
15055
+ * await calendar.selectCell({ date: new Date(2022, 2, 14) });
15056
+ * ```
15057
+ */
15058
+ class TnCalendarHarness extends ComponentHarness {
15059
+ /** The selector for the host element of a `TnCalendarComponent` instance. */
15060
+ static hostSelector = 'tn-calendar';
15061
+ _periodButton = this.locatorFor('.tn-calendar-period-button');
15062
+ _previousButton = this.locatorFor('.tn-calendar-previous-button');
15063
+ _nextButton = this.locatorFor('.tn-calendar-next-button');
15064
+ _monthView = this.locatorForOptional('tn-month-view');
15065
+ /**
15066
+ * Gets a `HarnessPredicate` that can be used to search for a calendar with specific
15067
+ * attributes.
15068
+ *
15069
+ * @param options Options for filtering which calendar instances are considered a match.
15070
+ * @returns A `HarnessPredicate` configured with the given options.
15071
+ */
15072
+ static with(options = {}) {
15073
+ return new HarnessPredicate(TnCalendarHarness, options);
15074
+ }
15075
+ /**
15076
+ * Gets a list of cells inside the calendar. Padding cells for days outside the month
15077
+ * are not included.
15078
+ *
15079
+ * @param filter Optionally filters which cells are included.
15080
+ */
15081
+ async getCells(filter = {}) {
15082
+ return this.locatorForAll(TnCalendarCellHarness.with(filter))();
15083
+ }
15084
+ /** Gets the current view that is being shown inside the calendar. */
15085
+ async getCurrentView() {
15086
+ return (await this._monthView()) ? 'month' : 'year';
15087
+ }
15088
+ /** Gets the label of the current calendar view, e.g. `'MAR 2022'`. */
15089
+ async getCurrentViewLabel() {
15090
+ return (await this._periodButton()).text();
15091
+ }
15092
+ /** Changes the calendar view by clicking on the view toggle button. */
15093
+ async changeView() {
15094
+ return (await this._periodButton()).click();
15095
+ }
15096
+ /** Goes to the next page of the current view (e.g. next month when inside the month view). */
15097
+ async next() {
15098
+ return (await this._nextButton()).click();
15099
+ }
15100
+ /** Goes to the previous page of the current view (e.g. previous month in the month view). */
15101
+ async previous() {
15102
+ return (await this._previousButton()).click();
15103
+ }
15104
+ /**
15105
+ * Selects a cell in the current calendar view.
15106
+ *
15107
+ * @param filter An optional filter to apply to the cells. The first cell matching the
15108
+ * filter will be selected.
15109
+ */
15110
+ async selectCell(filter = {}) {
15111
+ const cells = await this.getCells(filter);
15112
+ if (!cells.length) {
15113
+ throw Error(`Cannot find calendar cell matching filter ${JSON.stringify(filter)}`);
15114
+ }
15115
+ await cells[0].select();
15116
+ }
15117
+ /**
15118
+ * Gets the cell holding the grid's roving tabindex — the one cell Tab reaches, and the
15119
+ * one the arrow keys move.
15120
+ *
15121
+ * `null` only when every cell in the view is disabled, where there is nothing to focus.
15122
+ */
15123
+ async getActiveCell() {
15124
+ const [active] = await this.getCells({ active: true });
15125
+ return active ?? null;
15126
+ }
15127
+ /**
15128
+ * Moves the grid's roving tabindex with the keyboard, without going through the DOM.
15129
+ *
15130
+ * Arrows move by a day or a week (a year or a row of years); Home and End reach the
15131
+ * ends of the month or year page; PageUp and PageDown page, shifted by a year. Moving
15132
+ * never changes the selection — use `selectCell()` or `TnCalendarCellHarness.select()`
15133
+ * for that.
15134
+ *
15135
+ * @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
15136
+ * @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
15137
+ *
15138
+ * @example
15139
+ * ```typescript
15140
+ * await calendar.moveActiveCell(TestKey.DOWN_ARROW);
15141
+ * expect(await (await calendar.getActiveCell())!.getText()).toBe('17');
15142
+ * ```
15143
+ */
15144
+ async moveActiveCell(key, modifiers) {
15145
+ const active = await this.getActiveCell();
15146
+ if (!active) {
15147
+ throw Error('Cannot move the active cell: the calendar has no cell able to take focus.');
15148
+ }
15149
+ await active.press(key, modifiers);
15150
+ }
15151
+ }
14364
15152
 
14365
15153
  class TnDateInputComponent {
14366
15154
  overlay = inject(Overlay);
@@ -14381,9 +15169,7 @@ class TnDateInputComponent {
14381
15169
  dayRef = viewChild.required('dayInput');
14382
15170
  yearRef = viewChild.required('yearInput');
14383
15171
  calendarTemplate = viewChild.required('calendarTemplate');
14384
- calendar = viewChild.required(TnCalendarComponent);
14385
15172
  wrapperEl = viewChild.required('wrapper');
14386
- destroy$ = new Subject();
14387
15173
  overlayRef;
14388
15174
  portal;
14389
15175
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
@@ -14399,8 +15185,6 @@ class TnDateInputComponent {
14399
15185
  this.updateDisplayValues();
14400
15186
  }
14401
15187
  ngOnDestroy() {
14402
- this.destroy$.next();
14403
- this.destroy$.complete();
14404
15188
  this.close();
14405
15189
  }
14406
15190
  // ControlValueAccessor implementation
@@ -14543,11 +15327,8 @@ class TnDateInputComponent {
14543
15327
  }
14544
15328
  this.createOverlay();
14545
15329
  this.isOpen.set(true);
14546
- // Reset calendar interaction state when opening
14547
- const cal = this.calendar();
14548
- if (cal) {
14549
- setTimeout(() => cal.resetInteractionState(), 0);
14550
- }
15330
+ // No calendar state to reset — the calendar renders whatever date this component
15331
+ // binds to it, so reopening already shows the current value.
14551
15332
  }
14552
15333
  close() {
14553
15334
  if (this.overlayRef) {
@@ -14604,6 +15385,8 @@ class TnDateInputComponent {
14604
15385
  panelClass: 'tn-datepicker-overlay'
14605
15386
  });
14606
15387
  // Close datepicker when backdrop is clicked
15388
+ // No teardown to track: `dispose()` in close() ends this subscription with the
15389
+ // overlay, and ngOnDestroy closes.
14607
15390
  this.overlayRef.backdropClick().subscribe(() => {
14608
15391
  this.close();
14609
15392
  });
@@ -14617,7 +15400,7 @@ class TnDateInputComponent {
14617
15400
  useExisting: forwardRef(() => TnDateInputComponent),
14618
15401
  multi: true
14619
15402
  }
14620
- ], viewQueries: [{ propertyName: "monthRef", first: true, predicate: ["monthInput"], descendants: true, isSignal: true }, { propertyName: "dayRef", first: true, predicate: ["dayInput"], descendants: true, isSignal: true }, { propertyName: "yearRef", first: true, predicate: ["yearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: TnCalendarComponent, descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
15403
+ ], viewQueries: [{ propertyName: "monthRef", first: true, predicate: ["monthInput"], descendants: true, isSignal: true }, { propertyName: "dayRef", first: true, predicate: ["dayInput"], descendants: true, isSignal: true }, { propertyName: "yearRef", first: true, predicate: ["yearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "activeDate", "locale", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
14621
15404
  }
14622
15405
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateInputComponent, decorators: [{
14623
15406
  type: Component,
@@ -14630,7 +15413,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
14630
15413
  ], host: {
14631
15414
  'class': 'tn-date-input'
14632
15415
  }, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
14633
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TnCalendarComponent), { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
15416
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
14634
15417
 
14635
15418
  /**
14636
15419
  * Formats a Date into zero-padded month, day, and year strings.
@@ -14667,12 +15450,8 @@ async function setInputValue(el, value) {
14667
15450
  }
14668
15451
  await el.blur();
14669
15452
  }
14670
- const MONTHS = [
14671
- 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
14672
- 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC',
14673
- ];
14674
15453
  /**
14675
- * Navigates the calendar overlay to the target month/year, then clicks the day cell.
15454
+ * Navigates the calendar overlay to the target month, then clicks that day.
14676
15455
  * Assumes the calendar popup is already open.
14677
15456
  *
14678
15457
  * @param rootLocator The document root locator factory (for finding overlay elements).
@@ -14680,45 +15459,48 @@ const MONTHS = [
14680
15459
  */
14681
15460
  async function selectCalendarDate(rootLocator, date) {
14682
15461
  await navigateCalendarTo(rootLocator, date);
14683
- const dayStr = date.getDate().toString();
14684
- const cells = await rootLocator.locatorForAll('.tn-calendar-body-cell:not([disabled])')();
14685
- for (const cell of cells) {
14686
- if ((await cell.text()).trim() === dayStr) {
14687
- await cell.click();
14688
- return;
14689
- }
14690
- }
14691
- throw new Error(`Could not find enabled calendar cell for day ${dayStr}`);
15462
+ // Matched on `data-tn-date` rather than the cell's text: the text is formatted for the
15463
+ // locale, right down to the numerals, so "15" finds nothing in an Arabic calendar.
15464
+ const target = isoDateString(date);
15465
+ const cell = await rootLocator.locatorForOptional(`.tn-calendar-body-cell[data-tn-date="${target}"]:not([disabled])`)();
15466
+ if (!cell) {
15467
+ throw new Error(`Could not find an enabled calendar cell for ${target}`);
15468
+ }
15469
+ await cell.click();
14692
15470
  }
14693
15471
  /**
14694
- * Navigates the calendar to show the month/year of the target date by
14695
- * clicking previous/next buttons as needed.
15472
+ * Pages the calendar to the month the target date falls in.
15473
+ *
15474
+ * Reads where the calendar currently sits from the days it is rendering rather than from
15475
+ * the header, which is written in the app's language and numerals. Every day cell in the
15476
+ * grid belongs to the displayed month, so the first one answers the question.
14696
15477
  */
14697
15478
  async function navigateCalendarTo(rootLocator, date) {
14698
- const targetLabel = `${MONTHS[date.getMonth()]} ${date.getFullYear()}`;
14699
- // Safety limit to prevent infinite loops
14700
- for (let i = 0; i < 120; i++) {
14701
- const periodButton = await rootLocator.locatorFor('.tn-calendar-period-button')();
14702
- const currentLabel = (await periodButton.text()).trim();
14703
- if (currentLabel === targetLabel) {
15479
+ const monthsFrom = (from) => {
15480
+ return (date.getFullYear() * 12 + date.getMonth()) - (from.year * 12 + from.month);
15481
+ };
15482
+ // A generous bound on the paging loop: a decade either way, and far more than the
15483
+ // handful of clicks any real navigation needs.
15484
+ for (let step = 0; step < 240; step++) {
15485
+ const shown = await displayedMonth(rootLocator);
15486
+ const delta = monthsFrom(shown);
15487
+ if (delta === 0) {
14704
15488
  return;
14705
15489
  }
14706
- const currentDate = parseCalendarLabel(currentLabel);
14707
- const targetTime = date.getFullYear() * 12 + date.getMonth();
14708
- const currentTime = currentDate.year * 12 + currentDate.month;
14709
- const navSelector = targetTime > currentTime
14710
- ? '.tn-calendar-next-button'
14711
- : '.tn-calendar-previous-button';
14712
- const navButton = await rootLocator.locatorFor(navSelector)();
14713
- await navButton.click();
15490
+ const button = await rootLocator.locatorFor(delta > 0 ? '.tn-calendar-next-button' : '.tn-calendar-previous-button')();
15491
+ await button.click();
14714
15492
  }
14715
- throw new Error(`Could not navigate calendar to ${targetLabel}`);
15493
+ throw new Error(`Could not navigate the calendar to ${isoDateString(date)}`);
14716
15494
  }
14717
- function parseCalendarLabel(label) {
14718
- const parts = label.split(' ');
14719
- const monthIndex = MONTHS.indexOf(parts[0]);
14720
- const year = parseInt(parts[1], 10);
14721
- return { year, month: monthIndex >= 0 ? monthIndex : 0 };
15495
+ /** The month on screen, read off the first day cell the grid rendered. */
15496
+ async function displayedMonth(rootLocator) {
15497
+ const cell = await rootLocator.locatorForOptional('.tn-calendar-body-cell[data-tn-date]')();
15498
+ const stamp = await cell?.getAttribute('data-tn-date');
15499
+ if (!stamp) {
15500
+ throw new Error('Could not read the displayed month: the calendar rendered no day cells.');
15501
+ }
15502
+ const [year, month] = stamp.split('-').map(Number);
15503
+ return { year, month: month - 1 };
14722
15504
  }
14723
15505
 
14724
15506
  /**
@@ -14850,9 +15632,7 @@ class TnDateRangeInputComponent {
14850
15632
  endDayRef = viewChild.required('endDayInput');
14851
15633
  endYearRef = viewChild.required('endYearInput');
14852
15634
  calendarTemplate = viewChild.required('calendarTemplate');
14853
- calendar = viewChild.required(TnCalendarComponent);
14854
15635
  wrapperEl = viewChild.required('wrapper');
14855
- destroy$ = new Subject();
14856
15636
  overlayRef;
14857
15637
  portal;
14858
15638
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
@@ -14867,19 +15647,14 @@ class TnDateRangeInputComponent {
14867
15647
  endDay = signal('', ...(ngDevMode ? [{ debugName: "endDay" }] : []));
14868
15648
  endYear = signal('', ...(ngDevMode ? [{ debugName: "endYear" }] : []));
14869
15649
  currentFocus = 'start';
14870
- // Always provide current range to calendar for initial display
14871
- initialRange = computed(() => {
14872
- return this.value();
14873
- }, ...(ngDevMode ? [{ debugName: "initialRange" }] : []));
14874
15650
  overlay = inject(Overlay);
14875
15651
  viewContainerRef = inject(ViewContainerRef);
15652
+ injector = inject(Injector);
14876
15653
  ngOnInit() {
14877
15654
  // Initialize display values
14878
15655
  this.updateDisplayValues();
14879
15656
  }
14880
15657
  ngOnDestroy() {
14881
- this.destroy$.next();
14882
- this.destroy$.complete();
14883
15658
  this.close();
14884
15659
  }
14885
15660
  // ControlValueAccessor implementation
@@ -14947,8 +15722,9 @@ class TnDateRangeInputComponent {
14947
15722
  if (this.endYearRef()?.nativeElement) {
14948
15723
  this.endYearRef().nativeElement.value = '';
14949
15724
  }
14950
- // Focus end month for next selection
14951
- setTimeout(() => this.endMonthRef()?.nativeElement?.focus(), 0);
15725
+ // Move to the end date once the cleared segments have been painted. Tied to the
15726
+ // render rather than to a zero timeout, as the calendar's own focus handling is.
15727
+ afterNextRender(() => this.endMonthRef()?.nativeElement?.focus(), { injector: this.injector });
14952
15728
  }
14953
15729
  else if (range.start && range.end) {
14954
15730
  // Both dates selected - close calendar
@@ -15108,11 +15884,8 @@ class TnDateRangeInputComponent {
15108
15884
  }
15109
15885
  this.createOverlay();
15110
15886
  this.isOpen.set(true);
15111
- // Reset calendar interaction state when opening
15112
- const cal = this.calendar();
15113
- if (cal) {
15114
- setTimeout(() => cal.resetInteractionState(), 0);
15115
- }
15887
+ // No calendar state to reset — the calendar renders whatever range this component
15888
+ // binds to it, so reopening already shows the current value.
15116
15889
  }
15117
15890
  close() {
15118
15891
  if (this.overlayRef) {
@@ -15169,6 +15942,8 @@ class TnDateRangeInputComponent {
15169
15942
  panelClass: 'tn-datepicker-overlay'
15170
15943
  });
15171
15944
  // Close datepicker when backdrop is clicked
15945
+ // No teardown to track: `dispose()` in close() ends this subscription with the
15946
+ // overlay, and ngOnDestroy closes.
15172
15947
  this.overlayRef.backdropClick().subscribe(() => {
15173
15948
  this.close();
15174
15949
  });
@@ -15182,7 +15957,7 @@ class TnDateRangeInputComponent {
15182
15957
  useExisting: forwardRef(() => TnDateRangeInputComponent),
15183
15958
  multi: true
15184
15959
  }
15185
- ], viewQueries: [{ propertyName: "startMonthRef", first: true, predicate: ["startMonthInput"], descendants: true, isSignal: true }, { propertyName: "startDayRef", first: true, predicate: ["startDayInput"], descendants: true, isSignal: true }, { propertyName: "startYearRef", first: true, predicate: ["startYearInput"], descendants: true, isSignal: true }, { propertyName: "endMonthRef", first: true, predicate: ["endMonthInput"], descendants: true, isSignal: true }, { propertyName: "endDayRef", first: true, predicate: ["endDayInput"], descendants: true, isSignal: true }, { propertyName: "endYearRef", first: true, predicate: ["endYearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: TnCalendarComponent, descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"initialRange()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
15960
+ ], viewQueries: [{ propertyName: "startMonthRef", first: true, predicate: ["startMonthInput"], descendants: true, isSignal: true }, { propertyName: "startDayRef", first: true, predicate: ["startDayInput"], descendants: true, isSignal: true }, { propertyName: "startYearRef", first: true, predicate: ["startYearInput"], descendants: true, isSignal: true }, { propertyName: "endMonthRef", first: true, predicate: ["endMonthInput"], descendants: true, isSignal: true }, { propertyName: "endDayRef", first: true, predicate: ["endDayInput"], descendants: true, isSignal: true }, { propertyName: "endYearRef", first: true, predicate: ["endYearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "activeDate", "locale", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
15186
15961
  }
15187
15962
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateRangeInputComponent, decorators: [{
15188
15963
  type: Component,
@@ -15194,8 +15969,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
15194
15969
  }
15195
15970
  ], host: {
15196
15971
  'class': 'tn-date-range-input'
15197
- }, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"initialRange()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
15198
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TnCalendarComponent), { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
15972
+ }, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
15973
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
15199
15974
 
15200
15975
  /**
15201
15976
  * Harness for interacting with `tn-date-range-input` in tests.
@@ -18948,14 +19723,15 @@ var TnToastPosition;
18948
19723
  TnToastPosition["Bottom"] = "bottom";
18949
19724
  })(TnToastPosition || (TnToastPosition = {}));
18950
19725
 
18951
- // Material icons render via the `material-icons` CSS font (see icon.component.ts),
18952
- // so they don't need sprite scanning — the literal `mat-` prefix matches what
18953
- // the runtime icon resolver would produce from a Material library name.
19726
+ // The sprite generator only sees icon names in templates and in `tnIconMarker()` calls.
19727
+ // These live in a map, so without the marker they'd be dropped from the sprite on the
19728
+ // next regeneration — and there is no `material-icons` font in this library to fall
19729
+ // back to, so the toast would render an empty icon.
18954
19730
  const TOAST_ICONS = {
18955
- [TnToastType.Info]: 'mat-info',
18956
- [TnToastType.Success]: 'mat-check_circle',
18957
- [TnToastType.Warning]: 'mat-warning',
18958
- [TnToastType.Error]: 'mat-error',
19731
+ [TnToastType.Info]: tnIconMarker('info', 'material'),
19732
+ [TnToastType.Success]: tnIconMarker('check_circle', 'material'),
19733
+ [TnToastType.Warning]: tnIconMarker('warning', 'material'),
19734
+ [TnToastType.Error]: tnIconMarker('error', 'material'),
18959
19735
  };
18960
19736
  class TnToastComponent {
18961
19737
  message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : []));
@@ -19792,5 +20568,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
19792
20568
  * Generated bundle index. Do not edit.
19793
20569
  */
19794
20570
 
19795
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
20571
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_CALENDAR_INTL, TN_CALENDAR_INTL_DEFAULTS, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarCellHarness, TnCalendarComponent, TnCalendarHarness, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnCalendarIntl, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
19796
20572
  //# sourceMappingURL=truenas-ui-components.mjs.map