@skra/ui 0.4.0 → 0.5.0

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.
Files changed (60) hide show
  1. package/fesm2022/skra-ui-accordion.mjs +2 -2
  2. package/fesm2022/skra-ui-accordion.mjs.map +1 -1
  3. package/fesm2022/skra-ui-alert.mjs +2 -2
  4. package/fesm2022/skra-ui-alert.mjs.map +1 -1
  5. package/fesm2022/skra-ui-avatar.mjs.map +1 -1
  6. package/fesm2022/skra-ui-badge.mjs.map +1 -1
  7. package/fesm2022/skra-ui-button.mjs +2 -2
  8. package/fesm2022/skra-ui-button.mjs.map +1 -1
  9. package/fesm2022/skra-ui-checkbox.mjs +2 -2
  10. package/fesm2022/skra-ui-checkbox.mjs.map +1 -1
  11. package/fesm2022/skra-ui-chip.mjs +2 -2
  12. package/fesm2022/skra-ui-chip.mjs.map +1 -1
  13. package/fesm2022/skra-ui-combobox.mjs +120 -120
  14. package/fesm2022/skra-ui-combobox.mjs.map +1 -1
  15. package/fesm2022/skra-ui-core.mjs +36 -36
  16. package/fesm2022/skra-ui-core.mjs.map +1 -1
  17. package/fesm2022/skra-ui-counter.mjs.map +1 -1
  18. package/fesm2022/skra-ui-datepicker.mjs +605 -190
  19. package/fesm2022/skra-ui-datepicker.mjs.map +1 -1
  20. package/fesm2022/skra-ui-dialog.mjs.map +1 -1
  21. package/fesm2022/skra-ui-field.mjs.map +1 -1
  22. package/fesm2022/skra-ui-icon.mjs.map +1 -1
  23. package/fesm2022/skra-ui-input.mjs +2 -2
  24. package/fesm2022/skra-ui-input.mjs.map +1 -1
  25. package/fesm2022/skra-ui-kbd.mjs.map +1 -1
  26. package/fesm2022/skra-ui-layout.mjs.map +1 -1
  27. package/fesm2022/skra-ui-link.mjs +2 -2
  28. package/fesm2022/skra-ui-link.mjs.map +1 -1
  29. package/fesm2022/skra-ui-loading-indicator.mjs.map +1 -1
  30. package/fesm2022/skra-ui-menu.mjs +2 -2
  31. package/fesm2022/skra-ui-menu.mjs.map +1 -1
  32. package/fesm2022/skra-ui-multi-select.mjs +94 -94
  33. package/fesm2022/skra-ui-multi-select.mjs.map +1 -1
  34. package/fesm2022/skra-ui-native-select.mjs +2 -2
  35. package/fesm2022/skra-ui-native-select.mjs.map +1 -1
  36. package/fesm2022/skra-ui-popover.mjs.map +1 -1
  37. package/fesm2022/skra-ui-progress.mjs.map +1 -1
  38. package/fesm2022/skra-ui-radio.mjs +2 -2
  39. package/fesm2022/skra-ui-radio.mjs.map +1 -1
  40. package/fesm2022/skra-ui-rating-display.mjs.map +1 -1
  41. package/fesm2022/skra-ui-select.mjs +92 -92
  42. package/fesm2022/skra-ui-select.mjs.map +1 -1
  43. package/fesm2022/skra-ui-skeleton.mjs +8 -8
  44. package/fesm2022/skra-ui-skeleton.mjs.map +1 -1
  45. package/fesm2022/skra-ui-star-rating.mjs +2 -2
  46. package/fesm2022/skra-ui-star-rating.mjs.map +1 -1
  47. package/fesm2022/skra-ui-status-dot.mjs.map +1 -1
  48. package/fesm2022/skra-ui-switch.mjs +2 -2
  49. package/fesm2022/skra-ui-switch.mjs.map +1 -1
  50. package/fesm2022/skra-ui-tabs.mjs +2 -2
  51. package/fesm2022/skra-ui-tabs.mjs.map +1 -1
  52. package/fesm2022/skra-ui-textarea.mjs +2 -2
  53. package/fesm2022/skra-ui-textarea.mjs.map +1 -1
  54. package/fesm2022/skra-ui-toast.mjs +2 -2
  55. package/fesm2022/skra-ui-toast.mjs.map +1 -1
  56. package/fesm2022/skra-ui-tooltip.mjs.map +1 -1
  57. package/fesm2022/skra-ui-typography.mjs.map +1 -1
  58. package/package.json +3 -3
  59. package/types/skra-ui-datepicker.d.ts +146 -46
  60. package/types/skra-ui-datepicker.d.ts.map +1 -1
@@ -1,10 +1,11 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, booleanAttribute, output, viewChild, inject, Injector, signal, computed, DestroyRef, afterNextRender, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { input, booleanAttribute, output, viewChild, inject, Injector, signal, linkedSignal, computed, DestroyRef, effect, untracked, afterNextRender, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { SkraFieldContext } from '@skra/ui/field';
4
4
  import { _attachPositioner } from '@skra/ui/core';
5
5
 
6
6
  /**
7
- * calendar-math — the ONE sanctioned site for DatePicker date arithmetic (ADR 0050 §3/§6).
7
+ * calendar-math — the ONE sanctioned site for DatePicker date arithmetic (ADR 0050 §3/§6) AND for every
8
+ * precision-aware key operation (ADR 0057 R3 — no parallel helper).
8
9
  *
9
10
  * Zoneless calendar-date discipline (the timezone off-by-one is the bug class this exists to prevent):
10
11
  * - The value is an ISO 8601 date-only string (`YYYY-MM-DD`) — NEVER materialized as a `Date`.
@@ -147,43 +148,178 @@ function monthGrid(y, m, firstDayOfWeek) {
147
148
  weeks.push(cells.slice(i, i + 7));
148
149
  return weeks;
149
150
  }
151
+ const ISO_MONTH = /^(\d{4})-(\d{2})$/;
152
+ /** Strict `YYYY-MM` parse. Returns `null` for anything invalid. */
153
+ function parseIsoMonth(iso) {
154
+ const match = ISO_MONTH.exec(iso);
155
+ if (!match)
156
+ return null;
157
+ const y = Number(match[1]);
158
+ const m = Number(match[2]);
159
+ if (m < 1 || m > 12)
160
+ return null;
161
+ return { y, m };
162
+ }
163
+ /** Serialize a calendar month to `YYYY-MM`. */
164
+ function toIsoMonth({ y, m }) {
165
+ return `${pad(y, 4)}-${pad(m, 2)}`;
166
+ }
167
+ /** `true` iff `iso` is a strictly valid `YYYY-MM` month key. */
168
+ function isValidIsoMonth(iso) {
169
+ return parseIsoMonth(iso) !== null;
170
+ }
171
+ /** `true` iff `iso` is strictly valid AT `precision` (ADR 0057 R3 — format follows precision). */
172
+ function isValidAt(iso, precision) {
173
+ return precision === 'month' ? isValidIsoMonth(iso) : isValidIso(iso);
174
+ }
175
+ /**
176
+ * Re-key an ISO key to `precision`, or `''` when it cannot be expressed there.
177
+ * Truncating down is a slice (`2026-06-14` → `2026-06`); widening up anchors to the first of the
178
+ * month (`2026-06` → `2026-06-01`), which is the only choice that keeps the key valid.
179
+ */
180
+ function reKey(key, precision) {
181
+ if (key === '')
182
+ return '';
183
+ if (precision === 'month') {
184
+ if (isValidIsoMonth(key))
185
+ return key;
186
+ return isValidIso(key) ? key.slice(0, 7) : '';
187
+ }
188
+ if (isValidIso(key))
189
+ return key;
190
+ return isValidIsoMonth(key) ? `${key}-01` : '';
191
+ }
192
+ /**
193
+ * Normalize a `min`/`max` bound to `precision`, or `''` (= unbounded) when it cannot be expressed there.
194
+ *
195
+ * ADR 0057 R4: in month precision a DAY bound's day component is deliberately ignored — `min="2026-03-15"`
196
+ * truncates to `2026-03`, so **March 2026 stays selectable**. The value is the month, not a day inside it,
197
+ * so a partially-in-range month is in range. In day precision a MONTH bound is rejected rather than
198
+ * guessed: widening it would mean `min` anchors to the 1st but `max` would have to anchor to the last day,
199
+ * and inventing that asymmetry silently is worse than treating the bound as unset.
200
+ */
201
+ function boundAt(bound, precision) {
202
+ if (bound === '')
203
+ return '';
204
+ if (precision === 'month')
205
+ return reKey(bound, 'month');
206
+ return isValidIso(bound) ? bound : '';
207
+ }
208
+ /**
209
+ * Inclusive range test AT `precision` — the candidate and BOTH bounds are truncated to the same
210
+ * precision before the lexicographic compare (ADR 0057 R4).
211
+ *
212
+ * Comparing mixed-precision keys byte-wise is the trap this exists to close: `'2026-03' < '2026-03-15'`
213
+ * is lexicographically true, so an un-truncated compare would silently disable a month that is in range.
214
+ */
215
+ function isInRangeAt(key, min, max, precision) {
216
+ const k = reKey(key, precision);
217
+ if (k === '')
218
+ return false;
219
+ return isInRange(k, boundAt(min, precision), boundAt(max, precision));
220
+ }
221
+ /** Clamp `key` into `[min,max]` AT `precision` (each `''` = unbounded). Seeds the displayed period. */
222
+ function clampAt(key, min, max, precision) {
223
+ const k = reKey(key, precision);
224
+ if (k === '')
225
+ return k;
226
+ return clampIso(k, boundAt(min, precision), boundAt(max, precision));
227
+ }
228
+ /** Add `n` months (may be negative) to a `YYYY-MM` key. No day component, so nothing to clamp. */
229
+ function addMonthsToKey(key, n) {
230
+ const month = parseIsoMonth(key);
231
+ if (!month)
232
+ return key;
233
+ const total = month.y * 12 + (month.m - 1) + n;
234
+ const y = Math.floor(total / 12);
235
+ const m = (((total % 12) + 12) % 12) + 1; // 1-based, safe for negative totals
236
+ return toIsoMonth({ y, m });
237
+ }
238
+ /** Add `n` years to a `YYYY-MM` key. Delegates to month arithmetic. */
239
+ function addYearsToKey(key, n) {
240
+ return addMonthsToKey(key, n * 12);
241
+ }
242
+ /** First month of the calendar quarter containing `key` (the month grid's ROW start — ADR 0057 §5). */
243
+ function quarterStartKey(key) {
244
+ const month = parseIsoMonth(key);
245
+ if (!month)
246
+ return key;
247
+ return toIsoMonth({ y: month.y, m: Math.floor((month.m - 1) / 3) * 3 + 1 });
248
+ }
249
+ /** Last month of the calendar quarter containing `key` (the month grid's ROW end). */
250
+ function quarterEndKey(key) {
251
+ return addMonthsToKey(quarterStartKey(key), 2);
252
+ }
253
+ /**
254
+ * Build a month grid for year `y`: the 12 months as `YYYY-MM` keys in **4 rows × 3 columns**.
255
+ * Rows are quarter-aligned, which is what makes Arrow Up/Down (∓3 months) a meaningful step, and
256
+ * 3 wide columns fit full localized month names at the panel width the 7-column day grid already sets.
257
+ * Unlike `monthGrid` there is no padding — a year always holds exactly 12 months.
258
+ */
259
+ function yearGrid(y) {
260
+ const rows = [];
261
+ for (let q = 0; q < 4; q++) {
262
+ const row = [];
263
+ for (let i = 1; i <= 3; i++)
264
+ row.push(toIsoMonth({ y, m: q * 3 + i }));
265
+ rows.push(row);
266
+ }
267
+ return rows;
268
+ }
269
+ /** Today's LOCAL calendar key at `precision` (local accessors — "today" is local-civil, ADR 0050 §3). */
270
+ function todayAt(precision) {
271
+ return reKey(today(), precision);
272
+ }
150
273
 
151
274
  const NONE = { type: 'none' };
275
+ /** Grid width per view — also the Arrow Up/Down step (one row). */
276
+ const COLUMNS = { day: 7, month: 3 };
152
277
  /**
153
278
  * SkraCalendarKeyboard — the WAI-ARIA APG **Date Picker Dialog** grid keyboard model (2-D), confined to
154
279
  * ONE file. The THIRD per-pattern keyboard controller and the FIRST that does NOT wrap a CDK key manager:
155
280
  * neither `FocusKeyManager` (Menu) nor `ActiveDescendantKeyManager` (Select/Combobox) models a 2-D grid
156
- * whose focusable cell set is recomputed on month change (ADR 0044 §7 note, 2026-06-14). Date arithmetic
281
+ * whose focusable cell set is recomputed on period change (ADR 0044 §7 note, 2026-06-14). Date arithmetic
157
282
  * is delegated to `calendar-math` (the ADR 0050 single math site); this controller only maps keys to
158
283
  * intents. Roving REAL focus (NOT `aria-activedescendant`) — the divergence from Select/Combobox.
159
284
  *
160
- * Navigation is unclamped (a user may move onto a disabled out-of-range day — APG-discoverable); only
285
+ * **One model, two unit sets (ADR 0057 R8).** The `day` and `month` views share every key, every intent and
286
+ * the whole `switch` below; they differ ONLY in what a step, a row and a page mean. That parameterization is
287
+ * the falsifiable claim behind treating precision as a runtime axis rather than a second component — if this
288
+ * ever needs a second `resolve`, the classification was wrong and the feature splits.
289
+ *
290
+ * | Key | `day` view | `month` view |
291
+ * | --------------------- | ----------------------------- | ----------------------------- |
292
+ * | Left / Right | ∓1 day | ∓1 month |
293
+ * | Up / Down | ∓7 days (one week row) | ∓3 months (one quarter row) |
294
+ * | Home / End | first / last day of the week | first / last month of the quarter |
295
+ * | PageUp / PageDown | ∓1 month (clamped) | ∓1 year |
296
+ * | Shift+PageUp / -Down | ∓1 year (clamped) | ∓10 years |
297
+ *
298
+ * Home/End stay ROW-scoped in both views — APG grid semantics, and one rule beats two.
299
+ *
300
+ * Navigation is unclamped (a user may move onto a disabled out-of-range cell — APG-discoverable); only
161
301
  * SELECTION is range-guarded, and that guard lives in the component (one place, shared with pointer click).
162
302
  */
163
303
  class SkraCalendarKeyboard {
164
304
  resolve(event, ctx) {
165
- const { focusedIso, firstDayOfWeek } = ctx;
305
+ const columns = COLUMNS[ctx.view];
166
306
  switch (event.key) {
167
307
  case 'ArrowLeft':
168
- return this.toFocus(focusedIso, addDays(focusedIso, -1));
308
+ return this.toFocus(this.step(ctx, -1));
169
309
  case 'ArrowRight':
170
- return this.toFocus(focusedIso, addDays(focusedIso, 1));
310
+ return this.toFocus(this.step(ctx, 1));
171
311
  case 'ArrowUp':
172
- return this.toFocus(focusedIso, addDays(focusedIso, -7));
312
+ return this.toFocus(this.step(ctx, -columns));
173
313
  case 'ArrowDown':
174
- return this.toFocus(focusedIso, addDays(focusedIso, 7));
314
+ return this.toFocus(this.step(ctx, columns));
175
315
  case 'Home':
176
- return this.toFocus(focusedIso, this.weekStart(focusedIso, firstDayOfWeek));
316
+ return this.toFocus(this.rowStart(ctx));
177
317
  case 'End':
178
- return this.toFocus(focusedIso, addDays(this.weekStart(focusedIso, firstDayOfWeek), 6));
318
+ return this.toFocus(this.rowEnd(ctx));
179
319
  case 'PageUp':
180
- return this.toFocus(focusedIso, event.shiftKey
181
- ? addYearsClamped(focusedIso, -1)
182
- : addMonthsClamped(focusedIso, -1));
320
+ return this.toFocus(this.page(ctx, event.shiftKey, -1));
183
321
  case 'PageDown':
184
- return this.toFocus(focusedIso, event.shiftKey
185
- ? addYearsClamped(focusedIso, 1)
186
- : addMonthsClamped(focusedIso, 1));
322
+ return this.toFocus(this.page(ctx, event.shiftKey, 1));
187
323
  case 'Enter':
188
324
  case ' ':
189
325
  case 'Spacebar': // legacy key name
@@ -194,17 +330,34 @@ class SkraCalendarKeyboard {
194
330
  return NONE;
195
331
  }
196
332
  }
197
- toFocus(fromIso, toIso) {
198
- return {
199
- type: 'focus',
200
- iso: toIso,
201
- monthChanged: this.monthChanged(fromIso, toIso),
202
- };
333
+ /** One cell = one day / one month. */
334
+ step(ctx, n) {
335
+ return ctx.view === 'month'
336
+ ? addMonthsToKey(ctx.focusedIso, n)
337
+ : addDays(ctx.focusedIso, n);
338
+ }
339
+ /** Row start: the week (day view) / the calendar quarter (month view). */
340
+ rowStart(ctx) {
341
+ return ctx.view === 'month'
342
+ ? quarterStartKey(ctx.focusedIso)
343
+ : this.weekStart(ctx.focusedIso, ctx.firstDayOfWeek);
344
+ }
345
+ /** Row end: last day of the week / last month of the quarter. */
346
+ rowEnd(ctx) {
347
+ return ctx.view === 'month'
348
+ ? quarterEndKey(ctx.focusedIso)
349
+ : addDays(this.weekStart(ctx.focusedIso, ctx.firstDayOfWeek), 6);
203
350
  }
204
- monthChanged(a, b) {
205
- const pa = parseIso(a);
206
- const pb = parseIso(b);
207
- return !pa || !pb || pa.y !== pb.y || pa.m !== pb.m;
351
+ /** Page = the next container up; Shift = the one above that. Day math stays clamped (ADR 0050 §4). */
352
+ page(ctx, shiftKey, direction) {
353
+ if (ctx.view === 'month')
354
+ return addYearsToKey(ctx.focusedIso, direction * (shiftKey ? 10 : 1));
355
+ return shiftKey
356
+ ? addYearsClamped(ctx.focusedIso, direction)
357
+ : addMonthsClamped(ctx.focusedIso, direction);
358
+ }
359
+ toFocus(toIso) {
360
+ return { type: 'focus', iso: toIso };
208
361
  }
209
362
  weekStart(iso, firstDayOfWeek) {
210
363
  const p = parseIso(iso);
@@ -222,11 +375,28 @@ class SkraCalendarKeyboard {
222
375
  * formatter MUST set `timeZone: 'UTC'` (else the displayed date shifts off the zoneless value).
223
376
  */
224
377
  const UTC = 'UTC';
378
+ /**
379
+ * The grid, `monthGrid()` and `formatDayNumber()` are all GREGORIAN — non-Gregorian calendars re-shape the
380
+ * grid, not just its language, and are out of scope (ADR 0050 Q-8). Every formatter here MUST therefore
381
+ * pin the calendar: left to its default, `Intl` follows the LOCALE's, and the widget then labels a
382
+ * Gregorian grid with another calendar's months and years. Measured before this was pinned: `fa-IR`
383
+ * showed `۱۴۰۴` in the panel header and `۱۴۰۵` in the trigger for ONE value, and a day cell displaying
384
+ * `۱۴` named `خرداد ۱۴۰۵` — which is 2026-06-04, not 2026-06-14. Localizing the LANGUAGE is the job;
385
+ * re-basing the CALENDAR is a different feature (ADR 0050 §6 adapter seam).
386
+ */
387
+ const GREGORY = 'gregory';
388
+ /** Zero-pad a year / month for building the ISO key a formatter is fed. No locale involved. */
389
+ function pad4(n) {
390
+ return String(n).padStart(4, '0');
391
+ }
392
+ function pad2(n) {
393
+ return String(n).padStart(2, '0');
394
+ }
225
395
  /** Resolve the host's default BCP-47 locale tag, SSR-safe (no `navigator` in Node). */
226
396
  function hostLocale() {
227
397
  return typeof navigator !== 'undefined' ? navigator.language : undefined;
228
398
  }
229
- /** Trigger display of the selected date (`dateStyle: 'medium'`). `''`/invalid → `''`. */
399
+ /** Trigger display of the selected date, `day` precision (`dateStyle: 'medium'`). `''`/invalid → `''`. */
230
400
  function formatValue(iso, locale) {
231
401
  const dt = toUtcDate(iso);
232
402
  if (!dt)
@@ -234,27 +404,93 @@ function formatValue(iso, locale) {
234
404
  return new Intl.DateTimeFormat(locale, {
235
405
  dateStyle: 'medium',
236
406
  timeZone: UTC,
407
+ calendar: GREGORY,
237
408
  }).format(dt);
238
409
  }
410
+ /**
411
+ * Trigger display of the selected month, `month` precision (ADR 0045 — the trigger shows the VALUE).
412
+ * Takes a `YYYY-MM` key; `''`/invalid → `''`.
413
+ */
414
+ function formatMonthValue(key, locale) {
415
+ const match = /^(\d{4})-(\d{2})$/.exec(key);
416
+ if (!match)
417
+ return '';
418
+ return formatMonthYear(Number(match[1]), Number(match[2]), locale);
419
+ }
239
420
  /** Header / nav-button label for a month: localized `month long + year` (e.g. "czerwiec 2026"). */
240
421
  function formatMonthYear(y, m, locale) {
241
- const dt = toUtcDate(`${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-01`);
422
+ const dt = toUtcDate(`${pad4(y)}-${pad2(m)}-01`);
242
423
  if (!dt)
243
424
  return '';
244
425
  return new Intl.DateTimeFormat(locale, {
245
426
  month: 'long',
246
427
  year: 'numeric',
247
428
  timeZone: UTC,
429
+ calendar: GREGORY,
248
430
  }).format(dt);
249
431
  }
250
- /** Full localized date for a day cell's `[skra-sr-only]` accessible-name extension (ADR 033 Path 2). */
251
- function formatDayName(iso, locale) {
432
+ /**
433
+ * The day cell's `[skra-sr-only]` accessible-name **extension** — the localized month and year, with the
434
+ * day number REMOVED (ADR 0057 R6).
435
+ *
436
+ * The visible text of the cell is already the day number, and `[skra-sr-only]` keeps its content in
437
+ * `textContent` (that is the whole point of the clip technique), so an extension formatted with
438
+ * `dateStyle: 'long'` restates it: the name computes to `1414 czerwca 2026` in `pl-PL` and
439
+ * `14June 14, 2026` in `en-US`. Path 2 extends the visible text; it never repeats it.
440
+ *
441
+ * The day part is dropped via `formatToParts` so the remaining parts keep the LOCALE's own order, together
442
+ * with the literal that separates it from its neighbour (the following one, or the preceding one when the
443
+ * day is last) so no stray `, ` or `月` is left behind. Accepted trade-off (ADR 0057 R6): in year-first
444
+ * locales the assembled name reads `14 2026年6月` rather than `2026年6月14日` — the visible text is
445
+ * positionally first in the DOM and WCAG 2.5.3 requires it to be contained in the name, so perfect
446
+ * grammatical order is not reachable without reordering the DOM. Containment and non-duplication are the
447
+ * properties that matter.
448
+ */
449
+ function formatDayNameExtension(iso, locale) {
252
450
  const dt = toUtcDate(iso);
253
451
  if (!dt)
254
452
  return '';
255
- return new Intl.DateTimeFormat(locale, {
453
+ const parts = new Intl.DateTimeFormat(locale, {
256
454
  dateStyle: 'long',
257
455
  timeZone: UTC,
456
+ calendar: GREGORY,
457
+ }).formatToParts(dt);
458
+ const dayIndex = parts.findIndex((part) => part.type === 'day');
459
+ if (dayIndex === -1)
460
+ return parts
461
+ .map((part) => part.value)
462
+ .join('')
463
+ .trim();
464
+ const separatorIndex = parts[dayIndex + 1]?.type === 'literal' ? dayIndex + 1 : dayIndex - 1;
465
+ return parts
466
+ .filter((_, index) => index !== dayIndex && index !== separatorIndex)
467
+ .map((part) => part.value)
468
+ .join('')
469
+ .trim();
470
+ }
471
+ /** Visible text of a month cell: the localized full month name (e.g. "czerwiec"), no year. */
472
+ function formatMonthCell(y, m, locale) {
473
+ const dt = toUtcDate(`${pad4(y)}-${pad2(m)}-01`);
474
+ if (!dt)
475
+ return '';
476
+ return new Intl.DateTimeFormat(locale, {
477
+ month: 'long',
478
+ timeZone: UTC,
479
+ calendar: GREGORY,
480
+ }).format(dt);
481
+ }
482
+ /**
483
+ * The month cell's `[skra-sr-only]` extension — the localized year (ADR 0057 R6). The visible text is
484
+ * already the month name, so the extension carries the year and nothing else.
485
+ */
486
+ function formatYear(y, locale) {
487
+ const dt = toUtcDate(`${pad4(y)}-01-01`);
488
+ if (!dt)
489
+ return '';
490
+ return new Intl.DateTimeFormat(locale, {
491
+ year: 'numeric',
492
+ timeZone: UTC,
493
+ calendar: GREGORY,
258
494
  }).format(dt);
259
495
  }
260
496
  /** Visible day number, localized digits (e.g. Arabic-Indic `٥`) — never a raw `toString()`. */
@@ -269,10 +505,12 @@ function weekdayHeaders(firstDayOfWeek, locale) {
269
505
  const shortFmt = new Intl.DateTimeFormat(locale, {
270
506
  weekday: 'short',
271
507
  timeZone: UTC,
508
+ calendar: GREGORY,
272
509
  });
273
510
  const longFmt = new Intl.DateTimeFormat(locale, {
274
511
  weekday: 'long',
275
512
  timeZone: UTC,
513
+ calendar: GREGORY,
276
514
  });
277
515
  const headers = [];
278
516
  for (let i = 0; i < 7; i++) {
@@ -310,26 +548,44 @@ function resolveFirstDayOfWeek(override, locale) {
310
548
 
311
549
  let nextDatePickerId = 0;
312
550
  /**
313
- * SkraDatePicker — pick a single **calendar date** from an APG Date Picker grid in the native top layer.
314
- * Second Tier-3 component. A value-display **trigger** (`<button>`) opens a non-modal top-layer **calendar
315
- * panel** (Popover API, `popover="auto"`, ADR 0044). The value is an **ISO 8601 date-only string**
316
- * (`YYYY-MM-DD`), consumer-owned (ADR 0050/037 — NO `@angular/forms`); all date math is the zoneless
317
- * UTC-calculator discipline in `calendar-math` (no date library). Localization (month/weekday names,
318
- * first-day-of-week, digits) is `Intl`-derived in `calendar-intl`; the 2-D grid keyboard is the
319
- * hand-rolled `SkraCalendarKeyboard` (ADR 0044 §7 — roving REAL focus, not aria-activedescendant).
551
+ * SkraDatePicker — pick a **calendar date** or a **calendar month** from an APG Date Picker grid in the
552
+ * native top layer. Second Tier-3 component. A value-display **trigger** (`<button>`) opens a non-modal
553
+ * top-layer **calendar panel** (Popover API, `popover="auto"`, ADR 0044). The value is an **ISO 8601 string**
554
+ * truncated to the active `precision` (`YYYY-MM-DD` / `YYYY-MM`), consumer-owned (ADR 0050/0057/037 — NO
555
+ * `@angular/forms`); all date math is the zoneless UTC-calculator discipline in `calendar-math` (no date
556
+ * library). Localization (month/weekday names, first-day-of-week, digits) is `Intl`-derived in
557
+ * `calendar-intl`; the 2-D grid keyboard is the hand-rolled `SkraCalendarKeyboard` (ADR 0044 §7 — roving
558
+ * REAL focus, not aria-activedescendant).
559
+ *
560
+ * **Precision is a runtime axis, not a second component (ADR 0057).** `precision="month"` swaps the 7×N day
561
+ * grid for a 3×4 quarter-aligned month grid inside the SAME trigger, panel, positioner, field DI wiring,
562
+ * dismissal model, selection model and keyboard controller. The value channel does not widen — `YYYY-MM` is
563
+ * a truncation of `YYYY-MM-DD`, both `string` — which is exactly why this is an input and cardinality
564
+ * (ADR 0046) is not. `week` / `time` / `datetime-local` are excluded by rule (ADR 0057 R5): they need a
565
+ * second calendar coordinate system or are not grids at all, and stay the native `skraInput` path.
320
566
  *
321
- * No projected content: the calendar days are GENERATED (the structural divergence from select/combobox —
322
- * there is no `skra-option` reuse). The only focusable element in the open panel is the roving day cell;
323
- * the month-nav buttons are pointer affordances (`tabindex="-1"`, driven by PageUp/PageDown on keyboard),
324
- * and Tab dismisses — so no focus-containment machinery is needed (spec Q-1).
567
+ * No projected content: the cells are GENERATED (the structural divergence from select/combobox — there is
568
+ * no `skra-option` reuse). The only focusable element in the open panel is the roving cell; the period-nav
569
+ * buttons are pointer affordances (`tabindex="-1"`, driven by PageUp/PageDown on keyboard), and Tab
570
+ * dismisses — so no focus-containment machinery is needed (spec Q-1).
325
571
  *
326
572
  * Full contract: adr/specs/component-datepicker-spec.md.
327
573
  */
328
574
  class SkraDatePicker {
329
- /** Consumer-owned value: ISO 8601 date-only (`YYYY-MM-DD`); `''` = unset. NEVER a Date (ADR 0050/037). */
575
+ /**
576
+ * Consumer-owned value: an ISO 8601 key truncated to `precision` — `YYYY-MM-DD` (`day`) or `YYYY-MM`
577
+ * (`month`); `''` = unset. NEVER a Date (ADR 0050/0057/037).
578
+ */
330
579
  value = input('', /* @ts-ignore */
331
580
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
332
- /** Inclusive lower bound (ISO); `''` = unbounded (ADR 0050 §2). */
581
+ /**
582
+ * Value granularity (ADR 0057 R1). `'day'` (default) emits `YYYY-MM-DD` over a 7×N day grid; `'month'`
583
+ * emits `YYYY-MM` over a 3×4 quarter-aligned month grid. NOT a look input, NOT a cardinality flag, and
584
+ * NOT reflected to a `data-skra-*` attribute — precision is configuration, not state.
585
+ */
586
+ precision = input('day', /* @ts-ignore */
587
+ ...(ngDevMode ? [{ debugName: "precision" }] : /* istanbul ignore next */ []));
588
+ /** Inclusive lower bound (ISO); `''` = unbounded (ADR 0050 §2, truncated to `precision` per ADR 0057 R4). */
333
589
  min = input('', /* @ts-ignore */
334
590
  ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
335
591
  /** Inclusive upper bound (ISO); `''` = unbounded. */
@@ -350,7 +606,7 @@ class SkraDatePicker {
350
606
  /** Standalone availability source — `SkraFieldContext` wins when present. */
351
607
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
352
608
  readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
353
- /** Fires ONLY on user date selection — never on a programmatic `value()` change (ADR 037/0050). */
609
+ /** Fires ONLY on user selection — never on a programmatic `value()`/`precision()` change (ADR 037/0057 R3). */
354
610
  valueChange = output();
355
611
  triggerRef = viewChild('trigger', /* @ts-ignore */
356
612
  ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
@@ -360,14 +616,35 @@ class SkraDatePicker {
360
616
  injector = inject(Injector);
361
617
  keyboard = new SkraCalendarKeyboard();
362
618
  panelId = `skra-datepicker-${nextDatePickerId}`;
363
- monthYearId = `skra-datepicker-monthyear-${nextDatePickerId++}`;
619
+ periodId = `skra-datepicker-period-${nextDatePickerId++}`;
364
620
  isOpen = signal(false, /* @ts-ignore */
365
621
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
622
+ /**
623
+ * Which grid is on screen. Seeded from `precision` and writable in between (ADR 0057 R9) so the deferred
624
+ * month/year quick-pick — stepping UP from the day grid to pick a month, then back down — is an additive
625
+ * change rather than a rewrite. In MVP nothing writes it, so `view() === precision()` always.
626
+ *
627
+ * NOTE for whoever lands quick-pick: it adds a second focusable element to the open panel and therefore
628
+ * RE-OPENS spec Q-1, which was resolved on the premise that the panel holds exactly one focusable stop
629
+ * and Tab dismisses. Re-resolve Q-1 first.
630
+ */
631
+ view = linkedSignal(() => this.precision(), /* @ts-ignore */
632
+ ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
366
633
  /** Today's LOCAL date (ADR 0050 §3) — stable for the session. */
367
634
  todayIso = today();
368
- /** The roving-focus day; SINGLE source of truth for what the grid shows AND which cell is focusable. */
369
- focusedIso = signal(this.todayIso, /* @ts-ignore */
370
- ...(ngDevMode ? [{ debugName: "focusedIso" }] : /* istanbul ignore next */ []));
635
+ /** The roving-focus cell; SINGLE source of truth for what the grid shows AND which cell is focusable. */
636
+ focusedKey = signal(this.todayIso, /* @ts-ignore */
637
+ ...(ngDevMode ? [{ debugName: "focusedKey" }] : /* istanbul ignore next */ []));
638
+ /**
639
+ * The focused key re-keyed to the active view. A `precision` flip while the panel is closed leaves a key
640
+ * at the previous granularity behind; re-keying defensively keeps every downstream `computed` total
641
+ * without an effect or a reset cascade. `onOpen` re-seeds properly from the value.
642
+ */
643
+ focusedAt = computed(() => {
644
+ const view = this.view();
645
+ return reKey(this.focusedKey(), view) || todayAt(view);
646
+ }, /* @ts-ignore */
647
+ ...(ngDevMode ? [{ debugName: "focusedAt" }] : /* istanbul ignore next */ []));
371
648
  positioner;
372
649
  /** Set when a close should return focus to the trigger (keyboard/select), not on outside-click. */
373
650
  returnFocus = false;
@@ -385,76 +662,122 @@ class SkraDatePicker {
385
662
  describedBy = computed(() => this.context?.describedByIds() ?? null, /* @ts-ignore */
386
663
  ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
387
664
  // ── Derived calendar state ──
388
- /** The committed value, defensively normalized: invalid/out-of-range → `''` for display (ADR 0050 §1). */
389
- selectedIso = computed(() => {
390
- const v = this.value();
391
- return isValidIso(v) && isInRange(v, this.min(), this.max()) ? v : '';
665
+ /**
666
+ * The committed value, defensively normalized: wrong-format-for-precision, invalid or out-of-range → `''`
667
+ * for display (ADR 0050 §1 as amended by ADR 0057 R3). Keyed on `precision` (what the value IS), not on
668
+ * `view` (what is on screen) — the two only diverge once quick-pick lands.
669
+ */
670
+ selectedKey = computed(() => {
671
+ const value = this.value();
672
+ const precision = this.precision();
673
+ return isValidAt(value, precision) &&
674
+ isInRangeAt(value, this.min(), this.max(), precision)
675
+ ? value
676
+ : '';
392
677
  }, /* @ts-ignore */
393
- ...(ngDevMode ? [{ debugName: "selectedIso" }] : /* istanbul ignore next */ []));
394
- formattedValue = computed(() => formatValue(this.selectedIso(), this.locale()), /* @ts-ignore */
678
+ ...(ngDevMode ? [{ debugName: "selectedKey" }] : /* istanbul ignore next */ []));
679
+ formattedValue = computed(() => this.precision() === 'month'
680
+ ? formatMonthValue(this.selectedKey(), this.locale())
681
+ : formatValue(this.selectedKey(), this.locale()), /* @ts-ignore */
395
682
  ...(ngDevMode ? [{ debugName: "formattedValue" }] : /* istanbul ignore next */ []));
396
683
  firstDow = computed(() => resolveFirstDayOfWeek(this.firstDayOfWeek(), this.locale()), /* @ts-ignore */
397
684
  ...(ngDevMode ? [{ debugName: "firstDow" }] : /* istanbul ignore next */ []));
398
- /** The month being shown — always the focused day's month (keeps the roving tabindex valid). */
399
- displayedMonth = computed(() => {
400
- const p = parseIso(this.focusedIso()) ?? parseIso(this.todayIso);
401
- return { y: p.y, m: p.m };
685
+ /**
686
+ * The container the grid shows — the focused cell's MONTH in day view, its YEAR in month view (`m` is
687
+ * then meaningless). Always derived from the focused cell, which keeps the roving tabindex valid.
688
+ */
689
+ displayed = computed(() => {
690
+ const key = this.focusedAt();
691
+ if (this.view() === 'month') {
692
+ const month = parseIsoMonth(key);
693
+ return { y: month?.y ?? parseIso(this.todayIso).y, m: 1 };
694
+ }
695
+ const date = parseIso(key) ?? parseIso(this.todayIso);
696
+ return { y: date.y, m: date.m };
402
697
  }, /* @ts-ignore */
403
- ...(ngDevMode ? [{ debugName: "displayedMonth" }] : /* istanbul ignore next */ []));
404
- monthYearLabel = computed(() => formatMonthYear(this.displayedMonth().y, this.displayedMonth().m, this.locale()), /* @ts-ignore */
405
- ...(ngDevMode ? [{ debugName: "monthYearLabel" }] : /* istanbul ignore next */ []));
406
- prevMonthLabel = computed(() => this.monthOffsetLabel(-1), /* @ts-ignore */
407
- ...(ngDevMode ? [{ debugName: "prevMonthLabel" }] : /* istanbul ignore next */ []));
408
- nextMonthLabel = computed(() => this.monthOffsetLabel(1), /* @ts-ignore */
409
- ...(ngDevMode ? [{ debugName: "nextMonthLabel" }] : /* istanbul ignore next */ []));
698
+ ...(ngDevMode ? [{ debugName: "displayed" }] : /* istanbul ignore next */ []));
699
+ periodLabel = computed(() => {
700
+ const { y, m } = this.displayed();
701
+ return this.view() === 'month'
702
+ ? formatYear(y, this.locale())
703
+ : formatMonthYear(y, m, this.locale());
704
+ }, /* @ts-ignore */
705
+ ...(ngDevMode ? [{ debugName: "periodLabel" }] : /* istanbul ignore next */ []));
706
+ prevPeriodLabel = computed(() => this.periodOffsetLabel(-1), /* @ts-ignore */
707
+ ...(ngDevMode ? [{ debugName: "prevPeriodLabel" }] : /* istanbul ignore next */ []));
708
+ nextPeriodLabel = computed(() => this.periodOffsetLabel(1), /* @ts-ignore */
709
+ ...(ngDevMode ? [{ debugName: "nextPeriodLabel" }] : /* istanbul ignore next */ []));
710
+ /**
711
+ * `aria-current` token for the cell holding today. ARIA 1.2 has no `month` value, and `date` is
712
+ * defined as the current DATE within a collection of dates — accurate for a day cell, a stretch for a
713
+ * month cell, where the generic `true` ("current item within a set") is the honest mapping.
714
+ */
715
+ currentToken = computed(() => this.view() === 'month' ? 'true' : 'date', /* @ts-ignore */
716
+ ...(ngDevMode ? [{ debugName: "currentToken" }] : /* istanbul ignore next */ []));
410
717
  weekdayHeadersView = computed(() => weekdayHeaders(this.firstDow(), this.locale()), /* @ts-ignore */
411
718
  ...(ngDevMode ? [{ debugName: "weekdayHeadersView" }] : /* istanbul ignore next */ []));
412
- weeks = computed(() => {
413
- const { y, m } = this.displayedMonth();
414
- const loc = this.locale();
415
- const sel = this.selectedIso();
416
- const foc = this.focusedIso();
417
- const min = this.min();
418
- const max = this.max();
419
- return monthGrid(y, m, this.firstDow()).map((week, wi) => ({
420
- key: wi,
421
- days: week.map((iso, di) => iso === null
422
- ? this.emptyCell(`${wi}-${di}`)
423
- : {
424
- key: iso,
425
- iso,
426
- dayNum: formatDayNumber(parseIso(iso).d, loc),
427
- srName: formatDayName(iso, loc),
428
- today: iso === this.todayIso,
429
- selected: iso === sel,
430
- disabled: !isInRange(iso, min, max),
431
- focused: iso === foc,
432
- }),
433
- }));
434
- }, /* @ts-ignore */
435
- ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
719
+ /** ONE grid renderer, parameterized by view — the ADR 0057 R8 claim, made structural. */
720
+ rows = computed(() => this.view() === 'month' ? this.buildMonthRows() : this.buildDayRows(), /* @ts-ignore */
721
+ ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
436
722
  constructor() {
437
723
  inject(DestroyRef).onDestroy(() => this.positioner?.destroy());
724
+ // A `precision` flip while the panel is OPEN re-stamps the whole grid: cells are tracked by their
725
+ // ISO key, and a day key never matches a month key, so `@for` destroys and rebuilds every cell
726
+ // button and DOM focus falls back to <body>. The roving tabindex survives, but nothing is focused,
727
+ // so the arrow keys would be dead until the user clicked. Re-seed and re-focus. (Found in review.)
728
+ effect(() => {
729
+ this.view(); // the only tracked read — this effect exists to react to a view swap
730
+ if (!untracked(() => this.isOpen()))
731
+ return;
732
+ untracked(() => {
733
+ this.focusedKey.set(this.seedFocus());
734
+ this.scheduleFocus();
735
+ });
736
+ });
438
737
  if (typeof ngDevMode !== 'undefined' && ngDevMode) {
439
- // Dev-only sanity warnings (stripped in prod) — never thrown (ADR 0050 §1).
440
- const warn = computed(() => {
441
- const v = this.value();
442
- if (v !== '' && !isValidIso(v))
443
- return `skra-datepicker: value "${v}" is not a valid YYYY-MM-DD date; shown as unset.`;
444
- if (this.min() !== '' &&
445
- this.max() !== '' &&
446
- compareIso(this.min(), this.max()) > 0)
447
- return `skra-datepicker: min "${this.min()}" is after max "${this.max()}" — every day is disabled.`;
448
- return null;
449
- }, /* @ts-ignore */
450
- ...(ngDevMode ? [{ debugName: "warn" }] : /* istanbul ignore next */ []));
451
- afterNextRender(() => {
452
- const message = warn();
738
+ // Dev-only sanity warnings (stripped in prod) — never thrown (ADR 0050 §1). REACTIVE on purpose:
739
+ // ADR 0057 R3 contemplates a runtime `precision` flip, and a one-shot first-render check would go
740
+ // silent for exactly the mismatch that flip creates. (The `afterNextRender` version this replaces
741
+ // read a `computed` once and was reactive in appearance only — found in review.)
742
+ effect(() => {
743
+ const message = this.devWarning();
453
744
  if (message)
454
745
  console.warn(message);
455
- }, { injector: this.injector });
746
+ });
456
747
  }
457
748
  }
749
+ /**
750
+ * The dev-mode diagnostic, or `null` when the inputs are coherent. Split out of the constructor so it
751
+ * is a plain reactive expression rather than a closure buried in a lifecycle hook.
752
+ */
753
+ devWarning = computed(() => {
754
+ const value = this.value();
755
+ const precision = this.precision();
756
+ const rawMin = this.min();
757
+ const rawMax = this.max();
758
+ if (value !== '' && !isValidAt(value, precision))
759
+ return `skra-datepicker: value "${value}" is not a valid ${precision === 'month' ? 'YYYY-MM month' : 'YYYY-MM-DD date'} key for precision="${precision}"; shown as unset.`;
760
+ const min = boundAt(rawMin, precision);
761
+ const max = boundAt(rawMax, precision);
762
+ if (min !== '' && max !== '' && compareIso(min, max) > 0)
763
+ return `skra-datepicker: min "${rawMin}" is after max "${rawMax}" — every cell is disabled.`;
764
+ // A bound that cannot be expressed at the active precision is NOT an error, but it is never what
765
+ // the author meant, and silence is the failure mode: in month precision a day bound is truncated
766
+ // (ADR 0057 R4), and in day precision a month bound is dropped to unbounded rather than guessed.
767
+ // Report BOTH bounds — reporting only `min` hid the `max` case.
768
+ const ignored = [
769
+ ['min', rawMin],
770
+ ['max', rawMax],
771
+ ].filter(([, raw]) => raw !== '' && boundAt(raw, precision) !== raw);
772
+ if (ignored.length > 0)
773
+ return ignored
774
+ .map(([name, raw]) => precision === 'month'
775
+ ? `skra-datepicker: ${name}="${raw}" carries a day component that precision="month" ignores; compared as "${raw.slice(0, 7)}".`
776
+ : `skra-datepicker: ${name}="${raw}" is not a valid YYYY-MM-DD key for precision="day"; treated as unbounded. A month bound is NOT widened — "min" would anchor to the 1st but "max" would have to anchor to the last day, and inventing that asymmetry silently is worse.`)
777
+ .join(' ');
778
+ return null;
779
+ }, /* @ts-ignore */
780
+ ...(ngDevMode ? [{ debugName: "devWarning" }] : /* istanbul ignore next */ []));
458
781
  // ── Trigger interaction ──
459
782
  onTriggerClick(event) {
460
783
  if (this.isReadonly())
@@ -478,18 +801,19 @@ class SkraDatePicker {
478
801
  return;
479
802
  }
480
803
  const intent = this.keyboard.resolve(event, {
481
- focusedIso: this.focusedIso(),
804
+ focusedIso: this.focusedAt(),
482
805
  firstDayOfWeek: this.firstDow(),
806
+ view: this.view(),
483
807
  });
484
808
  switch (intent.type) {
485
809
  case 'focus':
486
810
  event.preventDefault();
487
- this.focusedIso.set(intent.iso);
811
+ this.focusedKey.set(intent.iso);
488
812
  this.scheduleFocus(); // re-focus after the grid re-stamps
489
813
  break;
490
814
  case 'select':
491
815
  event.preventDefault();
492
- this.selectDate(this.focusedIso());
816
+ this.selectCell(this.focusedAt());
493
817
  break;
494
818
  case 'close':
495
819
  this.returnFocus = true;
@@ -499,19 +823,39 @@ class SkraDatePicker {
499
823
  break;
500
824
  }
501
825
  }
502
- // ── Month navigation (pointer; focus stays on the nav button) ──
503
- goToPreviousMonth() {
504
- this.focusedIso.set(addMonthsClamped(this.focusedIso(), -1));
826
+ // ── Period navigation (pointer; focus stays on the nav button) ──
827
+ goToPreviousPeriod() {
828
+ this.stepPeriod(-1);
829
+ }
830
+ goToNextPeriod() {
831
+ this.stepPeriod(1);
505
832
  }
506
- goToNextMonth() {
507
- this.focusedIso.set(addMonthsClamped(this.focusedIso(), 1));
833
+ /** One period = one month (day view) / one year (month view). */
834
+ stepPeriod(offset) {
835
+ const key = this.focusedAt();
836
+ this.focusedKey.set(this.view() === 'month'
837
+ ? addYearsToKey(key, offset)
838
+ : addMonthsClamped(key, offset));
508
839
  }
509
840
  // ── Selection ──
510
- selectDate(iso) {
511
- if (!isInRange(iso, this.min(), this.max()))
512
- return; // guard out-of-range (one place: keyboard + pointer)
841
+ selectCell(iso) {
842
+ const precision = this.precision();
843
+ if (this.view() !== precision) {
844
+ // Reserved for the ADR 0057 R9 quick-pick. The moment a NAVIGATION view can differ from the
845
+ // terminal precision, a cell click in it is a step DOWN, not a commit — emitting here would
846
+ // silently write `2026-06-01` for a click on "June" under `precision="day"`. No such view exists
847
+ // yet, so this is a hard stop, not a behaviour: the step-down path must be filled in before a
848
+ // navigation view can ship. Cheap to add now, invisible to fix later. (Raised in review.)
849
+ if (typeof ngDevMode !== 'undefined' && ngDevMode)
850
+ console.warn(`skra-datepicker: a cell was activated in the "${this.view()}" view while precision is ` +
851
+ `"${precision}". Nothing was emitted — a navigation view needs the ADR 0057 R9 step-down path.`);
852
+ return;
853
+ }
854
+ // Guard out-of-range in ONE place (keyboard + pointer), at the precision the value is emitted in.
855
+ if (!isInRangeAt(iso, this.min(), this.max(), precision))
856
+ return;
513
857
  this.returnFocus = true;
514
- this.valueChange.emit(iso);
858
+ this.valueChange.emit(reKey(iso, precision));
515
859
  this.close();
516
860
  }
517
861
  // ── Panel open / close (native toggle from the Popover API) ──
@@ -524,7 +868,7 @@ class SkraDatePicker {
524
868
  this.onClose();
525
869
  }
526
870
  onOpen() {
527
- this.focusedIso.set(this.seedFocus());
871
+ this.focusedKey.set(this.seedFocus());
528
872
  const anchor = this.triggerRef()?.nativeElement;
529
873
  const panel = this.panelRef()?.nativeElement;
530
874
  if (anchor && panel) {
@@ -551,23 +895,82 @@ class SkraDatePicker {
551
895
  close() {
552
896
  this.panelRef()?.nativeElement.hidePopover?.();
553
897
  }
554
- /** Seed the focused day on open: the valid current value, else today, clamped into `[min,max]`. */
898
+ /** Seed the focused cell on open: the valid current value, else today, clamped into `[min,max]`. */
555
899
  seedFocus() {
556
- const v = this.value();
557
- const base = isValidIso(v) ? v : this.todayIso;
558
- return clampIso(base, this.min(), this.max());
900
+ const value = this.value();
901
+ const precision = this.precision();
902
+ const base = isValidAt(value, precision) ? value : todayAt(precision);
903
+ return clampAt(base, this.min(), this.max(), precision);
559
904
  }
560
- monthOffsetLabel(n) {
561
- const { y, m } = this.displayedMonth();
562
- const ref = parseIso(addMonthsClamped(toIso({ y, m, d: 1 }), n));
563
- return formatMonthYear(ref.y, ref.m, this.locale());
905
+ // ── Grid builders ──
906
+ buildDayRows() {
907
+ const { y, m } = this.displayed();
908
+ const locale = this.locale();
909
+ const selected = reKey(this.selectedKey(), 'day');
910
+ const focused = this.focusedAt();
911
+ const min = this.min();
912
+ const max = this.max();
913
+ return monthGrid(y, m, this.firstDow()).map((week, rowIndex) => ({
914
+ key: rowIndex,
915
+ cells: week.map((iso, columnIndex) => iso === null
916
+ ? this.emptyCell(`${rowIndex}-${columnIndex}`)
917
+ : {
918
+ key: iso,
919
+ iso,
920
+ label: formatDayNumber(parseIso(iso).d, locale),
921
+ srExt: this.srExtension(formatDayNameExtension(iso, locale)),
922
+ today: iso === this.todayIso,
923
+ selected: iso === selected,
924
+ disabled: !isInRangeAt(iso, min, max, 'day'),
925
+ focused: iso === focused,
926
+ }),
927
+ }));
928
+ }
929
+ buildMonthRows() {
930
+ const { y } = this.displayed();
931
+ const locale = this.locale();
932
+ const selected = reKey(this.selectedKey(), 'month');
933
+ const focused = this.focusedAt();
934
+ const todayKey = todayAt('month');
935
+ const min = this.min();
936
+ const max = this.max();
937
+ const yearLabel = this.srExtension(formatYear(y, locale));
938
+ return yearGrid(y).map((quarter, rowIndex) => ({
939
+ key: rowIndex,
940
+ cells: quarter.map((key) => ({
941
+ key,
942
+ iso: key,
943
+ label: formatMonthCell(y, parseIsoMonth(key).m, locale),
944
+ srExt: yearLabel,
945
+ today: key === todayKey,
946
+ selected: key === selected,
947
+ disabled: !isInRangeAt(key, min, max, 'month'),
948
+ focused: key === focused,
949
+ })),
950
+ }));
951
+ }
952
+ /**
953
+ * The accessible-name separator is baked into the extension rather than left to the template or to the
954
+ * engine: `[skra-sr-only]` is absolutely positioned, so whether the name computation inserts a space
955
+ * between the visible text and the extension is engine-dependent. One explicit space, asserted by test.
956
+ */
957
+ srExtension(text) {
958
+ return text === '' ? '' : ` ${text}`;
959
+ }
960
+ periodOffsetLabel(offset) {
961
+ const { y, m } = this.displayed();
962
+ const locale = this.locale();
963
+ if (this.view() === 'month')
964
+ return formatYear(y + offset, locale);
965
+ const ref = parseIso(addMonthsClamped(toIso({ y, m, d: 1 }), offset));
966
+ return ref ? formatMonthYear(ref.y, ref.m, locale) : '';
564
967
  }
565
968
  emptyCell(key) {
566
969
  return {
567
970
  key,
568
971
  iso: null,
569
- dayNum: '',
570
- srName: '',
972
+ label: '',
973
+ srExt: '',
571
974
  today: false,
572
975
  selected: false,
573
976
  disabled: true,
@@ -575,11 +978,11 @@ class SkraDatePicker {
575
978
  };
576
979
  }
577
980
  scheduleFocus() {
578
- afterNextRender(() => this.focusActiveDay(), { injector: this.injector });
981
+ afterNextRender(() => this.focusActiveCell(), { injector: this.injector });
579
982
  }
580
- focusActiveDay() {
983
+ focusActiveCell() {
581
984
  this.panelRef()
582
- ?.nativeElement.querySelector('.skra-datepicker-day[tabindex="0"]')
985
+ ?.nativeElement.querySelector('.skra-datepicker-cell[tabindex="0"]')
583
986
  ?.focus();
584
987
  }
585
988
  /** Read a length comp token (px/rem) off the panel computed style for the positioner. */
@@ -597,7 +1000,7 @@ class SkraDatePicker {
597
1000
  return parseFloat(raw) || 0;
598
1001
  }
599
1002
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraDatePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
600
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraDatePicker, isStandalone: true, selector: "skra-datepicker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "attr.data-skra-disabled": "isDisabled() ? \"\" : null", "attr.data-skra-readonly": "isReadonly() ? \"\" : null", "attr.data-skra-validation": "hasError() ? \"invalid\" : null" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], exportAs: ["skraDatePicker"], ngImport: i0, template: `
1003
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.1", type: SkraDatePicker, isStandalone: true, selector: "skra-datepicker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, precision: { classPropertyName: "precision", publicName: "precision", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "attr.data-skra-disabled": "isDisabled() ? \"\" : null", "attr.data-skra-readonly": "isReadonly() ? \"\" : null", "attr.data-skra-validation": "hasError() ? \"invalid\" : null" } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], exportAs: ["skraDatePicker"], ngImport: i0, template: `
601
1004
  <button
602
1005
  #trigger
603
1006
  class="skra-datepicker-trigger"
@@ -641,7 +1044,7 @@ class SkraDatePicker {
641
1044
  popover="auto"
642
1045
  role="dialog"
643
1046
  aria-modal="false"
644
- [attr.aria-labelledby]="monthYearId"
1047
+ [attr.aria-labelledby]="periodId"
645
1048
  (toggle)="onPanelToggle($event)"
646
1049
  >
647
1050
  <div class="skra-datepicker-header">
@@ -649,8 +1052,8 @@ class SkraDatePicker {
649
1052
  type="button"
650
1053
  class="skra-datepicker-nav"
651
1054
  tabindex="-1"
652
- [attr.aria-label]="prevMonthLabel()"
653
- (click)="goToPreviousMonth()"
1055
+ [attr.aria-label]="prevPeriodLabel()"
1056
+ (click)="goToPreviousPeriod()"
654
1057
  >
655
1058
  <svg
656
1059
  viewBox="0 0 24 24"
@@ -663,17 +1066,17 @@ class SkraDatePicker {
663
1066
  </svg>
664
1067
  </button>
665
1068
  <span
666
- class="skra-datepicker-monthyear"
667
- [id]="monthYearId"
1069
+ class="skra-datepicker-period"
1070
+ [id]="periodId"
668
1071
  aria-live="polite"
669
- >{{ monthYearLabel() }}</span
1072
+ >{{ periodLabel() }}</span
670
1073
  >
671
1074
  <button
672
1075
  type="button"
673
1076
  class="skra-datepicker-nav"
674
1077
  tabindex="-1"
675
- [attr.aria-label]="nextMonthLabel()"
676
- (click)="goToNextMonth()"
1078
+ [attr.aria-label]="nextPeriodLabel()"
1079
+ (click)="goToNextPeriod()"
677
1080
  >
678
1081
  <svg
679
1082
  viewBox="0 0 24 24"
@@ -690,21 +1093,24 @@ class SkraDatePicker {
690
1093
  <table
691
1094
  class="skra-datepicker-grid"
692
1095
  role="grid"
693
- [attr.aria-labelledby]="monthYearId"
1096
+ [class.skra-datepicker-grid--month]="view() === 'month'"
1097
+ [attr.aria-labelledby]="periodId"
694
1098
  >
695
- <thead>
696
- <tr>
697
- @for (wd of weekdayHeadersView(); track wd.key) {
698
- <th scope="col" class="skra-datepicker-weekday">
699
- <abbr [title]="wd.long">{{ wd.short }}</abbr>
700
- </th>
701
- }
702
- </tr>
703
- </thead>
1099
+ @if (view() !== 'month') {
1100
+ <thead>
1101
+ <tr>
1102
+ @for (wd of weekdayHeadersView(); track wd.key) {
1103
+ <th scope="col" class="skra-datepicker-weekday">
1104
+ <abbr [title]="wd.long">{{ wd.short }}</abbr>
1105
+ </th>
1106
+ }
1107
+ </tr>
1108
+ </thead>
1109
+ }
704
1110
  <tbody>
705
- @for (week of weeks(); track week.key) {
1111
+ @for (row of rows(); track row.key) {
706
1112
  <tr>
707
- @for (cell of week.days; track cell.key) {
1113
+ @for (cell of row.cells; track cell.key) {
708
1114
  @if (cell.iso) {
709
1115
  <td
710
1116
  role="gridcell"
@@ -712,23 +1118,26 @@ class SkraDatePicker {
712
1118
  >
713
1119
  <button
714
1120
  type="button"
715
- class="skra-datepicker-day"
1121
+ class="skra-datepicker-cell"
716
1122
  [attr.tabindex]="cell.focused ? 0 : -1"
717
- [attr.aria-current]="cell.today ? 'date' : null"
1123
+ [attr.aria-current]="cell.today ? currentToken() : null"
718
1124
  [attr.aria-disabled]="cell.disabled ? 'true' : null"
719
1125
  [attr.data-skra-selected]="cell.selected ? '' : null"
720
1126
  [attr.data-skra-today]="cell.today ? '' : null"
721
- (click)="selectDate(cell.iso)"
1127
+ (click)="selectCell(cell.iso)"
722
1128
  (keydown)="onGridKeydown($event)"
723
1129
  >
724
- {{ cell.dayNum
725
- }}<span class="skra-datepicker-day-sr" skra-sr-only>{{
726
- cell.srName
1130
+ {{ cell.label
1131
+ }}<span class="skra-datepicker-cell-sr" skra-sr-only>{{
1132
+ cell.srExt
727
1133
  }}</span>
728
1134
  </button>
729
1135
  </td>
730
1136
  } @else {
731
- <td class="skra-datepicker-day-empty" aria-hidden="true"></td>
1137
+ <td
1138
+ class="skra-datepicker-cell-empty"
1139
+ aria-hidden="true"
1140
+ ></td>
732
1141
  }
733
1142
  }
734
1143
  </tr>
@@ -736,7 +1145,7 @@ class SkraDatePicker {
736
1145
  </tbody>
737
1146
  </table>
738
1147
  </div>
739
- `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:inline-block;box-sizing:border-box;margin:0}.skra-datepicker-trigger{display:flex;align-items:center;gap:var(--skra-comp-datepicker-gap);inline-size:100%;min-block-size:var(--skra-comp-datepicker-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-datepicker-padding-block);padding-inline:var(--skra-comp-datepicker-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-datepicker-background);color:var(--skra-comp-datepicker-foreground);border-color:var(--skra-comp-datepicker-border-color);border-width:var(--skra-comp-datepicker-border-width);border-style:var(--skra-comp-datepicker-border-style);border-radius:var(--skra-comp-datepicker-radius);box-shadow:var(--skra-comp-datepicker-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),border-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}.skra-datepicker-trigger:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-trigger:disabled{background:var(--skra-comp-datepicker-background-disabled);color:var(--skra-comp-datepicker-foreground-disabled);cursor:not-allowed}.skra-datepicker-trigger[aria-readonly=true]{background:var(--skra-comp-datepicker-background-readonly);cursor:default}.skra-datepicker-trigger[aria-invalid=true]{border-color:var(--skra-comp-datepicker-border-color-validation-invalid)}.skra-datepicker-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skra-datepicker-placeholder{flex:1 1 auto;color:var(--skra-comp-datepicker-foreground-disabled)}.skra-datepicker-icon{flex:none;inline-size:var(--skra-comp-datepicker-icon-size);block-size:var(--skra-comp-datepicker-icon-size);color:var(--skra-comp-datepicker-icon-color)}.skra-datepicker-panel{position:fixed;margin:0;inset:auto;box-sizing:border-box;inline-size:max-content;padding:var(--skra-comp-datepicker-panel-padding);background:var(--skra-comp-datepicker-panel-background);color:var(--skra-comp-datepicker-panel-foreground);border:var(--skra-comp-datepicker-border-width) solid var(--skra-comp-datepicker-panel-border-color);border-radius:var(--skra-comp-datepicker-panel-radius);box-shadow:var(--skra-comp-datepicker-panel-shadow);opacity:0;transition:opacity var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),display var(--skra-comp-datepicker-motion-duration) allow-discrete,overlay var(--skra-comp-datepicker-motion-duration) allow-discrete}.skra-datepicker-panel:popover-open{display:block;opacity:1}@starting-style{.skra-datepicker-panel:popover-open{opacity:0}}.skra-datepicker-header{display:flex;align-items:center;justify-content:space-between;gap:var(--skra-comp-datepicker-gap);margin-block-end:var(--skra-comp-datepicker-panel-padding)}.skra-datepicker-monthyear{flex:1 1 auto;text-align:center;color:var(--skra-comp-datepicker-header-foreground);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing)}.skra-datepicker-nav{flex:none;display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-nav-icon-size);block-size:var(--skra-comp-datepicker-nav-icon-size);margin:0;padding:0;background:transparent;border:0;color:var(--skra-comp-datepicker-nav-color);cursor:pointer}.skra-datepicker-nav svg{inline-size:100%;block-size:100%}.skra-datepicker-nav:hover{color:var(--skra-comp-datepicker-nav-color-hover)}.skra-datepicker-nav:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-grid{border-collapse:collapse}.skra-datepicker-weekday{inline-size:var(--skra-comp-datepicker-day-size);padding:0;color:var(--skra-comp-datepicker-weekday-foreground);font-family:var(--skra-sys-typography-role-body-small-font-family);font-size:var(--skra-sys-typography-role-body-small-font-size);font-weight:var(--skra-sys-typography-role-body-small-font-weight);line-height:var(--skra-sys-typography-role-body-small-line-height);letter-spacing:var(--skra-sys-typography-role-body-small-letter-spacing)}.skra-datepicker-weekday abbr{text-decoration:none}.skra-datepicker-day-empty{padding:0}.skra-datepicker-day{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-day-size);block-size:var(--skra-comp-datepicker-day-size);margin:0;padding:0;background:transparent;border:0;border-radius:var(--skra-comp-datepicker-day-radius);color:var(--skra-comp-datepicker-day-foreground);cursor:pointer;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}.skra-datepicker-day:hover:not([aria-disabled=true]){background:var(--skra-comp-datepicker-day-background-hover)}.skra-datepicker-day:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:-.0625rem}.skra-datepicker-day[data-skra-today]{box-shadow:inset 0 0 0 .125rem var(--skra-comp-datepicker-day-today-indicator-color)}.skra-datepicker-day[data-skra-selected]{background:var(--skra-comp-datepicker-day-background-selected);color:var(--skra-comp-datepicker-day-foreground-selected)}.skra-datepicker-day[aria-disabled=true]{color:var(--skra-comp-datepicker-day-foreground-disabled);cursor:default}@media(prefers-reduced-motion:reduce){.skra-datepicker-trigger,.skra-datepicker-panel,.skra-datepicker-day{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1148
+ `, isInline: true, styles: ["@charset \"UTF-8\";:host{display:inline-block;box-sizing:border-box;margin:0}.skra-datepicker-trigger{display:flex;align-items:center;gap:var(--skra-comp-datepicker-gap);inline-size:100%;min-block-size:var(--skra-comp-datepicker-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-datepicker-padding-block);padding-inline:var(--skra-comp-datepicker-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-datepicker-background);color:var(--skra-comp-datepicker-foreground);border-color:var(--skra-comp-datepicker-border-color);border-width:var(--skra-comp-datepicker-border-width);border-style:var(--skra-comp-datepicker-border-style);border-radius:var(--skra-comp-datepicker-radius);box-shadow:var(--skra-comp-datepicker-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),border-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}.skra-datepicker-trigger:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-trigger:disabled{background:var(--skra-comp-datepicker-background-disabled);color:var(--skra-comp-datepicker-foreground-disabled);cursor:not-allowed}.skra-datepicker-trigger[aria-readonly=true]{background:var(--skra-comp-datepicker-background-readonly);cursor:default}.skra-datepicker-trigger[aria-invalid=true]{border-color:var(--skra-comp-datepicker-border-color-validation-invalid)}.skra-datepicker-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skra-datepicker-placeholder{flex:1 1 auto;color:var(--skra-comp-datepicker-foreground-disabled)}.skra-datepicker-icon{flex:none;inline-size:var(--skra-comp-datepicker-icon-size);block-size:var(--skra-comp-datepicker-icon-size);color:var(--skra-comp-datepicker-icon-color)}.skra-datepicker-panel{position:fixed;margin:0;inset:auto;box-sizing:border-box;inline-size:max-content;padding:var(--skra-comp-datepicker-panel-padding);background:var(--skra-comp-datepicker-panel-background);color:var(--skra-comp-datepicker-panel-foreground);border:var(--skra-comp-datepicker-border-width) solid var(--skra-comp-datepicker-panel-border-color);border-radius:var(--skra-comp-datepicker-panel-radius);box-shadow:var(--skra-comp-datepicker-panel-shadow);opacity:0;transition:opacity var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),display var(--skra-comp-datepicker-motion-duration) allow-discrete,overlay var(--skra-comp-datepicker-motion-duration) allow-discrete}.skra-datepicker-panel:popover-open{display:block;opacity:1}@starting-style{.skra-datepicker-panel:popover-open{opacity:0}}.skra-datepicker-header{display:flex;align-items:center;justify-content:space-between;gap:var(--skra-comp-datepicker-gap);margin-block-end:var(--skra-comp-datepicker-panel-padding)}.skra-datepicker-period{flex:1 1 auto;text-align:center;color:var(--skra-comp-datepicker-header-foreground);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing)}.skra-datepicker-nav{flex:none;display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-nav-icon-size);block-size:var(--skra-comp-datepicker-nav-icon-size);margin:0;padding:0;background:transparent;border:0;color:var(--skra-comp-datepicker-nav-color);cursor:pointer}.skra-datepicker-nav svg{inline-size:100%;block-size:100%}@media(hover:hover)and (pointer:fine){.skra-datepicker-nav:hover{color:var(--skra-comp-datepicker-nav-color-hover)}}.skra-datepicker-nav:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-grid{border-collapse:collapse}.skra-datepicker-weekday{inline-size:var(--skra-comp-datepicker-cell-block-size);padding:0;color:var(--skra-comp-datepicker-weekday-foreground);font-family:var(--skra-sys-typography-role-body-small-font-family);font-size:var(--skra-sys-typography-role-body-small-font-size);font-weight:var(--skra-sys-typography-role-body-small-font-weight);line-height:var(--skra-sys-typography-role-body-small-line-height);letter-spacing:var(--skra-sys-typography-role-body-small-letter-spacing)}.skra-datepicker-weekday abbr{text-decoration:none}.skra-datepicker-cell-empty{padding:0}.skra-datepicker-cell{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-cell-block-size);block-size:var(--skra-comp-datepicker-cell-block-size);margin:0;padding:0;background:transparent;border:0;border-radius:var(--skra-comp-datepicker-cell-radius);color:var(--skra-comp-datepicker-cell-foreground);cursor:pointer;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}@media(hover:hover)and (pointer:fine){.skra-datepicker-cell:hover:not([aria-disabled=true]){background:var(--skra-comp-datepicker-cell-background-hover)}}.skra-datepicker-cell:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:-.0625rem}.skra-datepicker-cell[data-skra-today]{box-shadow:inset 0 0 0 .125rem var(--skra-comp-datepicker-cell-today-indicator-color)}.skra-datepicker-cell[data-skra-selected]{background:var(--skra-comp-datepicker-cell-background-selected);color:var(--skra-comp-datepicker-cell-foreground-selected)}.skra-datepicker-cell[aria-disabled=true]{color:var(--skra-comp-datepicker-cell-foreground-disabled);cursor:default}.skra-datepicker-grid--month .skra-datepicker-cell{inline-size:auto;min-inline-size:max(var(--skra-comp-datepicker-month-cell-min-inline-size),var(--skra-comp-datepicker-cell-block-size) * 7 / 3)}@media(prefers-reduced-motion:reduce){.skra-datepicker-trigger,.skra-datepicker-panel,.skra-datepicker-cell{transition-duration:0ms}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
740
1149
  }
741
1150
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: SkraDatePicker, decorators: [{
742
1151
  type: Component,
@@ -784,7 +1193,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
784
1193
  popover="auto"
785
1194
  role="dialog"
786
1195
  aria-modal="false"
787
- [attr.aria-labelledby]="monthYearId"
1196
+ [attr.aria-labelledby]="periodId"
788
1197
  (toggle)="onPanelToggle($event)"
789
1198
  >
790
1199
  <div class="skra-datepicker-header">
@@ -792,8 +1201,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
792
1201
  type="button"
793
1202
  class="skra-datepicker-nav"
794
1203
  tabindex="-1"
795
- [attr.aria-label]="prevMonthLabel()"
796
- (click)="goToPreviousMonth()"
1204
+ [attr.aria-label]="prevPeriodLabel()"
1205
+ (click)="goToPreviousPeriod()"
797
1206
  >
798
1207
  <svg
799
1208
  viewBox="0 0 24 24"
@@ -806,17 +1215,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
806
1215
  </svg>
807
1216
  </button>
808
1217
  <span
809
- class="skra-datepicker-monthyear"
810
- [id]="monthYearId"
1218
+ class="skra-datepicker-period"
1219
+ [id]="periodId"
811
1220
  aria-live="polite"
812
- >{{ monthYearLabel() }}</span
1221
+ >{{ periodLabel() }}</span
813
1222
  >
814
1223
  <button
815
1224
  type="button"
816
1225
  class="skra-datepicker-nav"
817
1226
  tabindex="-1"
818
- [attr.aria-label]="nextMonthLabel()"
819
- (click)="goToNextMonth()"
1227
+ [attr.aria-label]="nextPeriodLabel()"
1228
+ (click)="goToNextPeriod()"
820
1229
  >
821
1230
  <svg
822
1231
  viewBox="0 0 24 24"
@@ -833,21 +1242,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
833
1242
  <table
834
1243
  class="skra-datepicker-grid"
835
1244
  role="grid"
836
- [attr.aria-labelledby]="monthYearId"
1245
+ [class.skra-datepicker-grid--month]="view() === 'month'"
1246
+ [attr.aria-labelledby]="periodId"
837
1247
  >
838
- <thead>
839
- <tr>
840
- @for (wd of weekdayHeadersView(); track wd.key) {
841
- <th scope="col" class="skra-datepicker-weekday">
842
- <abbr [title]="wd.long">{{ wd.short }}</abbr>
843
- </th>
844
- }
845
- </tr>
846
- </thead>
1248
+ @if (view() !== 'month') {
1249
+ <thead>
1250
+ <tr>
1251
+ @for (wd of weekdayHeadersView(); track wd.key) {
1252
+ <th scope="col" class="skra-datepicker-weekday">
1253
+ <abbr [title]="wd.long">{{ wd.short }}</abbr>
1254
+ </th>
1255
+ }
1256
+ </tr>
1257
+ </thead>
1258
+ }
847
1259
  <tbody>
848
- @for (week of weeks(); track week.key) {
1260
+ @for (row of rows(); track row.key) {
849
1261
  <tr>
850
- @for (cell of week.days; track cell.key) {
1262
+ @for (cell of row.cells; track cell.key) {
851
1263
  @if (cell.iso) {
852
1264
  <td
853
1265
  role="gridcell"
@@ -855,23 +1267,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
855
1267
  >
856
1268
  <button
857
1269
  type="button"
858
- class="skra-datepicker-day"
1270
+ class="skra-datepicker-cell"
859
1271
  [attr.tabindex]="cell.focused ? 0 : -1"
860
- [attr.aria-current]="cell.today ? 'date' : null"
1272
+ [attr.aria-current]="cell.today ? currentToken() : null"
861
1273
  [attr.aria-disabled]="cell.disabled ? 'true' : null"
862
1274
  [attr.data-skra-selected]="cell.selected ? '' : null"
863
1275
  [attr.data-skra-today]="cell.today ? '' : null"
864
- (click)="selectDate(cell.iso)"
1276
+ (click)="selectCell(cell.iso)"
865
1277
  (keydown)="onGridKeydown($event)"
866
1278
  >
867
- {{ cell.dayNum
868
- }}<span class="skra-datepicker-day-sr" skra-sr-only>{{
869
- cell.srName
1279
+ {{ cell.label
1280
+ }}<span class="skra-datepicker-cell-sr" skra-sr-only>{{
1281
+ cell.srExt
870
1282
  }}</span>
871
1283
  </button>
872
1284
  </td>
873
1285
  } @else {
874
- <td class="skra-datepicker-day-empty" aria-hidden="true"></td>
1286
+ <td
1287
+ class="skra-datepicker-cell-empty"
1288
+ aria-hidden="true"
1289
+ ></td>
875
1290
  }
876
1291
  }
877
1292
  </tr>
@@ -883,8 +1298,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", ngImpor
883
1298
  '[attr.data-skra-disabled]': 'isDisabled() ? "" : null',
884
1299
  '[attr.data-skra-readonly]': 'isReadonly() ? "" : null',
885
1300
  '[attr.data-skra-validation]': 'hasError() ? "invalid" : null',
886
- }, styles: ["@charset \"UTF-8\";:host{display:inline-block;box-sizing:border-box;margin:0}.skra-datepicker-trigger{display:flex;align-items:center;gap:var(--skra-comp-datepicker-gap);inline-size:100%;min-block-size:var(--skra-comp-datepicker-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-datepicker-padding-block);padding-inline:var(--skra-comp-datepicker-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-datepicker-background);color:var(--skra-comp-datepicker-foreground);border-color:var(--skra-comp-datepicker-border-color);border-width:var(--skra-comp-datepicker-border-width);border-style:var(--skra-comp-datepicker-border-style);border-radius:var(--skra-comp-datepicker-radius);box-shadow:var(--skra-comp-datepicker-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),border-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}.skra-datepicker-trigger:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-trigger:disabled{background:var(--skra-comp-datepicker-background-disabled);color:var(--skra-comp-datepicker-foreground-disabled);cursor:not-allowed}.skra-datepicker-trigger[aria-readonly=true]{background:var(--skra-comp-datepicker-background-readonly);cursor:default}.skra-datepicker-trigger[aria-invalid=true]{border-color:var(--skra-comp-datepicker-border-color-validation-invalid)}.skra-datepicker-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skra-datepicker-placeholder{flex:1 1 auto;color:var(--skra-comp-datepicker-foreground-disabled)}.skra-datepicker-icon{flex:none;inline-size:var(--skra-comp-datepicker-icon-size);block-size:var(--skra-comp-datepicker-icon-size);color:var(--skra-comp-datepicker-icon-color)}.skra-datepicker-panel{position:fixed;margin:0;inset:auto;box-sizing:border-box;inline-size:max-content;padding:var(--skra-comp-datepicker-panel-padding);background:var(--skra-comp-datepicker-panel-background);color:var(--skra-comp-datepicker-panel-foreground);border:var(--skra-comp-datepicker-border-width) solid var(--skra-comp-datepicker-panel-border-color);border-radius:var(--skra-comp-datepicker-panel-radius);box-shadow:var(--skra-comp-datepicker-panel-shadow);opacity:0;transition:opacity var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),display var(--skra-comp-datepicker-motion-duration) allow-discrete,overlay var(--skra-comp-datepicker-motion-duration) allow-discrete}.skra-datepicker-panel:popover-open{display:block;opacity:1}@starting-style{.skra-datepicker-panel:popover-open{opacity:0}}.skra-datepicker-header{display:flex;align-items:center;justify-content:space-between;gap:var(--skra-comp-datepicker-gap);margin-block-end:var(--skra-comp-datepicker-panel-padding)}.skra-datepicker-monthyear{flex:1 1 auto;text-align:center;color:var(--skra-comp-datepicker-header-foreground);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing)}.skra-datepicker-nav{flex:none;display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-nav-icon-size);block-size:var(--skra-comp-datepicker-nav-icon-size);margin:0;padding:0;background:transparent;border:0;color:var(--skra-comp-datepicker-nav-color);cursor:pointer}.skra-datepicker-nav svg{inline-size:100%;block-size:100%}.skra-datepicker-nav:hover{color:var(--skra-comp-datepicker-nav-color-hover)}.skra-datepicker-nav:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-grid{border-collapse:collapse}.skra-datepicker-weekday{inline-size:var(--skra-comp-datepicker-day-size);padding:0;color:var(--skra-comp-datepicker-weekday-foreground);font-family:var(--skra-sys-typography-role-body-small-font-family);font-size:var(--skra-sys-typography-role-body-small-font-size);font-weight:var(--skra-sys-typography-role-body-small-font-weight);line-height:var(--skra-sys-typography-role-body-small-line-height);letter-spacing:var(--skra-sys-typography-role-body-small-letter-spacing)}.skra-datepicker-weekday abbr{text-decoration:none}.skra-datepicker-day-empty{padding:0}.skra-datepicker-day{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-day-size);block-size:var(--skra-comp-datepicker-day-size);margin:0;padding:0;background:transparent;border:0;border-radius:var(--skra-comp-datepicker-day-radius);color:var(--skra-comp-datepicker-day-foreground);cursor:pointer;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}.skra-datepicker-day:hover:not([aria-disabled=true]){background:var(--skra-comp-datepicker-day-background-hover)}.skra-datepicker-day:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:-.0625rem}.skra-datepicker-day[data-skra-today]{box-shadow:inset 0 0 0 .125rem var(--skra-comp-datepicker-day-today-indicator-color)}.skra-datepicker-day[data-skra-selected]{background:var(--skra-comp-datepicker-day-background-selected);color:var(--skra-comp-datepicker-day-foreground-selected)}.skra-datepicker-day[aria-disabled=true]{color:var(--skra-comp-datepicker-day-foreground-disabled);cursor:default}@media(prefers-reduced-motion:reduce){.skra-datepicker-trigger,.skra-datepicker-panel,.skra-datepicker-day{transition-duration:0ms}}\n"] }]
887
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
1301
+ }, styles: ["@charset \"UTF-8\";:host{display:inline-block;box-sizing:border-box;margin:0}.skra-datepicker-trigger{display:flex;align-items:center;gap:var(--skra-comp-datepicker-gap);inline-size:100%;min-block-size:var(--skra-comp-datepicker-min-block-size);box-sizing:border-box;margin:0;padding-block:var(--skra-comp-datepicker-padding-block);padding-inline:var(--skra-comp-datepicker-padding-inline);text-align:start;cursor:pointer;background:var(--skra-comp-datepicker-background);color:var(--skra-comp-datepicker-foreground);border-color:var(--skra-comp-datepicker-border-color);border-width:var(--skra-comp-datepicker-border-width);border-style:var(--skra-comp-datepicker-border-style);border-radius:var(--skra-comp-datepicker-radius);box-shadow:var(--skra-comp-datepicker-shadow);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),border-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}.skra-datepicker-trigger:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-trigger:disabled{background:var(--skra-comp-datepicker-background-disabled);color:var(--skra-comp-datepicker-foreground-disabled);cursor:not-allowed}.skra-datepicker-trigger[aria-readonly=true]{background:var(--skra-comp-datepicker-background-readonly);cursor:default}.skra-datepicker-trigger[aria-invalid=true]{border-color:var(--skra-comp-datepicker-border-color-validation-invalid)}.skra-datepicker-value{flex:1 1 auto;min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.skra-datepicker-placeholder{flex:1 1 auto;color:var(--skra-comp-datepicker-foreground-disabled)}.skra-datepicker-icon{flex:none;inline-size:var(--skra-comp-datepicker-icon-size);block-size:var(--skra-comp-datepicker-icon-size);color:var(--skra-comp-datepicker-icon-color)}.skra-datepicker-panel{position:fixed;margin:0;inset:auto;box-sizing:border-box;inline-size:max-content;padding:var(--skra-comp-datepicker-panel-padding);background:var(--skra-comp-datepicker-panel-background);color:var(--skra-comp-datepicker-panel-foreground);border:var(--skra-comp-datepicker-border-width) solid var(--skra-comp-datepicker-panel-border-color);border-radius:var(--skra-comp-datepicker-panel-radius);box-shadow:var(--skra-comp-datepicker-panel-shadow);opacity:0;transition:opacity var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing),display var(--skra-comp-datepicker-motion-duration) allow-discrete,overlay var(--skra-comp-datepicker-motion-duration) allow-discrete}.skra-datepicker-panel:popover-open{display:block;opacity:1}@starting-style{.skra-datepicker-panel:popover-open{opacity:0}}.skra-datepicker-header{display:flex;align-items:center;justify-content:space-between;gap:var(--skra-comp-datepicker-gap);margin-block-end:var(--skra-comp-datepicker-panel-padding)}.skra-datepicker-period{flex:1 1 auto;text-align:center;color:var(--skra-comp-datepicker-header-foreground);font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing)}.skra-datepicker-nav{flex:none;display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-nav-icon-size);block-size:var(--skra-comp-datepicker-nav-icon-size);margin:0;padding:0;background:transparent;border:0;color:var(--skra-comp-datepicker-nav-color);cursor:pointer}.skra-datepicker-nav svg{inline-size:100%;block-size:100%}@media(hover:hover)and (pointer:fine){.skra-datepicker-nav:hover{color:var(--skra-comp-datepicker-nav-color-hover)}}.skra-datepicker-nav:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:.125rem}.skra-datepicker-grid{border-collapse:collapse}.skra-datepicker-weekday{inline-size:var(--skra-comp-datepicker-cell-block-size);padding:0;color:var(--skra-comp-datepicker-weekday-foreground);font-family:var(--skra-sys-typography-role-body-small-font-family);font-size:var(--skra-sys-typography-role-body-small-font-size);font-weight:var(--skra-sys-typography-role-body-small-font-weight);line-height:var(--skra-sys-typography-role-body-small-line-height);letter-spacing:var(--skra-sys-typography-role-body-small-letter-spacing)}.skra-datepicker-weekday abbr{text-decoration:none}.skra-datepicker-cell-empty{padding:0}.skra-datepicker-cell{display:inline-flex;align-items:center;justify-content:center;inline-size:var(--skra-comp-datepicker-cell-block-size);block-size:var(--skra-comp-datepicker-cell-block-size);margin:0;padding:0;background:transparent;border:0;border-radius:var(--skra-comp-datepicker-cell-radius);color:var(--skra-comp-datepicker-cell-foreground);cursor:pointer;font-family:var(--skra-sys-typography-role-body-medium-font-family);font-size:var(--skra-sys-typography-role-body-medium-font-size);font-weight:var(--skra-sys-typography-role-body-medium-font-weight);line-height:var(--skra-sys-typography-role-body-medium-line-height);letter-spacing:var(--skra-sys-typography-role-body-medium-letter-spacing);transition:background-color var(--skra-comp-datepicker-motion-duration) var(--skra-comp-datepicker-motion-easing)}@media(hover:hover)and (pointer:fine){.skra-datepicker-cell:hover:not([aria-disabled=true]){background:var(--skra-comp-datepicker-cell-background-hover)}}.skra-datepicker-cell:focus-visible{outline:var(--skra-comp-datepicker-focus-ring-width) solid var(--skra-comp-datepicker-focus-ring-color);outline-offset:-.0625rem}.skra-datepicker-cell[data-skra-today]{box-shadow:inset 0 0 0 .125rem var(--skra-comp-datepicker-cell-today-indicator-color)}.skra-datepicker-cell[data-skra-selected]{background:var(--skra-comp-datepicker-cell-background-selected);color:var(--skra-comp-datepicker-cell-foreground-selected)}.skra-datepicker-cell[aria-disabled=true]{color:var(--skra-comp-datepicker-cell-foreground-disabled);cursor:default}.skra-datepicker-grid--month .skra-datepicker-cell{inline-size:auto;min-inline-size:max(var(--skra-comp-datepicker-month-cell-min-inline-size),var(--skra-comp-datepicker-cell-block-size) * 7 / 3)}@media(prefers-reduced-motion:reduce){.skra-datepicker-trigger,.skra-datepicker-panel,.skra-datepicker-cell{transition-duration:0ms}}\n"] }]
1302
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], precision: [{ type: i0.Input, args: [{ isSignal: true, alias: "precision", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
888
1303
 
889
1304
  /**
890
1305
  * Generated bundle index. Do not edit.