@melodicdev/components 1.5.11 → 1.5.13

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.
@@ -34,6 +34,14 @@ export declare class ButtonComponent implements IElementRef, OnInit {
34
34
  loading: boolean;
35
35
  /** Make button full width */
36
36
  fullWidth: boolean;
37
+ /** If set, renders as an anchor tag instead of a button */
38
+ href: string | null;
39
+ /** Anchor target (only used when href is set) */
40
+ target: string | null;
41
+ /** Anchor rel (only used when href is set) */
42
+ rel: string | null;
43
+ /** Anchor download (only used when href is set) */
44
+ download: string | null;
37
45
  onInit(): void;
38
46
  /** Whether the button is effectively disabled */
39
47
  get isDisabled(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"button.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button.component.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,6CAA6C,CAAC;AAIrD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAMa,eAAgB,YAAW,WAAW,EAAE,MAAM;IACnD,UAAU,EAAG,WAAW,CAAC;IAEhC,2BAA2B;IACpB,OAAO,EAAE,aAAa,CAAa;IAE1C,kBAAkB;IACX,IAAI,EAAE,IAAI,CAAQ;IAEzB,uBAAuB;IAChB,IAAI,EAAE,UAAU,CAAY;IAEnC,yBAAyB;IAClB,QAAQ,UAAS;IAExB,yBAAyB;IAClB,OAAO,UAAS;IAEvB,6BAA6B;IACtB,SAAS,UAAS;IAElB,MAAM,IAAI,IAAI;IAOrB,iDAAiD;IACjD,IAAW,UAAU,IAAI,OAAO,CAE/B;IAED,0BAA0B;IACnB,WAAW,GAAI,OAAO,UAAU,KAAG,IAAI,CAe5C;CACF"}
1
+ {"version":3,"file":"button.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button.component.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,6CAA6C,CAAC;AAIrD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAMa,eAAgB,YAAW,WAAW,EAAE,MAAM;IACnD,UAAU,EAAG,WAAW,CAAC;IAEhC,2BAA2B;IACpB,OAAO,EAAE,aAAa,CAAa;IAE1C,kBAAkB;IACX,IAAI,EAAE,IAAI,CAAQ;IAEzB,uBAAuB;IAChB,IAAI,EAAE,UAAU,CAAY;IAEnC,yBAAyB;IAClB,QAAQ,UAAS;IAExB,yBAAyB;IAClB,OAAO,UAAS;IAEvB,6BAA6B;IACtB,SAAS,UAAS;IAEzB,2DAA2D;IACpD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC,iDAAiD;IAC1C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEpC,8CAA8C;IACvC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEjC,mDAAmD;IAC5C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,MAAM,IAAI,IAAI;IAOrB,iDAAiD;IACjD,IAAW,UAAU,IAAI,OAAO,CAE/B;IAED,0BAA0B;IACnB,WAAW,GAAI,OAAO,UAAU,KAAG,IAAI,CAe5C;CACF"}
@@ -41,6 +41,14 @@ let ButtonComponent = class ButtonComponent {
41
41
  this.loading = false;
42
42
  /** Make button full width */
43
43
  this.fullWidth = false;
44
+ /** If set, renders as an anchor tag instead of a button */
45
+ this.href = null;
46
+ /** Anchor target (only used when href is set) */
47
+ this.target = null;
48
+ /** Anchor rel (only used when href is set) */
49
+ this.rel = null;
50
+ /** Anchor download (only used when href is set) */
51
+ this.download = null;
44
52
  /** Handle click events */
45
53
  this.handleClick = (event) => {
46
54
  if (this.isDisabled) {
@@ -72,7 +80,7 @@ ButtonComponent = __decorate([
72
80
  selector: 'ml-button',
73
81
  template: buttonTemplate,
74
82
  styles: buttonStyles,
75
- attributes: ['variant', 'size', 'type', 'disabled', 'loading', 'full-width']
83
+ attributes: ['variant', 'size', 'type', 'disabled', 'loading', 'full-width', 'href', 'target', 'rel', 'download']
76
84
  })
77
85
  ], ButtonComponent);
78
86
  export { ButtonComponent };
@@ -1 +1 @@
1
- {"version":3,"file":"button.template.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,6CAgChD"}
1
+ {"version":3,"file":"button.template.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,6CAwDhD"}
@@ -1,31 +1,52 @@
1
1
  import { html, classMap, when } from '@melodicdev/core';
2
2
  export function buttonTemplate(c) {
3
- return html `
4
- <button
5
- type="${c.type}"
6
- class=${classMap({
3
+ const classes = classMap({
7
4
  'ml-button': true,
8
5
  [`ml-button--${c.variant}`]: true,
9
6
  [`ml-button--${c.size}`]: true,
10
7
  'ml-button--disabled': c.isDisabled,
11
8
  'ml-button--loading': c.loading,
12
9
  'ml-button--full-width': c.fullWidth
13
- })}
10
+ });
11
+ const content = html `
12
+ ${when(c.loading, () => html `
13
+ <span class="ml-button__spinner">
14
+ <ml-spinner size="sm"></ml-spinner>
15
+ </span>
16
+ `)}
17
+ <span class="ml-button__content">
18
+ <slot name="icon-start"></slot>
19
+ <slot></slot>
20
+ <slot name="icon-end"></slot>
21
+ </span>
22
+ `;
23
+ if (c.href != null) {
24
+ return html `
25
+ <a
26
+ href=${c.isDisabled ? undefined : c.href}
27
+ target=${c.target ?? undefined}
28
+ rel=${c.rel ?? undefined}
29
+ download=${c.download ?? undefined}
30
+ class=${classes}
31
+ role="button"
32
+ @click=${c.handleClick}
33
+ aria-disabled=${c.isDisabled ? 'true' : 'false'}
34
+ aria-busy=${c.loading ? 'true' : 'false'}
35
+ >
36
+ ${content}
37
+ </a>
38
+ `;
39
+ }
40
+ return html `
41
+ <button
42
+ type="${c.type}"
43
+ class=${classes}
14
44
  ?disabled=${c.isDisabled}
15
45
  @click=${c.handleClick}
16
46
  aria-disabled=${c.isDisabled ? 'true' : 'false'}
17
47
  aria-busy=${c.loading ? 'true' : 'false'}
18
48
  >
19
- ${when(c.loading, () => html `
20
- <span class="ml-button__spinner">
21
- <ml-spinner size="sm"></ml-spinner>
22
- </span>
23
- `)}
24
- <span class="ml-button__content">
25
- <slot name="icon-start"></slot>
26
- <slot></slot>
27
- <slot name="icon-end"></slot>
28
- </span>
49
+ ${content}
29
50
  </button>
30
51
  `;
31
52
  }
@@ -9,6 +9,21 @@ export interface CalendarDay {
9
9
  isSelected: boolean;
10
10
  isDisabled: boolean;
11
11
  }
12
+ export interface CalendarMonth {
13
+ index: number;
14
+ label: string;
15
+ isSelected: boolean;
16
+ isCurrent: boolean;
17
+ isDisabled: boolean;
18
+ }
19
+ export interface CalendarYear {
20
+ year: number;
21
+ isSelected: boolean;
22
+ isCurrent: boolean;
23
+ isDisabled: boolean;
24
+ isPlaceholder: boolean;
25
+ }
26
+ export type CalendarView = 'day' | 'month' | 'year';
12
27
  /**
13
28
  * ml-calendar - Standalone calendar grid
14
29
  *
@@ -16,6 +31,7 @@ export interface CalendarDay {
16
31
  * ```html
17
32
  * <ml-calendar value="2026-02-08"></ml-calendar>
18
33
  * <ml-calendar min="2026-01-01" max="2026-12-31"></ml-calendar>
34
+ * <ml-calendar min-year="1900" max-year="2030"></ml-calendar>
19
35
  * ```
20
36
  *
21
37
  * @fires ml:select - Emitted when a date is selected. Detail: { value: string }
@@ -28,22 +44,54 @@ export declare class CalendarComponent implements IElementRef, OnInit, OnAttribu
28
44
  min: string;
29
45
  /** Maximum selectable date (YYYY-MM-DD) */
30
46
  max: string;
47
+ /** Earliest year reachable in the year picker (defaults to currentYear - 120) */
48
+ minYear: number | string;
49
+ /** Latest year reachable in the year picker (defaults to currentYear + 10) */
50
+ maxYear: number | string;
31
51
  /** Currently viewed month (0-11) */
32
52
  viewMonth: number;
33
53
  /** Currently viewed year */
34
54
  viewYear: number;
55
+ /** Which sub-view the calendar is showing */
56
+ view: CalendarView;
57
+ /** First year shown on the current page of the year grid */
58
+ yearPageStart: number;
35
59
  onInit(): void;
36
60
  onAttributeChange(name: string, _: unknown, newVal: unknown): void;
37
61
  private navigateToValue;
38
62
  get monthLabel(): string;
63
+ get yearLabel(): string;
64
+ get yearRangeLabel(): string;
65
+ get headerLabel(): string;
39
66
  get weekdays(): string[];
67
+ get resolvedMinYear(): number;
68
+ get resolvedMaxYear(): number;
40
69
  get days(): CalendarDay[];
70
+ get months(): CalendarMonth[];
71
+ get years(): CalendarYear[];
72
+ get canPageYearsBack(): boolean;
73
+ get canPageYearsForward(): boolean;
41
74
  prevYear: () => void;
42
75
  nextYear: () => void;
43
76
  prevMonth: () => void;
44
77
  nextMonth: () => void;
78
+ headerPrev: () => void;
79
+ headerNext: () => void;
80
+ headerPrevFar: () => void;
81
+ headerNextFar: () => void;
82
+ prevYearPage: () => void;
83
+ nextYearPage: () => void;
84
+ openMonthView: () => void;
85
+ openYearView: () => void;
86
+ selectViewMonth: (month: CalendarMonth) => void;
87
+ selectViewYear: (year: CalendarYear) => void;
45
88
  selectDay: (day: CalendarDay) => void;
46
89
  goToToday: () => void;
90
+ handleGridKeyDown: (event: KeyboardEvent) => void;
91
+ private computeYearPageStart;
92
+ private selectedMonthForYear;
93
+ private selectedYear;
94
+ private isMonthDisabled;
47
95
  private isDisabled;
48
96
  }
49
97
  //# sourceMappingURL=calendar.component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-picker/calendar.component.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAI/E,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;CACpB;AAeD;;;;;;;;;;GAUG;AACH,qBAMa,iBAAkB,YAAW,WAAW,EAAE,MAAM,EAAE,iBAAiB;IACxE,UAAU,EAAG,WAAW,CAAC;IAEhC,+CAA+C;IACxC,KAAK,SAAM;IAElB,2CAA2C;IACpC,GAAG,SAAM;IAEhB,2CAA2C;IACpC,GAAG,SAAM;IAEhB,oCAAoC;IAC7B,SAAS,SAAyB;IAEzC,4BAA4B;IACrB,QAAQ,SAA4B;IAEpC,MAAM,IAAI,IAAI;IAId,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI;IAMzE,OAAO,CAAC,eAAe;IASvB,IAAW,UAAU,IAAI,MAAM,CAE9B;IAED,IAAW,QAAQ,IAAI,MAAM,EAAE,CAE9B;IAED,IAAW,IAAI,IAAI,WAAW,EAAE,CAiE/B;IAEM,QAAQ,QAAO,IAAI,CAExB;IAEK,QAAQ,QAAO,IAAI,CAExB;IAEK,SAAS,QAAO,IAAI,CAOzB;IAEK,SAAS,QAAO,IAAI,CAOzB;IAEK,SAAS,GAAI,KAAK,WAAW,KAAG,IAAI,CAazC;IAEK,SAAS,QAAO,IAAI,CAezB;IAEF,OAAO,CAAC,UAAU;CAKlB"}
1
+ {"version":3,"file":"calendar.component.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-picker/calendar.component.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,11 @@ import { MelodicComponent } from '@melodicdev/core';
8
8
  import { calendarTemplate } from './calendar.template.js';
9
9
  import { calendarStyles } from './calendar.styles.js';
10
10
  const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
11
+ const MONTH_NAMES_SHORT = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
11
12
  const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
13
+ const YEARS_PER_PAGE = 12;
14
+ const DEFAULT_MIN_YEAR_OFFSET = 120;
15
+ const DEFAULT_MAX_YEAR_OFFSET = 10;
12
16
  function toIso(year, month, day) {
13
17
  return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
14
18
  }
@@ -16,6 +20,16 @@ function todayIso() {
16
20
  const d = new Date();
17
21
  return toIso(d.getFullYear(), d.getMonth(), d.getDate());
18
22
  }
23
+ function parseYear(val) {
24
+ if (typeof val === 'number' && Number.isFinite(val))
25
+ return val;
26
+ if (typeof val === 'string' && val.trim() !== '') {
27
+ const n = Number.parseInt(val, 10);
28
+ if (Number.isFinite(n))
29
+ return n;
30
+ }
31
+ return null;
32
+ }
19
33
  /**
20
34
  * ml-calendar - Standalone calendar grid
21
35
  *
@@ -23,6 +37,7 @@ function todayIso() {
23
37
  * ```html
24
38
  * <ml-calendar value="2026-02-08"></ml-calendar>
25
39
  * <ml-calendar min="2026-01-01" max="2026-12-31"></ml-calendar>
40
+ * <ml-calendar min-year="1900" max-year="2030"></ml-calendar>
26
41
  * ```
27
42
  *
28
43
  * @fires ml:select - Emitted when a date is selected. Detail: { value: string }
@@ -35,10 +50,19 @@ let CalendarComponent = class CalendarComponent {
35
50
  this.min = '';
36
51
  /** Maximum selectable date (YYYY-MM-DD) */
37
52
  this.max = '';
53
+ /** Earliest year reachable in the year picker (defaults to currentYear - 120) */
54
+ this.minYear = '';
55
+ /** Latest year reachable in the year picker (defaults to currentYear + 10) */
56
+ this.maxYear = '';
38
57
  /** Currently viewed month (0-11) */
39
58
  this.viewMonth = new Date().getMonth();
40
59
  /** Currently viewed year */
41
60
  this.viewYear = new Date().getFullYear();
61
+ /** Which sub-view the calendar is showing */
62
+ this.view = 'day';
63
+ /** First year shown on the current page of the year grid */
64
+ this.yearPageStart = 0;
65
+ // ── Navigation ──────────────────────────────────────────────────────────
42
66
  this.prevYear = () => {
43
67
  this.viewYear--;
44
68
  };
@@ -63,11 +87,73 @@ let CalendarComponent = class CalendarComponent {
63
87
  this.viewMonth++;
64
88
  }
65
89
  };
90
+ this.headerPrev = () => {
91
+ if (this.view === 'year')
92
+ this.prevYearPage();
93
+ else if (this.view === 'month')
94
+ this.prevYear();
95
+ else
96
+ this.prevMonth();
97
+ };
98
+ this.headerNext = () => {
99
+ if (this.view === 'year')
100
+ this.nextYearPage();
101
+ else if (this.view === 'month')
102
+ this.nextYear();
103
+ else
104
+ this.nextMonth();
105
+ };
106
+ this.headerPrevFar = () => {
107
+ if (this.view === 'year')
108
+ this.prevYearPage();
109
+ else
110
+ this.prevYear();
111
+ };
112
+ this.headerNextFar = () => {
113
+ if (this.view === 'year')
114
+ this.nextYearPage();
115
+ else
116
+ this.nextYear();
117
+ };
118
+ this.prevYearPage = () => {
119
+ const next = this.yearPageStart - YEARS_PER_PAGE;
120
+ const minPage = this.computeYearPageStart(this.resolvedMinYear);
121
+ this.yearPageStart = Math.max(next, minPage);
122
+ };
123
+ this.nextYearPage = () => {
124
+ const next = this.yearPageStart + YEARS_PER_PAGE;
125
+ const maxPage = this.computeYearPageStart(this.resolvedMaxYear);
126
+ this.yearPageStart = Math.min(next, maxPage);
127
+ };
128
+ // ── View switching ──────────────────────────────────────────────────────
129
+ this.openMonthView = () => {
130
+ this.view = this.view === 'month' ? 'day' : 'month';
131
+ };
132
+ this.openYearView = () => {
133
+ if (this.view === 'year') {
134
+ this.view = 'day';
135
+ return;
136
+ }
137
+ this.yearPageStart = this.computeYearPageStart(this.viewYear);
138
+ this.view = 'year';
139
+ };
140
+ this.selectViewMonth = (month) => {
141
+ if (month.isDisabled)
142
+ return;
143
+ this.viewMonth = month.index;
144
+ this.view = 'day';
145
+ };
146
+ this.selectViewYear = (year) => {
147
+ if (year.isDisabled)
148
+ return;
149
+ this.viewYear = year.year;
150
+ this.view = 'month';
151
+ };
152
+ // ── Day selection ───────────────────────────────────────────────────────
66
153
  this.selectDay = (day) => {
67
154
  if (day.isDisabled)
68
155
  return;
69
156
  this.value = day.iso;
70
- // Navigate to the selected day's month if it's not the current view
71
157
  this.viewMonth = day.month;
72
158
  this.viewYear = day.year;
73
159
  this.elementRef.dispatchEvent(new CustomEvent('ml:select', {
@@ -80,6 +166,7 @@ let CalendarComponent = class CalendarComponent {
80
166
  const now = new Date();
81
167
  this.viewMonth = now.getMonth();
82
168
  this.viewYear = now.getFullYear();
169
+ this.view = 'day';
83
170
  const iso = todayIso();
84
171
  if (!this.isDisabled(iso)) {
85
172
  this.value = iso;
@@ -90,13 +177,62 @@ let CalendarComponent = class CalendarComponent {
90
177
  }));
91
178
  }
92
179
  };
180
+ // ── Keyboard navigation ─────────────────────────────────────────────────
181
+ this.handleGridKeyDown = (event) => {
182
+ const key = event.key;
183
+ if (key !== 'ArrowLeft' && key !== 'ArrowRight' && key !== 'ArrowUp' && key !== 'ArrowDown' && key !== 'Home' && key !== 'End') {
184
+ return;
185
+ }
186
+ const target = event.target;
187
+ if (!target || target.tagName !== 'BUTTON')
188
+ return;
189
+ const grid = target.closest('.ml-calendar__grid, .ml-calendar__cell-grid');
190
+ if (!grid)
191
+ return;
192
+ const cells = Array.from(grid.querySelectorAll('button:not([disabled])')).filter((el) => el.tabIndex !== -1);
193
+ if (cells.length === 0)
194
+ return;
195
+ const idx = cells.indexOf(target);
196
+ if (idx === -1)
197
+ return;
198
+ const cols = this.view === 'day' ? 7 : 3;
199
+ let nextIdx = idx;
200
+ switch (key) {
201
+ case 'ArrowLeft':
202
+ nextIdx = idx - 1;
203
+ break;
204
+ case 'ArrowRight':
205
+ nextIdx = idx + 1;
206
+ break;
207
+ case 'ArrowUp':
208
+ nextIdx = idx - cols;
209
+ break;
210
+ case 'ArrowDown':
211
+ nextIdx = idx + cols;
212
+ break;
213
+ case 'Home':
214
+ nextIdx = 0;
215
+ break;
216
+ case 'End':
217
+ nextIdx = cells.length - 1;
218
+ break;
219
+ }
220
+ if (nextIdx < 0 || nextIdx >= cells.length) {
221
+ event.preventDefault();
222
+ return;
223
+ }
224
+ event.preventDefault();
225
+ cells[nextIdx].focus();
226
+ };
93
227
  }
94
228
  onInit() {
95
229
  this.navigateToValue();
230
+ this.yearPageStart = this.computeYearPageStart(this.viewYear);
96
231
  }
97
232
  onAttributeChange(name, _, newVal) {
98
233
  if (name === 'value' && newVal) {
99
234
  this.navigateToValue();
235
+ this.yearPageStart = this.computeYearPageStart(this.viewYear);
100
236
  }
101
237
  }
102
238
  navigateToValue() {
@@ -109,11 +245,32 @@ let CalendarComponent = class CalendarComponent {
109
245
  }
110
246
  }
111
247
  get monthLabel() {
112
- return `${MONTH_NAMES[this.viewMonth]} ${this.viewYear}`;
248
+ return MONTH_NAMES[this.viewMonth];
249
+ }
250
+ get yearLabel() {
251
+ return String(this.viewYear);
252
+ }
253
+ get yearRangeLabel() {
254
+ return `${this.yearPageStart} – ${this.yearPageStart + YEARS_PER_PAGE - 1}`;
255
+ }
256
+ get headerLabel() {
257
+ if (this.view === 'year')
258
+ return this.yearRangeLabel;
259
+ if (this.view === 'month')
260
+ return this.yearLabel;
261
+ return `${this.monthLabel} ${this.viewYear}`;
113
262
  }
114
263
  get weekdays() {
115
264
  return WEEKDAYS;
116
265
  }
266
+ get resolvedMinYear() {
267
+ const parsed = parseYear(this.minYear);
268
+ return parsed ?? new Date().getFullYear() - DEFAULT_MIN_YEAR_OFFSET;
269
+ }
270
+ get resolvedMaxYear() {
271
+ const parsed = parseYear(this.maxYear);
272
+ return parsed ?? new Date().getFullYear() + DEFAULT_MAX_YEAR_OFFSET;
273
+ }
117
274
  get days() {
118
275
  const year = this.viewYear;
119
276
  const month = this.viewMonth;
@@ -175,6 +332,77 @@ let CalendarComponent = class CalendarComponent {
175
332
  }
176
333
  return result;
177
334
  }
335
+ get months() {
336
+ const selectedMonth = this.selectedMonthForYear(this.viewYear);
337
+ const now = new Date();
338
+ return MONTH_NAMES_SHORT.map((label, index) => ({
339
+ index,
340
+ label,
341
+ isSelected: selectedMonth === index,
342
+ isCurrent: now.getFullYear() === this.viewYear && now.getMonth() === index,
343
+ isDisabled: this.isMonthDisabled(this.viewYear, index)
344
+ }));
345
+ }
346
+ get years() {
347
+ const result = [];
348
+ const selectedYear = this.selectedYear();
349
+ const now = new Date().getFullYear();
350
+ const minY = this.resolvedMinYear;
351
+ const maxY = this.resolvedMaxYear;
352
+ for (let i = 0; i < YEARS_PER_PAGE; i++) {
353
+ const year = this.yearPageStart + i;
354
+ const inRange = year >= minY && year <= maxY;
355
+ result.push({
356
+ year,
357
+ isSelected: selectedYear === year,
358
+ isCurrent: year === now,
359
+ isDisabled: !inRange,
360
+ isPlaceholder: !inRange && (year < minY || year > maxY)
361
+ });
362
+ }
363
+ return result;
364
+ }
365
+ get canPageYearsBack() {
366
+ return this.yearPageStart > this.resolvedMinYear;
367
+ }
368
+ get canPageYearsForward() {
369
+ return this.yearPageStart + YEARS_PER_PAGE - 1 < this.resolvedMaxYear;
370
+ }
371
+ // ── Helpers ─────────────────────────────────────────────────────────────
372
+ computeYearPageStart(year) {
373
+ const minY = this.resolvedMinYear;
374
+ return minY + Math.floor((year - minY) / YEARS_PER_PAGE) * YEARS_PER_PAGE;
375
+ }
376
+ selectedMonthForYear(year) {
377
+ if (!this.value)
378
+ return null;
379
+ const parts = this.value.split('-');
380
+ if (parts.length !== 3)
381
+ return null;
382
+ const y = Number.parseInt(parts[0], 10);
383
+ if (y !== year)
384
+ return null;
385
+ return Number.parseInt(parts[1], 10) - 1;
386
+ }
387
+ selectedYear() {
388
+ if (!this.value)
389
+ return null;
390
+ const parts = this.value.split('-');
391
+ if (parts.length !== 3)
392
+ return null;
393
+ return Number.parseInt(parts[0], 10);
394
+ }
395
+ isMonthDisabled(year, month) {
396
+ // A month is disabled only if its entire range falls outside min/max.
397
+ const lastDay = new Date(year, month + 1, 0).getDate();
398
+ const monthEnd = toIso(year, month, lastDay);
399
+ const monthStart = toIso(year, month, 1);
400
+ if (this.min && monthEnd < this.min)
401
+ return true;
402
+ if (this.max && monthStart > this.max)
403
+ return true;
404
+ return false;
405
+ }
178
406
  isDisabled(iso) {
179
407
  if (this.min && iso < this.min)
180
408
  return true;
@@ -188,7 +416,7 @@ CalendarComponent = __decorate([
188
416
  selector: 'ml-calendar',
189
417
  template: calendarTemplate,
190
418
  styles: calendarStyles,
191
- attributes: ['value', 'min', 'max']
419
+ attributes: ['value', 'min', 'max', 'min-year', 'max-year']
192
420
  })
193
421
  ], CalendarComponent);
194
422
  export { CalendarComponent };
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-picker/calendar.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,iDAqP1B,CAAC"}
1
+ {"version":3,"file":"calendar.styles.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-picker/calendar.styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,iDA2V1B,CAAC"}
@@ -7,10 +7,28 @@ export const calendarStyles = () => css `
7
7
  --ml-calendar-width: 280px;
8
8
  --ml-calendar-font-family: var(--ml-font-sans);
9
9
 
10
- /* --- Month label --- */
10
+ /* --- Month/year title --- */
11
11
  --ml-calendar-month-font-size: var(--ml-text-sm);
12
12
  --ml-calendar-month-font-weight: var(--ml-font-semibold);
13
13
  --ml-calendar-month-color: var(--ml-color-text);
14
+ --ml-calendar-title-btn-padding-x: var(--ml-space-1-5);
15
+ --ml-calendar-title-btn-padding-y: var(--ml-space-1);
16
+ --ml-calendar-title-btn-border-radius: var(--ml-radius-md);
17
+ --ml-calendar-title-btn-hover-bg: var(--ml-color-surface-raised);
18
+ --ml-calendar-title-btn-hover-color: var(--ml-color-text);
19
+
20
+ /* --- Month/year cells --- */
21
+ --ml-calendar-cell-font-size: var(--ml-text-sm);
22
+ --ml-calendar-cell-font-weight: var(--ml-font-regular);
23
+ --ml-calendar-cell-color: var(--ml-color-text);
24
+ --ml-calendar-cell-border-radius: var(--ml-radius-md);
25
+ --ml-calendar-cell-hover-bg: var(--ml-color-surface-raised);
26
+ --ml-calendar-cell-selected-bg: var(--ml-color-primary);
27
+ --ml-calendar-cell-selected-color: var(--ml-color-text-inverse);
28
+ --ml-calendar-cell-selected-font-weight: var(--ml-font-semibold);
29
+ --ml-calendar-cell-selected-hover-bg: var(--ml-color-primary-hover);
30
+ --ml-calendar-cell-current-font-weight: var(--ml-font-semibold);
31
+ --ml-calendar-cell-height: 3rem;
14
32
 
15
33
  /* --- Nav buttons --- */
16
34
  --ml-calendar-nav-size: 2rem;
@@ -84,10 +102,94 @@ export const calendarStyles = () => css `
84
102
  gap: 0;
85
103
  }
86
104
 
87
- .ml-calendar__month-label {
105
+ .ml-calendar__title {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: var(--ml-space-1);
109
+ }
110
+
111
+ .ml-calendar__title-btn {
112
+ border: none;
113
+ background: none;
114
+ padding: var(--ml-calendar-title-btn-padding-y) var(--ml-calendar-title-btn-padding-x);
115
+ border-radius: var(--ml-calendar-title-btn-border-radius);
116
+ font-family: inherit;
88
117
  font-size: var(--ml-calendar-month-font-size);
89
118
  font-weight: var(--ml-calendar-month-font-weight);
90
119
  color: var(--ml-calendar-month-color);
120
+ cursor: pointer;
121
+ transition: background-color var(--ml-calendar-transition-duration) var(--ml-calendar-transition-easing), color var(--ml-calendar-transition-duration) var(--ml-calendar-transition-easing);
122
+ }
123
+
124
+ .ml-calendar__title-btn:hover {
125
+ background-color: var(--ml-calendar-title-btn-hover-bg);
126
+ color: var(--ml-calendar-title-btn-hover-color);
127
+ }
128
+
129
+ .ml-calendar__title-btn:focus-visible {
130
+ outline: none;
131
+ box-shadow: var(--ml-calendar-focus-shadow);
132
+ }
133
+
134
+ .ml-calendar__title-btn--wide {
135
+ min-width: 0;
136
+ }
137
+
138
+ /* Month / year cell grid */
139
+ .ml-calendar__cell-grid {
140
+ display: grid;
141
+ grid-template-columns: repeat(3, 1fr);
142
+ gap: var(--ml-space-1);
143
+ padding: var(--ml-space-1) 0;
144
+ }
145
+
146
+ .ml-calendar__cell {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ height: var(--ml-calendar-cell-height);
151
+ border: none;
152
+ border-radius: var(--ml-calendar-cell-border-radius);
153
+ background: none;
154
+ font-family: inherit;
155
+ font-size: var(--ml-calendar-cell-font-size);
156
+ font-weight: var(--ml-calendar-cell-font-weight);
157
+ color: var(--ml-calendar-cell-color);
158
+ cursor: pointer;
159
+ padding: 0;
160
+ transition:
161
+ background-color var(--ml-calendar-transition-duration) var(--ml-calendar-transition-easing),
162
+ color var(--ml-calendar-transition-duration) var(--ml-calendar-transition-easing);
163
+ }
164
+
165
+ .ml-calendar__cell:hover:not(:disabled):not(.ml-calendar__cell--selected) {
166
+ background-color: var(--ml-calendar-cell-hover-bg);
167
+ }
168
+
169
+ .ml-calendar__cell:focus-visible {
170
+ outline: none;
171
+ box-shadow: var(--ml-calendar-focus-shadow);
172
+ z-index: 1;
173
+ }
174
+
175
+ .ml-calendar__cell--current {
176
+ font-weight: var(--ml-calendar-cell-current-font-weight);
177
+ }
178
+
179
+ .ml-calendar__cell--selected {
180
+ background-color: var(--ml-calendar-cell-selected-bg);
181
+ color: var(--ml-calendar-cell-selected-color);
182
+ font-weight: var(--ml-calendar-cell-selected-font-weight);
183
+ }
184
+
185
+ .ml-calendar__cell--selected:hover:not(:disabled) {
186
+ background-color: var(--ml-calendar-cell-selected-hover-bg);
187
+ }
188
+
189
+ .ml-calendar__cell--disabled,
190
+ .ml-calendar__cell:disabled {
191
+ opacity: var(--ml-calendar-disabled-opacity);
192
+ cursor: not-allowed;
91
193
  }
92
194
 
93
195
  .ml-calendar__nav {
@@ -1 +1 @@
1
- {"version":3,"file":"calendar.template.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-picker/calendar.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,yBAAyB,CAAC;AAE9E,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,iBAAiB,6CAyDpD"}
1
+ {"version":3,"file":"calendar.template.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/date-picker/calendar.template.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAA4C,MAAM,yBAAyB,CAAC;AAqF3G,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,iBAAiB,6CAsEpD"}