@skra/ui 0.2.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 (160) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +39 -0
  4. package/consumer-guide/README.md +84 -0
  5. package/consumer-guide/accessible-names.md +80 -0
  6. package/consumer-guide/forms-and-binding.md +149 -0
  7. package/consumer-guide/layout.md +86 -0
  8. package/consumer-guide/overlays.md +100 -0
  9. package/consumer-guide/setup-and-gotchas.md +103 -0
  10. package/consumer-guide/theming-and-encapsulation.md +115 -0
  11. package/consumer-guide/wrapping-patterns.md +90 -0
  12. package/fesm2022/skra-ui-accordion.mjs +333 -0
  13. package/fesm2022/skra-ui-accordion.mjs.map +1 -0
  14. package/fesm2022/skra-ui-alert.mjs +110 -0
  15. package/fesm2022/skra-ui-alert.mjs.map +1 -0
  16. package/fesm2022/skra-ui-avatar.mjs +83 -0
  17. package/fesm2022/skra-ui-avatar.mjs.map +1 -0
  18. package/fesm2022/skra-ui-badge.mjs +91 -0
  19. package/fesm2022/skra-ui-badge.mjs.map +1 -0
  20. package/fesm2022/skra-ui-button.mjs +67 -0
  21. package/fesm2022/skra-ui-button.mjs.map +1 -0
  22. package/fesm2022/skra-ui-checkbox.mjs +98 -0
  23. package/fesm2022/skra-ui-checkbox.mjs.map +1 -0
  24. package/fesm2022/skra-ui-chip.mjs +82 -0
  25. package/fesm2022/skra-ui-chip.mjs.map +1 -0
  26. package/fesm2022/skra-ui-combobox.mjs +335 -0
  27. package/fesm2022/skra-ui-combobox.mjs.map +1 -0
  28. package/fesm2022/skra-ui-core.mjs +529 -0
  29. package/fesm2022/skra-ui-core.mjs.map +1 -0
  30. package/fesm2022/skra-ui-counter.mjs +53 -0
  31. package/fesm2022/skra-ui-counter.mjs.map +1 -0
  32. package/fesm2022/skra-ui-datepicker.mjs +867 -0
  33. package/fesm2022/skra-ui-datepicker.mjs.map +1 -0
  34. package/fesm2022/skra-ui-dialog.mjs +133 -0
  35. package/fesm2022/skra-ui-dialog.mjs.map +1 -0
  36. package/fesm2022/skra-ui-field.mjs +293 -0
  37. package/fesm2022/skra-ui-field.mjs.map +1 -0
  38. package/fesm2022/skra-ui-icon.mjs +54 -0
  39. package/fesm2022/skra-ui-icon.mjs.map +1 -0
  40. package/fesm2022/skra-ui-input.mjs +42 -0
  41. package/fesm2022/skra-ui-input.mjs.map +1 -0
  42. package/fesm2022/skra-ui-kbd.mjs +44 -0
  43. package/fesm2022/skra-ui-kbd.mjs.map +1 -0
  44. package/fesm2022/skra-ui-layout.mjs +119 -0
  45. package/fesm2022/skra-ui-layout.mjs.map +1 -0
  46. package/fesm2022/skra-ui-link.mjs +29 -0
  47. package/fesm2022/skra-ui-link.mjs.map +1 -0
  48. package/fesm2022/skra-ui-loading-indicator.mjs +24 -0
  49. package/fesm2022/skra-ui-loading-indicator.mjs.map +1 -0
  50. package/fesm2022/skra-ui-menu.mjs +334 -0
  51. package/fesm2022/skra-ui-menu.mjs.map +1 -0
  52. package/fesm2022/skra-ui-multi-select.mjs +179 -0
  53. package/fesm2022/skra-ui-multi-select.mjs.map +1 -0
  54. package/fesm2022/skra-ui-native-select.mjs +123 -0
  55. package/fesm2022/skra-ui-native-select.mjs.map +1 -0
  56. package/fesm2022/skra-ui-popover.mjs +158 -0
  57. package/fesm2022/skra-ui-popover.mjs.map +1 -0
  58. package/fesm2022/skra-ui-progress.mjs +57 -0
  59. package/fesm2022/skra-ui-progress.mjs.map +1 -0
  60. package/fesm2022/skra-ui-radio.mjs +92 -0
  61. package/fesm2022/skra-ui-radio.mjs.map +1 -0
  62. package/fesm2022/skra-ui-rating-display.mjs +81 -0
  63. package/fesm2022/skra-ui-rating-display.mjs.map +1 -0
  64. package/fesm2022/skra-ui-select.mjs +151 -0
  65. package/fesm2022/skra-ui-select.mjs.map +1 -0
  66. package/fesm2022/skra-ui-skeleton.mjs +65 -0
  67. package/fesm2022/skra-ui-skeleton.mjs.map +1 -0
  68. package/fesm2022/skra-ui-star-rating.mjs +110 -0
  69. package/fesm2022/skra-ui-star-rating.mjs.map +1 -0
  70. package/fesm2022/skra-ui-status-dot.mjs +61 -0
  71. package/fesm2022/skra-ui-status-dot.mjs.map +1 -0
  72. package/fesm2022/skra-ui-switch.mjs +94 -0
  73. package/fesm2022/skra-ui-switch.mjs.map +1 -0
  74. package/fesm2022/skra-ui-tabs.mjs +356 -0
  75. package/fesm2022/skra-ui-tabs.mjs.map +1 -0
  76. package/fesm2022/skra-ui-textarea.mjs +81 -0
  77. package/fesm2022/skra-ui-textarea.mjs.map +1 -0
  78. package/fesm2022/skra-ui-toast.mjs +208 -0
  79. package/fesm2022/skra-ui-toast.mjs.map +1 -0
  80. package/fesm2022/skra-ui-tooltip.mjs +252 -0
  81. package/fesm2022/skra-ui-tooltip.mjs.map +1 -0
  82. package/fesm2022/skra-ui-typography.mjs +35 -0
  83. package/fesm2022/skra-ui-typography.mjs.map +1 -0
  84. package/fesm2022/skra-ui.mjs +4 -0
  85. package/fesm2022/skra-ui.mjs.map +1 -0
  86. package/package.json +175 -0
  87. package/types/skra-ui-accordion.d.ts +196 -0
  88. package/types/skra-ui-accordion.d.ts.map +1 -0
  89. package/types/skra-ui-alert.d.ts +81 -0
  90. package/types/skra-ui-alert.d.ts.map +1 -0
  91. package/types/skra-ui-avatar.d.ts +71 -0
  92. package/types/skra-ui-avatar.d.ts.map +1 -0
  93. package/types/skra-ui-badge.d.ts +73 -0
  94. package/types/skra-ui-badge.d.ts.map +1 -0
  95. package/types/skra-ui-button.d.ts +45 -0
  96. package/types/skra-ui-button.d.ts.map +1 -0
  97. package/types/skra-ui-checkbox.d.ts +52 -0
  98. package/types/skra-ui-checkbox.d.ts.map +1 -0
  99. package/types/skra-ui-chip.d.ts +60 -0
  100. package/types/skra-ui-chip.d.ts.map +1 -0
  101. package/types/skra-ui-combobox.d.ts +95 -0
  102. package/types/skra-ui-combobox.d.ts.map +1 -0
  103. package/types/skra-ui-core.d.ts +314 -0
  104. package/types/skra-ui-core.d.ts.map +1 -0
  105. package/types/skra-ui-counter.d.ts +33 -0
  106. package/types/skra-ui-counter.d.ts.map +1 -0
  107. package/types/skra-ui-datepicker.d.ts +164 -0
  108. package/types/skra-ui-datepicker.d.ts.map +1 -0
  109. package/types/skra-ui-dialog.d.ts +59 -0
  110. package/types/skra-ui-dialog.d.ts.map +1 -0
  111. package/types/skra-ui-field.d.ts +181 -0
  112. package/types/skra-ui-field.d.ts.map +1 -0
  113. package/types/skra-ui-icon.d.ts +38 -0
  114. package/types/skra-ui-icon.d.ts.map +1 -0
  115. package/types/skra-ui-input.d.ts +22 -0
  116. package/types/skra-ui-input.d.ts.map +1 -0
  117. package/types/skra-ui-kbd.d.ts +33 -0
  118. package/types/skra-ui-kbd.d.ts.map +1 -0
  119. package/types/skra-ui-layout.d.ts +86 -0
  120. package/types/skra-ui-layout.d.ts.map +1 -0
  121. package/types/skra-ui-link.d.ts +20 -0
  122. package/types/skra-ui-link.d.ts.map +1 -0
  123. package/types/skra-ui-loading-indicator.d.ts +9 -0
  124. package/types/skra-ui-loading-indicator.d.ts.map +1 -0
  125. package/types/skra-ui-menu.d.ts +116 -0
  126. package/types/skra-ui-menu.d.ts.map +1 -0
  127. package/types/skra-ui-multi-select.d.ts +47 -0
  128. package/types/skra-ui-multi-select.d.ts.map +1 -0
  129. package/types/skra-ui-native-select.d.ts +74 -0
  130. package/types/skra-ui-native-select.d.ts.map +1 -0
  131. package/types/skra-ui-popover.d.ts +57 -0
  132. package/types/skra-ui-popover.d.ts.map +1 -0
  133. package/types/skra-ui-progress.d.ts +41 -0
  134. package/types/skra-ui-progress.d.ts.map +1 -0
  135. package/types/skra-ui-radio.d.ts +54 -0
  136. package/types/skra-ui-radio.d.ts.map +1 -0
  137. package/types/skra-ui-rating-display.d.ts +56 -0
  138. package/types/skra-ui-rating-display.d.ts.map +1 -0
  139. package/types/skra-ui-select.d.ts +43 -0
  140. package/types/skra-ui-select.d.ts.map +1 -0
  141. package/types/skra-ui-skeleton.d.ts +42 -0
  142. package/types/skra-ui-skeleton.d.ts.map +1 -0
  143. package/types/skra-ui-star-rating.d.ts +71 -0
  144. package/types/skra-ui-star-rating.d.ts.map +1 -0
  145. package/types/skra-ui-status-dot.d.ts +62 -0
  146. package/types/skra-ui-status-dot.d.ts.map +1 -0
  147. package/types/skra-ui-switch.d.ts +54 -0
  148. package/types/skra-ui-switch.d.ts.map +1 -0
  149. package/types/skra-ui-tabs.d.ts +191 -0
  150. package/types/skra-ui-tabs.d.ts.map +1 -0
  151. package/types/skra-ui-textarea.d.ts +55 -0
  152. package/types/skra-ui-textarea.d.ts.map +1 -0
  153. package/types/skra-ui-toast.d.ts +140 -0
  154. package/types/skra-ui-toast.d.ts.map +1 -0
  155. package/types/skra-ui-tooltip.d.ts +90 -0
  156. package/types/skra-ui-tooltip.d.ts.map +1 -0
  157. package/types/skra-ui-typography.d.ts +17 -0
  158. package/types/skra-ui-typography.d.ts.map +1 -0
  159. package/types/skra-ui.d.ts +3 -0
  160. package/types/skra-ui.d.ts.map +1 -0
@@ -0,0 +1,867 @@
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';
3
+ import { SkraFieldContext } from '@skra/ui/field';
4
+ import { _attachPositioner } from '@skra/ui/core';
5
+
6
+ /**
7
+ * calendar-math — the ONE sanctioned site for DatePicker date arithmetic (ADR 0050 §3/§6).
8
+ *
9
+ * Zoneless calendar-date discipline (the timezone off-by-one is the bug class this exists to prevent):
10
+ * - The value is an ISO 8601 date-only string (`YYYY-MM-DD`) — NEVER materialized as a `Date`.
11
+ * - `Date` is used ONLY as a UTC calendar calculator — built via `Date.UTC(...)`, read via `getUTC*`.
12
+ * - `new Date(isoString)` is FORBIDDEN (date-only parses as UTC midnight → local accessors shift it).
13
+ * - "today" uses LOCAL accessors — "today" is inherently local-civil.
14
+ * - Month length + leap years are delegated to the platform engine, NEVER a hand-written leap rule.
15
+ *
16
+ * Months are **1-based** here (1=Jan … 12=Dec) to mirror the ISO string; the single 0-based conversion
17
+ * happens at the `Date.UTC` call sites. Weekdays are **ISO** (1=Mon … 7=Sun) to match `firstDayOfWeek`.
18
+ *
19
+ * This module is the ADR 0050 §6 swap-point: a future `Temporal.PlainDate` migration touches only here.
20
+ */
21
+ const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
22
+ /** Zero-pad to a fixed width (no locale, no `Intl` — pure formatting). */
23
+ function pad(n, width) {
24
+ return String(n).padStart(width, '0');
25
+ }
26
+ /** Days in month `m` (1-based) of year `y` — delegated to the engine (leap years handled for free). */
27
+ function daysInMonth(y, m) {
28
+ // Date.UTC month is 0-based; passing the 1-based `m` selects the NEXT month, day 0 = last of `m`.
29
+ return new Date(Date.UTC(y, m, 0)).getUTCDate();
30
+ }
31
+ /** ISO weekday (1=Mon … 7=Sun) of an arbitrary {y,m,d}, via the UTC calculator. */
32
+ function isoWeekday(y, m, d) {
33
+ const jsDay = new Date(Date.UTC(y, m - 1, d)).getUTCDay(); // 0=Sun … 6=Sat
34
+ return ((jsDay + 6) % 7) + 1; // → 1=Mon … 7=Sun
35
+ }
36
+ /** Strict `YYYY-MM-DD` parse with real calendar validation. Returns `null` for anything invalid. */
37
+ function parseIso(iso) {
38
+ const match = ISO_DATE.exec(iso);
39
+ if (!match)
40
+ return null;
41
+ const y = Number(match[1]);
42
+ const m = Number(match[2]);
43
+ const d = Number(match[3]);
44
+ if (m < 1 || m > 12)
45
+ return null;
46
+ if (d < 1 || d > daysInMonth(y, m))
47
+ return null; // rejects 2026-02-30, 2026-04-31, etc.
48
+ return { y, m, d };
49
+ }
50
+ /** `true` iff `iso` is a strictly valid `YYYY-MM-DD` calendar date. */
51
+ function isValidIso(iso) {
52
+ return parseIso(iso) !== null;
53
+ }
54
+ /** Serialize a calendar date to `YYYY-MM-DD`. */
55
+ function toIso({ y, m, d }) {
56
+ return `${pad(y, 4)}-${pad(m, 2)}-${pad(d, 2)}`;
57
+ }
58
+ /**
59
+ * Build a UTC `Date` from an ISO string — the ONLY sanctioned `Date`-from-value site (used by
60
+ * `calendar-intl` to feed `Intl.DateTimeFormat`, which MUST also pass `timeZone:'UTC'`). Returns the
61
+ * value as a UTC instant; callers MUST read it back via UTC accessors / a UTC-zoned formatter ONLY.
62
+ */
63
+ function toUtcDate(iso) {
64
+ const date = parseIso(iso);
65
+ if (!date)
66
+ return null;
67
+ return new Date(Date.UTC(date.y, date.m - 1, date.d));
68
+ }
69
+ /** Today's LOCAL calendar date as ISO (local accessors — "today" is local-civil, ADR 0050 §3). */
70
+ function today() {
71
+ const now = new Date();
72
+ return toIso({
73
+ y: now.getFullYear(),
74
+ m: now.getMonth() + 1,
75
+ d: now.getDate(),
76
+ });
77
+ }
78
+ /** Add `n` days (may be negative); crosses month/year boundaries via the UTC engine. */
79
+ function addDays(iso, n) {
80
+ const date = parseIso(iso);
81
+ if (!date)
82
+ return iso;
83
+ const dt = new Date(Date.UTC(date.y, date.m - 1, date.d));
84
+ dt.setUTCDate(dt.getUTCDate() + n);
85
+ return toIso({
86
+ y: dt.getUTCFullYear(),
87
+ m: dt.getUTCMonth() + 1,
88
+ d: dt.getUTCDate(),
89
+ });
90
+ }
91
+ /**
92
+ * Add `n` months (may be negative), preserving day-of-month CLAMPED to the target month length
93
+ * (ADR 0050 §4): Jan 31 + 1 month → Feb 28/29, never Mar 3.
94
+ */
95
+ function addMonthsClamped(iso, n) {
96
+ const date = parseIso(iso);
97
+ if (!date)
98
+ return iso;
99
+ const total = date.y * 12 + (date.m - 1) + n;
100
+ const y = Math.floor(total / 12);
101
+ const m = (((total % 12) + 12) % 12) + 1; // 1-based, safe for negative totals
102
+ const d = Math.min(date.d, daysInMonth(y, m));
103
+ return toIso({ y, m, d });
104
+ }
105
+ /** Add `n` years, clamped (Feb 29 + 1 year → Feb 28). Delegates to month arithmetic. */
106
+ function addYearsClamped(iso, n) {
107
+ return addMonthsClamped(iso, n * 12);
108
+ }
109
+ /** Lexicographic comparison — valid because same-format ISO 8601 date-only strings are byte-sortable. */
110
+ function compareIso(a, b) {
111
+ return a < b ? -1 : a > b ? 1 : 0;
112
+ }
113
+ /** Inclusive range test. `''` bound = unbounded on that side (ADR 0050 §2, lexicographic). */
114
+ function isInRange(iso, min, max) {
115
+ if (min !== '' && compareIso(iso, min) < 0)
116
+ return false;
117
+ if (max !== '' && compareIso(iso, max) > 0)
118
+ return false;
119
+ return true;
120
+ }
121
+ /** Clamp `iso` into `[min,max]` (each `''` = unbounded). Used to seed the displayed month / focus. */
122
+ function clampIso(iso, min, max) {
123
+ if (min !== '' && compareIso(iso, min) < 0)
124
+ return min;
125
+ if (max !== '' && compareIso(iso, max) > 0)
126
+ return max;
127
+ return iso;
128
+ }
129
+ /**
130
+ * Build a month grid: weeks of 7 cells, each a day ISO string or `null` (leading/trailing padding —
131
+ * adjacent-month days are NOT rendered in MVP). `firstDayOfWeek` is ISO (1=Mon … 7=Sun).
132
+ * Pure + deterministic; the component decorates cells with selected/today/disabled/focused state.
133
+ */
134
+ function monthGrid(y, m, firstDayOfWeek) {
135
+ const firstWeekday = isoWeekday(y, m, 1); // ISO weekday of the 1st
136
+ const leadingBlanks = (firstWeekday - firstDayOfWeek + 7) % 7;
137
+ const dim = daysInMonth(y, m);
138
+ const cells = [];
139
+ for (let i = 0; i < leadingBlanks; i++)
140
+ cells.push(null);
141
+ for (let d = 1; d <= dim; d++)
142
+ cells.push(toIso({ y, m, d }));
143
+ while (cells.length % 7 !== 0)
144
+ cells.push(null); // trailing padding
145
+ const weeks = [];
146
+ for (let i = 0; i < cells.length; i += 7)
147
+ weeks.push(cells.slice(i, i + 7));
148
+ return weeks;
149
+ }
150
+
151
+ const NONE = { type: 'none' };
152
+ /**
153
+ * SkraCalendarKeyboard — the WAI-ARIA APG **Date Picker Dialog** grid keyboard model (2-D), confined to
154
+ * ONE file. The THIRD per-pattern keyboard controller and the FIRST that does NOT wrap a CDK key manager:
155
+ * 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
157
+ * is delegated to `calendar-math` (the ADR 0050 single math site); this controller only maps keys to
158
+ * intents. Roving REAL focus (NOT `aria-activedescendant`) — the divergence from Select/Combobox.
159
+ *
160
+ * Navigation is unclamped (a user may move onto a disabled out-of-range day — APG-discoverable); only
161
+ * SELECTION is range-guarded, and that guard lives in the component (one place, shared with pointer click).
162
+ */
163
+ class SkraCalendarKeyboard {
164
+ resolve(event, ctx) {
165
+ const { focusedIso, firstDayOfWeek } = ctx;
166
+ switch (event.key) {
167
+ case 'ArrowLeft':
168
+ return this.toFocus(focusedIso, addDays(focusedIso, -1));
169
+ case 'ArrowRight':
170
+ return this.toFocus(focusedIso, addDays(focusedIso, 1));
171
+ case 'ArrowUp':
172
+ return this.toFocus(focusedIso, addDays(focusedIso, -7));
173
+ case 'ArrowDown':
174
+ return this.toFocus(focusedIso, addDays(focusedIso, 7));
175
+ case 'Home':
176
+ return this.toFocus(focusedIso, this.weekStart(focusedIso, firstDayOfWeek));
177
+ case 'End':
178
+ return this.toFocus(focusedIso, addDays(this.weekStart(focusedIso, firstDayOfWeek), 6));
179
+ case 'PageUp':
180
+ return this.toFocus(focusedIso, event.shiftKey
181
+ ? addYearsClamped(focusedIso, -1)
182
+ : addMonthsClamped(focusedIso, -1));
183
+ case 'PageDown':
184
+ return this.toFocus(focusedIso, event.shiftKey
185
+ ? addYearsClamped(focusedIso, 1)
186
+ : addMonthsClamped(focusedIso, 1));
187
+ case 'Enter':
188
+ case ' ':
189
+ case 'Spacebar': // legacy key name
190
+ return { type: 'select' };
191
+ case 'Escape':
192
+ return { type: 'close' };
193
+ default:
194
+ return NONE;
195
+ }
196
+ }
197
+ toFocus(fromIso, toIso) {
198
+ return {
199
+ type: 'focus',
200
+ iso: toIso,
201
+ monthChanged: this.monthChanged(fromIso, toIso),
202
+ };
203
+ }
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;
208
+ }
209
+ weekStart(iso, firstDayOfWeek) {
210
+ const p = parseIso(iso);
211
+ if (!p)
212
+ return iso;
213
+ const offset = (isoWeekday(p.y, p.m, p.d) - firstDayOfWeek + 7) % 7;
214
+ return addDays(iso, -offset);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * calendar-intl — DatePicker localization via the platform `Intl` (ADR 0050 §5). NO locale string
220
+ * catalog is bundled (atoms-agnostic); everything language/digit-related is derived from `Intl`, keyed
221
+ * by the consumer `locale`. Every `Date` fed to a formatter comes from `calendar-math.toUtcDate` AND the
222
+ * formatter MUST set `timeZone: 'UTC'` (else the displayed date shifts off the zoneless value).
223
+ */
224
+ const UTC = 'UTC';
225
+ /** Resolve the host's default BCP-47 locale tag, SSR-safe (no `navigator` in Node). */
226
+ function hostLocale() {
227
+ return typeof navigator !== 'undefined' ? navigator.language : undefined;
228
+ }
229
+ /** Trigger display of the selected date (`dateStyle: 'medium'`). `''`/invalid → `''`. */
230
+ function formatValue(iso, locale) {
231
+ const dt = toUtcDate(iso);
232
+ if (!dt)
233
+ return '';
234
+ return new Intl.DateTimeFormat(locale, {
235
+ dateStyle: 'medium',
236
+ timeZone: UTC,
237
+ }).format(dt);
238
+ }
239
+ /** Header / nav-button label for a month: localized `month long + year` (e.g. "czerwiec 2026"). */
240
+ function formatMonthYear(y, m, locale) {
241
+ const dt = toUtcDate(`${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-01`);
242
+ if (!dt)
243
+ return '';
244
+ return new Intl.DateTimeFormat(locale, {
245
+ month: 'long',
246
+ year: 'numeric',
247
+ timeZone: UTC,
248
+ }).format(dt);
249
+ }
250
+ /** Full localized date for a day cell's `[skra-sr-only]` accessible-name extension (ADR 033 Path 2). */
251
+ function formatDayName(iso, locale) {
252
+ const dt = toUtcDate(iso);
253
+ if (!dt)
254
+ return '';
255
+ return new Intl.DateTimeFormat(locale, {
256
+ dateStyle: 'long',
257
+ timeZone: UTC,
258
+ }).format(dt);
259
+ }
260
+ /** Visible day number, localized digits (e.g. Arabic-Indic `٥`) — never a raw `toString()`. */
261
+ function formatDayNumber(day, locale) {
262
+ return new Intl.NumberFormat(locale).format(day);
263
+ }
264
+ /**
265
+ * Localized weekday column headers, 7 entries starting at `firstDayOfWeek` (ISO 1=Mon … 7=Sun).
266
+ * Reference week: 2024-01-01 is a Monday, so day-of-month `d` (1–7) maps directly to ISO weekday `d`.
267
+ */
268
+ function weekdayHeaders(firstDayOfWeek, locale) {
269
+ const shortFmt = new Intl.DateTimeFormat(locale, {
270
+ weekday: 'short',
271
+ timeZone: UTC,
272
+ });
273
+ const longFmt = new Intl.DateTimeFormat(locale, {
274
+ weekday: 'long',
275
+ timeZone: UTC,
276
+ });
277
+ const headers = [];
278
+ for (let i = 0; i < 7; i++) {
279
+ const dow = ((firstDayOfWeek - 1 + i) % 7) + 1; // 1 … 7
280
+ const dt = toUtcDate(`2024-01-0${dow}`); // dow 1–7 → '2024-01-01' … '2024-01-07'
281
+ headers.push({
282
+ key: dow,
283
+ short: shortFmt.format(dt),
284
+ long: longFmt.format(dt),
285
+ });
286
+ }
287
+ return headers;
288
+ }
289
+ /**
290
+ * Resolve the first day of the week (ISO 1=Mon … 7=Sun): explicit `override` → `Intl` `getWeekInfo` →
291
+ * **Monday fallback**. The override is the MVP safety valve — `Intl.Locale.getWeekInfo()` support is
292
+ * spotty (older Firefox/Safari), so the detection path genuinely fails in mainstream browsers and the
293
+ * Monday fallback is wrong for en-US (Sun) / much of the Middle East (Sat) (ADR 0050 §5).
294
+ */
295
+ function resolveFirstDayOfWeek(override, locale) {
296
+ if (typeof override === 'number' && override >= 1 && override <= 7)
297
+ return override;
298
+ try {
299
+ const loc = new Intl.Locale(locale ?? hostLocale() ?? 'en');
300
+ const info = loc.getWeekInfo?.() ?? loc.weekInfo; // method (modern) or property (older engines)
301
+ const firstDay = info?.firstDay;
302
+ if (typeof firstDay === 'number' && firstDay >= 1 && firstDay <= 7)
303
+ return firstDay;
304
+ }
305
+ catch {
306
+ /* unsupported / malformed locale → fall through to the documented fallback */
307
+ }
308
+ return 1; // Monday (ISO-8601 week start)
309
+ }
310
+
311
+ let nextDatePickerId = 0;
312
+ /**
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).
320
+ *
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).
325
+ *
326
+ * Full contract: adr/specs/component-datepicker-spec.md.
327
+ */
328
+ class SkraDatePicker {
329
+ /** Consumer-owned value: ISO 8601 date-only (`YYYY-MM-DD`); `''` = unset. NEVER a Date (ADR 0050/037). */
330
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
331
+ /** Inclusive lower bound (ISO); `''` = unbounded (ADR 0050 §2). */
332
+ min = input('', ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
333
+ /** Inclusive upper bound (ISO); `''` = unbounded. */
334
+ max = input('', ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
335
+ /** `Intl` locale (BCP-47); `undefined` → host default (ADR 0050 §5). */
336
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
337
+ /** First day of week, ISO 1=Mon … 7=Sun; overrides `Intl.getWeekInfo` (the MVP safety valve, ADR 0050 §5). */
338
+ firstDayOfWeek = input(undefined, ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []));
339
+ /** Value-empty hint in the trigger (NOT the accessible name — ADR 033). */
340
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
341
+ /** Placement intent; flip/shift adjust at runtime. */
342
+ placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
343
+ /** Standalone availability source — `SkraFieldContext` wins when present. */
344
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
345
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
346
+ /** Fires ONLY on user date selection — never on a programmatic `value()` change (ADR 037/0050). */
347
+ valueChange = output();
348
+ triggerRef = viewChild('trigger', ...(ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []));
349
+ panelRef = viewChild('panel', ...(ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []));
350
+ context = inject(SkraFieldContext, { optional: true });
351
+ injector = inject(Injector);
352
+ keyboard = new SkraCalendarKeyboard();
353
+ panelId = `skra-datepicker-${nextDatePickerId}`;
354
+ monthYearId = `skra-datepicker-monthyear-${nextDatePickerId++}`;
355
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
356
+ /** Today's LOCAL date (ADR 0050 §3) — stable for the session. */
357
+ todayIso = today();
358
+ /** The roving-focus day; SINGLE source of truth for what the grid shows AND which cell is focusable. */
359
+ focusedIso = signal(this.todayIso, ...(ngDevMode ? [{ debugName: "focusedIso" }] : /* istanbul ignore next */ []));
360
+ positioner;
361
+ /** Set when a close should return focus to the trigger (keyboard/select), not on outside-click. */
362
+ returnFocus = false;
363
+ // ── Availability / validation (SkraFieldContext, ADR 013, with standalone fallbacks) ──
364
+ isDisabled = computed(() => this.context?.isDisabled() ?? this.disabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
365
+ isReadonly = computed(() => this.context?.isReadonly() ?? this.readonly(), ...(ngDevMode ? [{ debugName: "isReadonly" }] : /* istanbul ignore next */ []));
366
+ hasError = computed(() => this.context?.hasError() ?? false, ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
367
+ isRequired = computed(() => this.context?.isRequired() ?? false, ...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
368
+ triggerId = computed(() => this.context?.fieldId() || null, ...(ngDevMode ? [{ debugName: "triggerId" }] : /* istanbul ignore next */ []));
369
+ describedBy = computed(() => this.context?.describedByIds() ?? null, ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
370
+ // ── Derived calendar state ──
371
+ /** The committed value, defensively normalized: invalid/out-of-range → `''` for display (ADR 0050 §1). */
372
+ selectedIso = computed(() => {
373
+ const v = this.value();
374
+ return isValidIso(v) && isInRange(v, this.min(), this.max()) ? v : '';
375
+ }, ...(ngDevMode ? [{ debugName: "selectedIso" }] : /* istanbul ignore next */ []));
376
+ formattedValue = computed(() => formatValue(this.selectedIso(), this.locale()), ...(ngDevMode ? [{ debugName: "formattedValue" }] : /* istanbul ignore next */ []));
377
+ firstDow = computed(() => resolveFirstDayOfWeek(this.firstDayOfWeek(), this.locale()), ...(ngDevMode ? [{ debugName: "firstDow" }] : /* istanbul ignore next */ []));
378
+ /** The month being shown — always the focused day's month (keeps the roving tabindex valid). */
379
+ displayedMonth = computed(() => {
380
+ const p = parseIso(this.focusedIso()) ?? parseIso(this.todayIso);
381
+ return { y: p.y, m: p.m };
382
+ }, ...(ngDevMode ? [{ debugName: "displayedMonth" }] : /* istanbul ignore next */ []));
383
+ monthYearLabel = computed(() => formatMonthYear(this.displayedMonth().y, this.displayedMonth().m, this.locale()), ...(ngDevMode ? [{ debugName: "monthYearLabel" }] : /* istanbul ignore next */ []));
384
+ prevMonthLabel = computed(() => this.monthOffsetLabel(-1), ...(ngDevMode ? [{ debugName: "prevMonthLabel" }] : /* istanbul ignore next */ []));
385
+ nextMonthLabel = computed(() => this.monthOffsetLabel(1), ...(ngDevMode ? [{ debugName: "nextMonthLabel" }] : /* istanbul ignore next */ []));
386
+ weekdayHeadersView = computed(() => weekdayHeaders(this.firstDow(), this.locale()), ...(ngDevMode ? [{ debugName: "weekdayHeadersView" }] : /* istanbul ignore next */ []));
387
+ weeks = computed(() => {
388
+ const { y, m } = this.displayedMonth();
389
+ const loc = this.locale();
390
+ const sel = this.selectedIso();
391
+ const foc = this.focusedIso();
392
+ const min = this.min();
393
+ const max = this.max();
394
+ return monthGrid(y, m, this.firstDow()).map((week, wi) => ({
395
+ key: wi,
396
+ days: week.map((iso, di) => iso === null
397
+ ? this.emptyCell(`${wi}-${di}`)
398
+ : {
399
+ key: iso,
400
+ iso,
401
+ dayNum: formatDayNumber(parseIso(iso).d, loc),
402
+ srName: formatDayName(iso, loc),
403
+ today: iso === this.todayIso,
404
+ selected: iso === sel,
405
+ disabled: !isInRange(iso, min, max),
406
+ focused: iso === foc,
407
+ }),
408
+ }));
409
+ }, ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
410
+ constructor() {
411
+ inject(DestroyRef).onDestroy(() => this.positioner?.destroy());
412
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
413
+ // Dev-only sanity warnings (stripped in prod) — never thrown (ADR 0050 §1).
414
+ const warn = computed(() => {
415
+ const v = this.value();
416
+ if (v !== '' && !isValidIso(v))
417
+ return `skra-datepicker: value "${v}" is not a valid YYYY-MM-DD date; shown as unset.`;
418
+ if (this.min() !== '' &&
419
+ this.max() !== '' &&
420
+ compareIso(this.min(), this.max()) > 0)
421
+ return `skra-datepicker: min "${this.min()}" is after max "${this.max()}" — every day is disabled.`;
422
+ return null;
423
+ }, ...(ngDevMode ? [{ debugName: "warn" }] : /* istanbul ignore next */ []));
424
+ afterNextRender(() => {
425
+ const message = warn();
426
+ if (message)
427
+ console.warn(message);
428
+ }, { injector: this.injector });
429
+ }
430
+ }
431
+ // ── Trigger interaction ──
432
+ onTriggerClick(event) {
433
+ if (this.isReadonly())
434
+ event.preventDefault(); // block the native popovertarget open
435
+ }
436
+ onTriggerKeydown(event) {
437
+ if (this.isDisabled() || this.isReadonly() || this.isOpen())
438
+ return;
439
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
440
+ event.preventDefault();
441
+ this.open(); // Enter/Space/click open via the native popovertarget
442
+ }
443
+ }
444
+ // ── Grid keyboard (APG Date Picker, 2-D) ──
445
+ onGridKeydown(event) {
446
+ if (event.key === 'Tab') {
447
+ // APG: Tab dismisses the calendar; focus returns to the trigger (deterministic, no containment).
448
+ event.preventDefault();
449
+ this.returnFocus = true;
450
+ this.close();
451
+ return;
452
+ }
453
+ const intent = this.keyboard.resolve(event, {
454
+ focusedIso: this.focusedIso(),
455
+ firstDayOfWeek: this.firstDow(),
456
+ });
457
+ switch (intent.type) {
458
+ case 'focus':
459
+ event.preventDefault();
460
+ this.focusedIso.set(intent.iso);
461
+ this.scheduleFocus(); // re-focus after the grid re-stamps
462
+ break;
463
+ case 'select':
464
+ event.preventDefault();
465
+ this.selectDate(this.focusedIso());
466
+ break;
467
+ case 'close':
468
+ this.returnFocus = true;
469
+ this.close();
470
+ break;
471
+ case 'none':
472
+ break;
473
+ }
474
+ }
475
+ // ── Month navigation (pointer; focus stays on the nav button) ──
476
+ goToPreviousMonth() {
477
+ this.focusedIso.set(addMonthsClamped(this.focusedIso(), -1));
478
+ }
479
+ goToNextMonth() {
480
+ this.focusedIso.set(addMonthsClamped(this.focusedIso(), 1));
481
+ }
482
+ // ── Selection ──
483
+ selectDate(iso) {
484
+ if (!isInRange(iso, this.min(), this.max()))
485
+ return; // guard out-of-range (one place: keyboard + pointer)
486
+ this.returnFocus = true;
487
+ this.valueChange.emit(iso);
488
+ this.close();
489
+ }
490
+ // ── Panel open / close (native toggle from the Popover API) ──
491
+ onPanelToggle(event) {
492
+ const open = event.newState === 'open';
493
+ this.isOpen.set(open);
494
+ if (open)
495
+ this.onOpen();
496
+ else
497
+ this.onClose();
498
+ }
499
+ onOpen() {
500
+ this.focusedIso.set(this.seedFocus());
501
+ const anchor = this.triggerRef()?.nativeElement;
502
+ const panel = this.panelRef()?.nativeElement;
503
+ if (anchor && panel) {
504
+ this.positioner?.destroy();
505
+ this.positioner = _attachPositioner(anchor, panel, () => ({
506
+ placement: this.placement(),
507
+ offset: this.cssLenPx('--skra-comp-datepicker-panel-offset'),
508
+ viewportPadding: this.cssLenPx('--skra-comp-datepicker-panel-viewport-padding'),
509
+ }));
510
+ }
511
+ this.scheduleFocus();
512
+ }
513
+ onClose() {
514
+ this.positioner?.destroy();
515
+ this.positioner = undefined;
516
+ if (this.returnFocus) {
517
+ this.returnFocus = false;
518
+ this.triggerRef()?.nativeElement.focus();
519
+ }
520
+ }
521
+ open() {
522
+ this.panelRef()?.nativeElement.showPopover?.();
523
+ }
524
+ close() {
525
+ this.panelRef()?.nativeElement.hidePopover?.();
526
+ }
527
+ /** Seed the focused day on open: the valid current value, else today, clamped into `[min,max]`. */
528
+ seedFocus() {
529
+ const v = this.value();
530
+ const base = isValidIso(v) ? v : this.todayIso;
531
+ return clampIso(base, this.min(), this.max());
532
+ }
533
+ monthOffsetLabel(n) {
534
+ const { y, m } = this.displayedMonth();
535
+ const ref = parseIso(addMonthsClamped(toIso({ y, m, d: 1 }), n));
536
+ return formatMonthYear(ref.y, ref.m, this.locale());
537
+ }
538
+ emptyCell(key) {
539
+ return {
540
+ key,
541
+ iso: null,
542
+ dayNum: '',
543
+ srName: '',
544
+ today: false,
545
+ selected: false,
546
+ disabled: true,
547
+ focused: false,
548
+ };
549
+ }
550
+ scheduleFocus() {
551
+ afterNextRender(() => this.focusActiveDay(), { injector: this.injector });
552
+ }
553
+ focusActiveDay() {
554
+ this.panelRef()
555
+ ?.nativeElement.querySelector('.skra-datepicker-day[tabindex="0"]')
556
+ ?.focus();
557
+ }
558
+ /** Read a length comp token (px/rem) off the panel computed style for the positioner. */
559
+ cssLenPx(prop) {
560
+ const el = this.panelRef()?.nativeElement;
561
+ if (!el)
562
+ return 0;
563
+ const raw = getComputedStyle(el).getPropertyValue(prop).trim();
564
+ if (raw.endsWith('px'))
565
+ return parseFloat(raw);
566
+ if (raw.endsWith('rem')) {
567
+ const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
568
+ return parseFloat(raw) * rootFontSize;
569
+ }
570
+ return parseFloat(raw) || 0;
571
+ }
572
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraDatePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
573
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.7", 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: `
574
+ <button
575
+ #trigger
576
+ class="skra-datepicker-trigger"
577
+ type="button"
578
+ role="combobox"
579
+ aria-haspopup="dialog"
580
+ [attr.id]="triggerId()"
581
+ [attr.popovertarget]="panelId"
582
+ [attr.aria-controls]="panelId"
583
+ [attr.aria-expanded]="isOpen()"
584
+ [attr.aria-describedby]="describedBy()"
585
+ [attr.aria-invalid]="hasError() ? 'true' : null"
586
+ [attr.aria-required]="isRequired() ? 'true' : null"
587
+ [attr.aria-readonly]="isReadonly() ? 'true' : null"
588
+ [disabled]="isDisabled()"
589
+ (click)="onTriggerClick($event)"
590
+ (keydown)="onTriggerKeydown($event)"
591
+ >
592
+ @if (formattedValue(); as label) {
593
+ <span class="skra-datepicker-value">{{ label }}</span>
594
+ } @else {
595
+ <span class="skra-datepicker-placeholder">{{ placeholder() }}</span>
596
+ }
597
+ <svg
598
+ class="skra-datepicker-icon"
599
+ aria-hidden="true"
600
+ viewBox="0 0 24 24"
601
+ fill="none"
602
+ stroke="currentColor"
603
+ stroke-width="2"
604
+ >
605
+ <rect x="3" y="4" width="18" height="17" rx="2" />
606
+ <path d="M3 9h18M8 2v4M16 2v4" />
607
+ </svg>
608
+ </button>
609
+
610
+ <div
611
+ #panel
612
+ class="skra-datepicker-panel"
613
+ [id]="panelId"
614
+ popover="auto"
615
+ role="dialog"
616
+ aria-modal="false"
617
+ [attr.aria-labelledby]="monthYearId"
618
+ (toggle)="onPanelToggle($event)"
619
+ >
620
+ <div class="skra-datepicker-header">
621
+ <button
622
+ type="button"
623
+ class="skra-datepicker-nav"
624
+ tabindex="-1"
625
+ [attr.aria-label]="prevMonthLabel()"
626
+ (click)="goToPreviousMonth()"
627
+ >
628
+ <svg
629
+ viewBox="0 0 24 24"
630
+ fill="none"
631
+ stroke="currentColor"
632
+ stroke-width="2"
633
+ aria-hidden="true"
634
+ >
635
+ <path d="M15 6l-6 6 6 6" />
636
+ </svg>
637
+ </button>
638
+ <span
639
+ class="skra-datepicker-monthyear"
640
+ [id]="monthYearId"
641
+ aria-live="polite"
642
+ >{{ monthYearLabel() }}</span
643
+ >
644
+ <button
645
+ type="button"
646
+ class="skra-datepicker-nav"
647
+ tabindex="-1"
648
+ [attr.aria-label]="nextMonthLabel()"
649
+ (click)="goToNextMonth()"
650
+ >
651
+ <svg
652
+ viewBox="0 0 24 24"
653
+ fill="none"
654
+ stroke="currentColor"
655
+ stroke-width="2"
656
+ aria-hidden="true"
657
+ >
658
+ <path d="M9 6l6 6-6 6" />
659
+ </svg>
660
+ </button>
661
+ </div>
662
+
663
+ <table
664
+ class="skra-datepicker-grid"
665
+ role="grid"
666
+ [attr.aria-labelledby]="monthYearId"
667
+ >
668
+ <thead>
669
+ <tr>
670
+ @for (wd of weekdayHeadersView(); track wd.key) {
671
+ <th scope="col" class="skra-datepicker-weekday">
672
+ <abbr [title]="wd.long">{{ wd.short }}</abbr>
673
+ </th>
674
+ }
675
+ </tr>
676
+ </thead>
677
+ <tbody>
678
+ @for (week of weeks(); track week.key) {
679
+ <tr>
680
+ @for (cell of week.days; track cell.key) {
681
+ @if (cell.iso) {
682
+ <td
683
+ role="gridcell"
684
+ [attr.aria-selected]="cell.selected ? 'true' : null"
685
+ >
686
+ <button
687
+ type="button"
688
+ class="skra-datepicker-day"
689
+ [attr.tabindex]="cell.focused ? 0 : -1"
690
+ [attr.aria-current]="cell.today ? 'date' : null"
691
+ [attr.aria-disabled]="cell.disabled ? 'true' : null"
692
+ [attr.data-skra-selected]="cell.selected ? '' : null"
693
+ [attr.data-skra-today]="cell.today ? '' : null"
694
+ (click)="selectDate(cell.iso)"
695
+ (keydown)="onGridKeydown($event)"
696
+ >
697
+ {{ cell.dayNum
698
+ }}<span class="skra-datepicker-day-sr" skra-sr-only>{{
699
+ cell.srName
700
+ }}</span>
701
+ </button>
702
+ </td>
703
+ } @else {
704
+ <td class="skra-datepicker-day-empty" aria-hidden="true"></td>
705
+ }
706
+ }
707
+ </tr>
708
+ }
709
+ </tbody>
710
+ </table>
711
+ </div>
712
+ `, 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 });
713
+ }
714
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.7", ngImport: i0, type: SkraDatePicker, decorators: [{
715
+ type: Component,
716
+ args: [{ selector: 'skra-datepicker', standalone: true, exportAs: 'skraDatePicker', template: `
717
+ <button
718
+ #trigger
719
+ class="skra-datepicker-trigger"
720
+ type="button"
721
+ role="combobox"
722
+ aria-haspopup="dialog"
723
+ [attr.id]="triggerId()"
724
+ [attr.popovertarget]="panelId"
725
+ [attr.aria-controls]="panelId"
726
+ [attr.aria-expanded]="isOpen()"
727
+ [attr.aria-describedby]="describedBy()"
728
+ [attr.aria-invalid]="hasError() ? 'true' : null"
729
+ [attr.aria-required]="isRequired() ? 'true' : null"
730
+ [attr.aria-readonly]="isReadonly() ? 'true' : null"
731
+ [disabled]="isDisabled()"
732
+ (click)="onTriggerClick($event)"
733
+ (keydown)="onTriggerKeydown($event)"
734
+ >
735
+ @if (formattedValue(); as label) {
736
+ <span class="skra-datepicker-value">{{ label }}</span>
737
+ } @else {
738
+ <span class="skra-datepicker-placeholder">{{ placeholder() }}</span>
739
+ }
740
+ <svg
741
+ class="skra-datepicker-icon"
742
+ aria-hidden="true"
743
+ viewBox="0 0 24 24"
744
+ fill="none"
745
+ stroke="currentColor"
746
+ stroke-width="2"
747
+ >
748
+ <rect x="3" y="4" width="18" height="17" rx="2" />
749
+ <path d="M3 9h18M8 2v4M16 2v4" />
750
+ </svg>
751
+ </button>
752
+
753
+ <div
754
+ #panel
755
+ class="skra-datepicker-panel"
756
+ [id]="panelId"
757
+ popover="auto"
758
+ role="dialog"
759
+ aria-modal="false"
760
+ [attr.aria-labelledby]="monthYearId"
761
+ (toggle)="onPanelToggle($event)"
762
+ >
763
+ <div class="skra-datepicker-header">
764
+ <button
765
+ type="button"
766
+ class="skra-datepicker-nav"
767
+ tabindex="-1"
768
+ [attr.aria-label]="prevMonthLabel()"
769
+ (click)="goToPreviousMonth()"
770
+ >
771
+ <svg
772
+ viewBox="0 0 24 24"
773
+ fill="none"
774
+ stroke="currentColor"
775
+ stroke-width="2"
776
+ aria-hidden="true"
777
+ >
778
+ <path d="M15 6l-6 6 6 6" />
779
+ </svg>
780
+ </button>
781
+ <span
782
+ class="skra-datepicker-monthyear"
783
+ [id]="monthYearId"
784
+ aria-live="polite"
785
+ >{{ monthYearLabel() }}</span
786
+ >
787
+ <button
788
+ type="button"
789
+ class="skra-datepicker-nav"
790
+ tabindex="-1"
791
+ [attr.aria-label]="nextMonthLabel()"
792
+ (click)="goToNextMonth()"
793
+ >
794
+ <svg
795
+ viewBox="0 0 24 24"
796
+ fill="none"
797
+ stroke="currentColor"
798
+ stroke-width="2"
799
+ aria-hidden="true"
800
+ >
801
+ <path d="M9 6l6 6-6 6" />
802
+ </svg>
803
+ </button>
804
+ </div>
805
+
806
+ <table
807
+ class="skra-datepicker-grid"
808
+ role="grid"
809
+ [attr.aria-labelledby]="monthYearId"
810
+ >
811
+ <thead>
812
+ <tr>
813
+ @for (wd of weekdayHeadersView(); track wd.key) {
814
+ <th scope="col" class="skra-datepicker-weekday">
815
+ <abbr [title]="wd.long">{{ wd.short }}</abbr>
816
+ </th>
817
+ }
818
+ </tr>
819
+ </thead>
820
+ <tbody>
821
+ @for (week of weeks(); track week.key) {
822
+ <tr>
823
+ @for (cell of week.days; track cell.key) {
824
+ @if (cell.iso) {
825
+ <td
826
+ role="gridcell"
827
+ [attr.aria-selected]="cell.selected ? 'true' : null"
828
+ >
829
+ <button
830
+ type="button"
831
+ class="skra-datepicker-day"
832
+ [attr.tabindex]="cell.focused ? 0 : -1"
833
+ [attr.aria-current]="cell.today ? 'date' : null"
834
+ [attr.aria-disabled]="cell.disabled ? 'true' : null"
835
+ [attr.data-skra-selected]="cell.selected ? '' : null"
836
+ [attr.data-skra-today]="cell.today ? '' : null"
837
+ (click)="selectDate(cell.iso)"
838
+ (keydown)="onGridKeydown($event)"
839
+ >
840
+ {{ cell.dayNum
841
+ }}<span class="skra-datepicker-day-sr" skra-sr-only>{{
842
+ cell.srName
843
+ }}</span>
844
+ </button>
845
+ </td>
846
+ } @else {
847
+ <td class="skra-datepicker-day-empty" aria-hidden="true"></td>
848
+ }
849
+ }
850
+ </tr>
851
+ }
852
+ </tbody>
853
+ </table>
854
+ </div>
855
+ `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, host: {
856
+ '[attr.data-skra-disabled]': 'isDisabled() ? "" : null',
857
+ '[attr.data-skra-readonly]': 'isReadonly() ? "" : null',
858
+ '[attr.data-skra-validation]': 'hasError() ? "invalid" : null',
859
+ }, 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"] }]
860
+ }], 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 }] }] } });
861
+
862
+ /**
863
+ * Generated bundle index. Do not edit.
864
+ */
865
+
866
+ export { SkraCalendarKeyboard, SkraDatePicker };
867
+ //# sourceMappingURL=skra-ui-datepicker.mjs.map