@uni-design-system/uni-angular 8.0.0 → 8.1.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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, contentChildren, output, Renderer2, ElementRef, Directive, model, effect, viewChild, afterNextRender, ViewChild, afterRenderEffect, booleanAttribute, viewChildren } from '@angular/core';
3
3
  import { injectGlobal, css, keyframes } from '@emotion/css';
4
- import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, removeInputPlatformStyling, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
4
+ import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, removeInputPlatformStyling, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
5
5
  import { NgClass, NgTemplateOutlet, CommonModule } from '@angular/common';
6
6
 
7
7
  let nextUniqueId = 0;
@@ -50,6 +50,294 @@ function motionSafe(styles) {
50
50
  return { '@media (prefers-reduced-motion: no-preference)': styles };
51
51
  }
52
52
 
53
+ /**
54
+ * Canonical date/time value shapes shared by `uni-calendar`,
55
+ * `uni-date-input`, `uni-time-input` and `uni-date-time-input`.
56
+ *
57
+ * Plain ISO strings, never `Date`s: a calendar date is a label on a wall
58
+ * calendar, not an instant. `new Date('2026-08-20')` is midnight UTC — which
59
+ * is 19 Aug in Honolulu — the classic off-by-one every `Date`-valued picker
60
+ * ships. Strings are timezone-free, JSON-serializable and sortable with `<`.
61
+ * Zoned scheduling is the app's concern; these components never touch
62
+ * `Date.getTimezoneOffset()`.
63
+ */
64
+
65
+ function memoize(fn) {
66
+ const cache = new Map();
67
+ return ((...args) => {
68
+ const key = JSON.stringify(args);
69
+ if (cache.has(key)) {
70
+ return cache.get(key);
71
+ }
72
+ const result = fn(...args);
73
+ cache.set(key, result);
74
+ return result;
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Pure date/time helpers: ISO string math, `Intl`-driven formatting and
80
+ * parsing. No date library — `Date` appears only as UTC arithmetic inside
81
+ * these functions, so no result ever depends on the machine's timezone.
82
+ */
83
+ const pad = (n) => String(n).padStart(2, '0');
84
+ const toUTC = (date) => {
85
+ const [y, m, d] = date.split('-').map(Number);
86
+ return Date.UTC(y, m - 1, d);
87
+ };
88
+ const fromUTC = (timestamp) => {
89
+ const d = new Date(timestamp);
90
+ return isoDate(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
91
+ };
92
+ /** `(2026, 8, 5)` → `'2026-08-05'`. Does not validate — see `isValidDate`. */
93
+ const isoDate = (year, month, day) => `${year}-${pad(month)}-${pad(day)}`;
94
+ /** The ISO date when the parts form a real calendar day, else `null`. */
95
+ const isValidDate = (year, month, day) => month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month)
96
+ ? isoDate(year, month, day)
97
+ : null;
98
+ /** Days in a month, leap-year aware. */
99
+ const daysInMonth = (year, month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
100
+ const addDays = (date, days) => fromUTC(toUTC(date) + days * 86_400_000);
101
+ /** Same day ± n months, clamping the day to the target month's length. */
102
+ const addMonths = (date, months) => {
103
+ let [y, m] = date.split('-').map(Number);
104
+ const d = Number(date.slice(8, 10));
105
+ const total = y * 12 + (m - 1) + months;
106
+ y = Math.floor(total / 12);
107
+ m = ((total % 12) + 12) % 12 + 1;
108
+ return isoDate(y, m, Math.min(d, daysInMonth(y, m)));
109
+ };
110
+ /** Day of week, 0 = Sunday … 6 = Saturday. */
111
+ const dayOfWeek = (date) => new Date(toUTC(date)).getUTCDay();
112
+ /** `'2026-08-20'` → `'2026-08'`. */
113
+ const monthOf = (date) => date.slice(0, 7);
114
+ /** Today as a local-wall-clock ISO date (the one place local time matters). */
115
+ const todayIso = () => {
116
+ const d = new Date();
117
+ return isoDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
118
+ };
119
+ /** Inclusive day count of a range: `('-20', '-24')` → 5. */
120
+ const inclusiveDayCount = (start, end) => Math.round((toUTC(end) - toUTC(start)) / 86_400_000) + 1;
121
+ /**
122
+ * The week matrix a calendar renders: full weeks covering `'YYYY-MM'`,
123
+ * starting on `weekStart` (0 = Sunday). Outside cells are always present so
124
+ * showing or hiding them is a pure render decision.
125
+ */
126
+ const buildMonthGrid = (month, weekStart) => {
127
+ const [y, m] = month.split('-').map(Number);
128
+ const first = isoDate(y, m, 1);
129
+ const offset = (dayOfWeek(first) - weekStart + 7) % 7;
130
+ const total = daysInMonth(y, m);
131
+ const weeks = [];
132
+ let cursor = addDays(first, -offset);
133
+ for (let w = 0; w < Math.ceil((offset + total) / 7); w++) {
134
+ const week = [];
135
+ for (let c = 0; c < 7; c++) {
136
+ week.push({ date: cursor, outside: monthOf(cursor) !== month });
137
+ cursor = addDays(cursor, 1);
138
+ }
139
+ weeks.push(week);
140
+ }
141
+ return weeks;
142
+ };
143
+ // ---- Intl formatting (always UTC — the ISO string is re-hydrated as UTC
144
+ // midnight, so the local zone can never shift the label) ---------------------
145
+ const formatDate = (date, locale, options = { dateStyle: 'medium' }) => new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }).format(new Date(toUTC(date)));
146
+ /** `'2026-08'` → `'August 2026'` in the locale. */
147
+ const formatMonthHeading = (month, locale) => formatDate(`${month}-01`, locale, { month: 'long', year: 'numeric' });
148
+ /** Weekday header labels starting at `weekStart` (0 = Sunday). */
149
+ const weekdayNames = memoize((locale, weekStart, format) => {
150
+ const labels = [];
151
+ for (let i = 0; i < 7; i++) {
152
+ // 2023-01-01 is a Sunday.
153
+ const day = new Date(Date.UTC(2023, 0, 1 + ((weekStart + i) % 7)));
154
+ labels.push({
155
+ label: new Intl.DateTimeFormat(locale, { weekday: format, timeZone: 'UTC' }).format(day),
156
+ full: new Intl.DateTimeFormat(locale, { weekday: 'long', timeZone: 'UTC' }).format(day),
157
+ });
158
+ }
159
+ return labels;
160
+ });
161
+ /** `'15:00'` → `'3:00 PM'` (or `'15:00'` when `hour12` is false). */
162
+ const formatTime = (time, locale, hour12) => {
163
+ const [h, m] = time.split(':').map(Number);
164
+ return new Intl.DateTimeFormat(locale, {
165
+ hour: 'numeric',
166
+ minute: '2-digit',
167
+ hour12,
168
+ timeZone: 'UTC',
169
+ }).format(new Date(Date.UTC(2000, 0, 1, h, m)));
170
+ };
171
+ /**
172
+ * The locale's first day of week, 0 = Sunday … 6 = Saturday. Falls back to
173
+ * Sunday where `Intl.Locale` week info is unavailable.
174
+ */
175
+ const localeWeekStart = memoize((locale) => {
176
+ try {
177
+ const intlLocale = new Intl.Locale(locale);
178
+ const info = intlLocale.getWeekInfo?.() ?? intlLocale.weekInfo;
179
+ // Intl counts 1 = Monday … 7 = Sunday.
180
+ if (info?.firstDay)
181
+ return info.firstDay % 7;
182
+ }
183
+ catch {
184
+ // Unknown locale tag — fall through to the default.
185
+ }
186
+ return 0;
187
+ });
188
+ /** Whether the locale's default clock is 12-hour. */
189
+ const localeDefaultHour12 = memoize((locale) => new Intl.DateTimeFormat(locale, { hour: 'numeric' }).resolvedOptions().hour12 ?? true);
190
+ /** The locale's numeric-date field order, e.g. en-US → month, day, year. */
191
+ const localeFieldOrder = memoize((locale) => {
192
+ const parts = new Intl.DateTimeFormat(locale, { timeZone: 'UTC' }).formatToParts(new Date(Date.UTC(2000, 11, 31)));
193
+ return parts
194
+ .filter((p) => ['year', 'month', 'day'].includes(p.type))
195
+ .map((p) => p.type);
196
+ });
197
+ /** Long + short month names → month number, lowercased, dots stripped. */
198
+ const localeMonthNames = memoize((locale) => {
199
+ const map = new Map();
200
+ for (let i = 0; i < 12; i++) {
201
+ const date = new Date(Date.UTC(2000, i, 15));
202
+ for (const style of ['long', 'short']) {
203
+ const name = new Intl.DateTimeFormat(locale, { month: style, timeZone: 'UTC' })
204
+ .format(date)
205
+ .toLowerCase()
206
+ .replace(/\./g, '');
207
+ map.set(name, i + 1);
208
+ }
209
+ }
210
+ return map;
211
+ });
212
+ /** The locale's digit pattern as a placeholder, e.g. `'MM/DD/YYYY'`. */
213
+ const localeDatePlaceholder = memoize((locale) => new Intl.DateTimeFormat(locale, { timeZone: 'UTC' })
214
+ .format(new Date(Date.UTC(2000, 11, 31)))
215
+ .replace(/2000/, 'YYYY')
216
+ .replace(/12/, 'MM')
217
+ .replace(/31/, 'DD'));
218
+ // ---- Parsing (Intl-driven — digit order and month names come from the
219
+ // locale, never from a hardcoded table) ---------------------------------------
220
+ /** Missing year → the NEXT occurrence: nobody schedules into the past. */
221
+ const nextOccurrence = (month, day, today) => {
222
+ const year = Number(today.slice(0, 4));
223
+ const candidate = isValidDate(year, month, day);
224
+ if (candidate && candidate >= today)
225
+ return candidate;
226
+ return isValidDate(year + 1, month, day);
227
+ };
228
+ /**
229
+ * Free-typed date text → ISO date, or `null` when unreadable. Accepts, in
230
+ * order: ISO (`2026-08-20`), a month name from the locale's own long/short
231
+ * lists (`aug 20`, `20 aug 2026`, `August 20th`), and locale-numeric text
232
+ * (`8/20/2026`, `20.8.2026`, `08-20`) with the digit order taken from
233
+ * `Intl.DateTimeFormat(locale).formatToParts()`. A missing year resolves to
234
+ * the next occurrence; two-digit years are refused rather than guessed.
235
+ */
236
+ const parseDateText = (raw, locale, options = {}) => {
237
+ const today = options.today ?? todayIso();
238
+ const s = raw
239
+ .trim()
240
+ .toLowerCase()
241
+ .replace(/(\d+)(st|nd|rd|th)\b/g, '$1')
242
+ .replace(/,/g, ' ');
243
+ if (!s)
244
+ return null;
245
+ // 1. ISO — what agents and APIs write.
246
+ const iso = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
247
+ if (iso)
248
+ return isValidDate(+iso[1], +iso[2], +iso[3]);
249
+ // 2. Month name, from the locale's own list.
250
+ const monthNames = localeMonthNames(locale);
251
+ const tokens = s.split(/\s+/);
252
+ const monthToken = tokens.find((t) => monthNames.has(t.replace(/\./g, '')));
253
+ if (monthToken) {
254
+ const month = monthNames.get(monthToken.replace(/\./g, ''));
255
+ const numbers = tokens.filter((t) => /^\d+$/.test(t)).map(Number);
256
+ if (numbers.length === 1 && numbers[0] >= 1 && numbers[0] <= 31)
257
+ return nextOccurrence(month, numbers[0], today);
258
+ if (numbers.length === 2) {
259
+ const year = numbers.find((n) => n >= 1000);
260
+ const day = numbers.find((n) => n <= 31 && n !== year);
261
+ if (year && day)
262
+ return isValidDate(year, month, day);
263
+ }
264
+ return null;
265
+ }
266
+ // 3. Locale numeric — digit order from Intl, not hardcoded.
267
+ const numeric = s.match(/^(\d{1,4})[/.\-\s](\d{1,4})(?:[/.\-\s](\d{1,4}))?$/);
268
+ if (!numeric)
269
+ return null;
270
+ const parts = [numeric[1], numeric[2], numeric[3]].filter((p) => !!p);
271
+ if (parts.length === 3) {
272
+ if (parts[0].length === 4)
273
+ return isValidDate(+parts[0], +parts[1], +parts[2]); // 2026/8/20
274
+ const bag = {};
275
+ localeFieldOrder(locale).forEach((field, i) => (bag[field] = parts[i]));
276
+ if (!bag.year || bag.year.length < 4)
277
+ return null; // two-digit years are refused, not guessed
278
+ return isValidDate(+bag.year, +bag.month, +bag.day);
279
+ }
280
+ // Two numbers: month/day in locale order, year = next occurrence.
281
+ const bag = {};
282
+ localeFieldOrder(locale)
283
+ .filter((field) => field !== 'year')
284
+ .forEach((field, i) => (bag[field] = +parts[i]));
285
+ return bag.month && bag.day ? nextOccurrence(bag.month, bag.day, today) : null;
286
+ };
287
+ /**
288
+ * Free-typed time text → `'HH:mm'`, or `null`. Accepts `9`, `930`, `9:30`,
289
+ * `9.30`, `9 30`, `3p`, `3pm`, `3 PM`, `15:00`. Bare hours 1–7 with no
290
+ * meridiem lean PM when `hour12` — typing `3` into an appointment field
291
+ * means 15:00, not 03:00.
292
+ */
293
+ const parseTimeText = (raw, hour12) => {
294
+ const s = raw.trim().toLowerCase().replace(/\s+/g, '');
295
+ const match = s.match(/^(\d{1,2})(?:[:.h]?(\d{2}))?(a|am|p|pm)?$/);
296
+ if (!match)
297
+ return null;
298
+ let hour = +match[1];
299
+ const minute = match[2] ? +match[2] : 0;
300
+ const meridiem = match[3];
301
+ if (minute > 59)
302
+ return null;
303
+ if (meridiem) {
304
+ if (hour < 1 || hour > 12)
305
+ return null;
306
+ if (meridiem[0] === 'p' && hour !== 12)
307
+ hour += 12;
308
+ if (meridiem[0] === 'a' && hour === 12)
309
+ hour = 0;
310
+ }
311
+ else {
312
+ if (hour > 23)
313
+ return null;
314
+ if (hour12 && hour >= 1 && hour <= 7)
315
+ hour += 12;
316
+ }
317
+ return `${pad(hour)}:${pad(minute)}`;
318
+ };
319
+ /** Every `minuteStep` time between `min` and `max` (inclusive bounds). */
320
+ const timeSlots = (minuteStep, min, max) => {
321
+ const slots = [];
322
+ const lo = min ?? '00:00';
323
+ const hi = max ?? '23:59';
324
+ for (let minutes = 0; minutes < 24 * 60; minutes += minuteStep) {
325
+ const t = `${pad(Math.floor(minutes / 60))}:${pad(minutes % 60)}`;
326
+ if (t >= lo && t <= hi)
327
+ slots.push(t);
328
+ }
329
+ return slots;
330
+ };
331
+ // ---- Combined values --------------------------------------------------------
332
+ const splitDateTime = (value) => {
333
+ if (!value)
334
+ return {};
335
+ const [date, time] = value.split('T');
336
+ return { ...(date ? { date } : {}), ...(time ? { time } : {}) };
337
+ };
338
+ /** One combined value only when both parts are present. */
339
+ const joinDateTime = (date, time) => date && time ? `${date}T${time}` : undefined;
340
+
53
341
  /**
54
342
  * The keyboard and ARIA bookkeeping shared by every combobox-style popup:
55
343
  * open state, the active option index, and the `aria-activedescendant` id
@@ -519,19 +807,6 @@ class UniServerSideDatasource extends UniBaseDatasource {
519
807
  }
520
808
  }
521
809
 
522
- function memoize(fn) {
523
- const cache = new Map();
524
- return ((...args) => {
525
- const key = JSON.stringify(args);
526
- if (cache.has(key)) {
527
- return cache.get(key);
528
- }
529
- const result = fn(...args);
530
- cache.set(key, result);
531
- return result;
532
- });
533
- }
534
-
535
810
  class LocalStorageService {
536
811
  isLocalStorageAvailable = memoize(() => {
537
812
  try {
@@ -958,15 +1233,43 @@ class ThemeService {
958
1233
  const token = (color + '-container');
959
1234
  return this.colorPair(token, useVariant);
960
1235
  };
1236
+ /**
1237
+ * The shared keyboard-focus indicator's styles (WCAG 2.4.7), without a
1238
+ * selector — for controls that key the ring off their own state selector
1239
+ * (`&:focus + .checkbox`). Everything else spreads `focusRing()` instead.
1240
+ *
1241
+ * Themable: a theme that defines `focusRing` **border** and/or **shadow**
1242
+ * primitives replaces the default 2px outline with that border (drawn as
1243
+ * an outline hugging the control) plus the ring shadow — one focus
1244
+ * language for every control, from text fields to checkboxes to calendar
1245
+ * days. A `focusRing` **thickness** primitive sets the outline offset
1246
+ * (default 0 when themed, 2px for the classic outline; negative values
1247
+ * overlay the control's own border, reading as a border-color change).
1248
+ * Without those primitives the classic outline renders, in the given
1249
+ * color or `currentColor`.
1250
+ */
1251
+ focusRingStyle = (color, gap) => {
1252
+ const border = this.borders()['focusRing'];
1253
+ const shadow = this.shadows()['focusRing'];
1254
+ // An explicit per-call gap wins; else the theme's `focusRing` thickness
1255
+ // primitive; else the branch default (hugging when themed, classic 2px).
1256
+ const offset = gap ?? this.thicknesses()['focusRing'];
1257
+ if (border || shadow) {
1258
+ return {
1259
+ outline: border ?? 'none',
1260
+ outlineOffset: offset ?? 0,
1261
+ ...(shadow ? { boxShadow: shadow } : {}),
1262
+ };
1263
+ }
1264
+ return { outline: `2px solid ${color ?? 'currentColor'}`, outlineOffset: offset ?? '2px' };
1265
+ };
961
1266
  /**
962
1267
  * Shared keyboard-focus indicator (WCAG 2.4.7). Spread into a component's
963
1268
  * Emotion styles: `...this.theme.focusRing()` or `focusRing('primary')`.
1269
+ * See `focusRingStyle` for how themes restyle it.
964
1270
  */
965
1271
  focusRing = (token) => ({
966
- '&:focus-visible': {
967
- outline: `2px solid ${token ? this.colors()[token] : 'currentColor'}`,
968
- outlineOffset: '2px',
969
- },
1272
+ '&:focus-visible': this.focusRingStyle(token ? this.colors()[token] : undefined),
970
1273
  });
971
1274
  typeface = (typeface) => {
972
1275
  const typefaces = this.typeFaces();
@@ -2139,73 +2442,705 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2139
2442
  * This file exports all public-facing elements of the button-group component.
2140
2443
  */
2141
2444
 
2142
- class UniCardContentComponent extends BaseComponent {
2143
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2144
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: UniCardContentComponent, isStandalone: true, selector: "uni-card-content", providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], usesInheritance: true, ngImport: i0, template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2445
+ class BodyRenderDirective {
2446
+ el = inject(ElementRef);
2447
+ renderer = inject(Renderer2);
2448
+ ngOnInit() {
2449
+ const element = this.el.nativeElement;
2450
+ this.renderer.appendChild(document.body, element);
2451
+ }
2452
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2453
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: BodyRenderDirective, isStandalone: true, selector: "[uniBodyRender]", ngImport: i0 });
2145
2454
  }
2146
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, decorators: [{
2147
- type: Component,
2148
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-card-content', imports: [], providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n" }]
2455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, decorators: [{
2456
+ type: Directive,
2457
+ args: [{
2458
+ selector: '[uniBodyRender]',
2459
+ }]
2149
2460
  }] });
2150
2461
 
2151
- /**
2152
- * Typeface defaults by host element, so semantic HTML and the type scale
2153
- * reinforce each other: `<h1 uni-text>` is already headline-large.
2154
- */
2155
- const TagTypefaces = {
2156
- h1: 'headline-large',
2157
- h2: 'headline-medium',
2158
- h3: 'headline-small',
2159
- h4: 'title-large',
2160
- h5: 'title-medium',
2161
- h6: 'title-small',
2162
- p: 'body-1-long',
2163
- small: 'caption',
2164
- figcaption: 'caption',
2165
- blockquote: 'quote',
2166
- label: 'label',
2167
- };
2168
- /**
2169
- * The typography primitive, applied as an attribute to any element so
2170
- * semantics stay yours. The attribute value is the typeface:
2171
- * `<h1 uni-text="display-small">`, `<span uni-text="caption">`, dynamic via
2172
- * `[uni-text]="role()"`. With no value, the typeface is inferred from the
2173
- * host element (h1 → headline-large, p → body-1-long, …), falling back to
2174
- * `title-small`; the `typeface` input remains as an explicit override.
2175
- */
2176
- class UniTextComponent {
2462
+ class DragAndDropDirective {
2463
+ // TODO(v4): rename to fileDropped renaming is breaking
2464
+ // eslint-disable-next-line @angular-eslint/no-output-on-prefix
2465
+ onFileDropped = output();
2466
+ workspaceOpacity = signal('1', ...(ngDevMode ? [{ debugName: "workspaceOpacity" }] : /* istanbul ignore next */ []));
2467
+ // Dragover listener, when files are dragged over our host element
2468
+ onDragOver(event) {
2469
+ event.preventDefault();
2470
+ event.stopPropagation();
2471
+ this.workspaceOpacity.set('0.5');
2472
+ }
2473
+ // Dragleave listener, when files are dragged away from our host element
2474
+ onDragLeave(event) {
2475
+ event.preventDefault();
2476
+ event.stopPropagation();
2477
+ this.workspaceOpacity.set('1');
2478
+ }
2479
+ // Drop listener, when files are dropped on our host element
2480
+ onDrop(event) {
2481
+ event.preventDefault();
2482
+ event.stopPropagation();
2483
+ this.workspaceOpacity.set('1');
2484
+ const files = event.dataTransfer?.files;
2485
+ if (files && files.length > 0) {
2486
+ this.onFileDropped.emit(files);
2487
+ }
2488
+ }
2489
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2490
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: DragAndDropDirective, isStandalone: true, selector: "[uni-drag-n-drop], [dragAndDrop]", outputs: { onFileDropped: "onFileDropped" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "style.opacity": "workspaceOpacity()" } }, ngImport: i0 });
2491
+ }
2492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, decorators: [{
2493
+ type: Directive,
2494
+ args: [{
2495
+ selector: '[uni-drag-n-drop], [dragAndDrop]',
2496
+ host: {
2497
+ '[style.opacity]': 'workspaceOpacity()',
2498
+ '(dragover)': 'onDragOver($event)',
2499
+ '(dragleave)': 'onDragLeave($event)',
2500
+ '(drop)': 'onDrop($event)',
2501
+ },
2502
+ }]
2503
+ }], propDecorators: { onFileDropped: [{ type: i0.Output, args: ["onFileDropped"] }] } });
2504
+
2505
+ class UniIconButtonComponent {
2177
2506
  theme = inject(ThemeService);
2178
- tag = inject(ElementRef).nativeElement.tagName.toLowerCase();
2179
- /** Typeface via the selector attribute: `uni-text="headline-large"`. */
2180
- uniText = input('', { ...(ngDevMode ? { debugName: "uniText" } : /* istanbul ignore next */ {}), alias: 'uni-text' });
2181
- /** Explicit typeface; the attribute value wins when both are set. */
2182
- typeface = input(undefined, ...(ngDevMode ? [{ debugName: "typeface" }] : /* istanbul ignore next */ []));
2183
- color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
2184
- display = input(...(ngDevMode ? [undefined, { debugName: "display" }] : /* istanbul ignore next */ []));
2185
- align = input(...(ngDevMode ? [undefined, { debugName: "align" }] : /* istanbul ignore next */ []));
2186
- nowrap = input(...(ngDevMode ? [undefined, { debugName: "nowrap" }] : /* istanbul ignore next */ []));
2187
- maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
2188
- ellipsis = input(false, ...(ngDevMode ? [{ debugName: "ellipsis" }] : /* istanbul ignore next */ []));
2189
- resolvedTypeface = computed(() => this.uniText() || this.typeface() || TagTypefaces[this.tag] || 'title-small', ...(ngDevMode ? [{ debugName: "resolvedTypeface" }] : /* istanbul ignore next */ []));
2507
+ config = this.theme.component('iconButton');
2508
+ /**
2509
+ * Accessible name for the button. Alternative to projecting text content
2510
+ * (`<button icon-button>Close</button>`); one of the two is required for
2511
+ * an icon-only button to be announced correctly.
2512
+ */
2513
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2514
+ iconName = input(...(ngDevMode ? [undefined, { debugName: "iconName" }] : /* istanbul ignore next */ []));
2515
+ symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
2516
+ variant = input('ghost', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
2517
+ size = input('lg', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2518
+ disable = input(...(ngDevMode ? [undefined, { debugName: "disable" }] : /* istanbul ignore next */ []));
2519
+ loading = input(...(ngDevMode ? [undefined, { debugName: "loading" }] : /* istanbul ignore next */ []));
2520
+ opticalSize = input(24, ...(ngDevMode ? [{ debugName: "opticalSize" }] : /* istanbul ignore next */ []));
2521
+ srOnlyClass = css(visuallyHidden);
2522
+ /**
2523
+ * Sizes `iconName` from the size token's `fontSize`, the same value that sizes
2524
+ * a `symbolName` ligature — so the two paths render the same glyph size and
2525
+ * `symbolName` → `iconName` is a like-for-like swap. Without it a masked icon
2526
+ * fills the whole button box, since the base size tokens carry no padding.
2527
+ * Themes that do use padding (Carbon) set a matching `fontSize`, so they land
2528
+ * on the same glyph either way.
2529
+ */
2530
+ glyphSize = computed(() => {
2531
+ // Size tokens are Emotion style objects, so `fontSize` is typed wider than
2532
+ // a CSS length; anything exotic falls back to filling the button as before.
2533
+ const fontSize = this.config().sizes?.[this.size()]?.fontSize;
2534
+ return typeof fontSize === 'number' || typeof fontSize === 'string' ? fontSize : undefined;
2535
+ }, ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
2190
2536
  className = computed(() => {
2537
+ const { sizes, variants } = this.config();
2538
+ const sizeConfig = sizes && sizes[this.size()];
2539
+ const colorConfig = variants && variants[this.variant()];
2191
2540
  return css([
2192
2541
  {
2193
- ...this.theme.typeface(this.resolvedTypeface()),
2194
- ...this.theme.color(this.color()),
2195
- display: this.display(),
2542
+ position: 'relative',
2543
+ overflow: 'hidden',
2544
+ outline: 0,
2545
+ border: 0,
2546
+ cursor: 'pointer',
2547
+ transition: 'all 0.28s ease',
2548
+ // Token-driven radius (`max` = circle) with the legacy 999 fallback
2549
+ // for hand-authored themes that predate iconButton options.
2550
+ ...(this.theme.radius(this.config().options?.borderRadius) ?? { borderRadius: 999 }),
2551
+ // Block-level, but centring: the size tokens make the box bigger than
2552
+ // the glyph (an `sm` button is 22px around an 18px icon), so a plain
2553
+ // `display: block` parks the glyph in the top-left corner. Flex is
2554
+ // still block-level, so nothing about the button's own layout changes.
2555
+ // The accessible-name span is absolutely positioned and so stays out
2556
+ // of the flex flow.
2557
+ display: 'flex',
2558
+ alignItems: 'center',
2559
+ justifyContent: 'center',
2560
+ '&:disabled': {
2561
+ cursor: 'not-allowed !important',
2562
+ },
2563
+ '& symbol': {
2564
+ fontSize: 'inherit',
2565
+ lineHeight: 'inherit',
2566
+ },
2196
2567
  },
2197
- this.align() && {
2198
- textAlign: this.align(),
2568
+ sizeConfig && {
2569
+ ...sizeConfig,
2199
2570
  },
2200
- this.nowrap() && {
2201
- whiteSpace: 'nowrap',
2571
+ colorConfig && {
2572
+ ...colorConfig,
2202
2573
  },
2203
- this.maxWidth() && {
2204
- maxWidth: this.maxWidth(),
2205
- overflow: 'hidden',
2206
- whiteSpace: 'nowrap',
2207
- textOverflow: 'ellipsis',
2208
- display: 'inline-block',
2574
+ this.symbolName() &&
2575
+ !this.loading() && {
2576
+ padding: 0,
2577
+ },
2578
+ this.variant() !== 'ghost' && {
2579
+ '&:hover': {
2580
+ ...this.theme.boxShadow('raised'),
2581
+ },
2582
+ },
2583
+ this.variant() === 'ghost' && {
2584
+ '&:hover': {
2585
+ backgroundColor: 'rgba(0,0,0,0.1)',
2586
+ },
2587
+ },
2588
+ !this.loading() && {
2589
+ '&:disabled': {
2590
+ ...this.config().variants?.disabled,
2591
+ },
2592
+ },
2593
+ ]);
2594
+ }, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2595
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2596
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniIconButtonComponent, isStandalone: true, selector: "button[uni-icon-button], button[icon-button]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, iconName: { classPropertyName: "iconName", publicName: "iconName", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disable: { classPropertyName: "disable", publicName: "disable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, opticalSize: { classPropertyName: "opticalSize", publicName: "opticalSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.disabled": "disable() || loading() || null", "attr.aria-busy": "loading() ? 'true' : null", "attr.aria-label": "ariaLabel() || null", "class": "className()" } }, hostDirectives: [{ directive: RippleDirective }], ngImport: i0, template: `
2597
+ @if (loading()) {
2598
+ <uni-icon name="spinner" />
2599
+ } @else if (symbolName()) {
2600
+ <uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
2601
+ } @else if (iconName()) {
2602
+ <uni-icon [name]="iconName()!" [size]="glyphSize()" />
2603
+ }
2604
+ <!-- Projected text is the button's accessible name (visually hidden) -->
2605
+ <span [class]="srOnlyClass"><ng-content /></span>
2606
+ `, isInline: true, dependencies: [{ kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2607
+ }
2608
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, decorators: [{
2609
+ type: Component,
2610
+ args: [{
2611
+ selector: 'button[uni-icon-button], button[icon-button]',
2612
+ imports: [UniSymbolComponent, UniIconComponent],
2613
+ template: `
2614
+ @if (loading()) {
2615
+ <uni-icon name="spinner" />
2616
+ } @else if (symbolName()) {
2617
+ <uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
2618
+ } @else if (iconName()) {
2619
+ <uni-icon [name]="iconName()!" [size]="glyphSize()" />
2620
+ }
2621
+ <!-- Projected text is the button's accessible name (visually hidden) -->
2622
+ <span [class]="srOnlyClass"><ng-content /></span>
2623
+ `,
2624
+ changeDetection: ChangeDetectionStrategy.OnPush,
2625
+ host: {
2626
+ '[attr.disabled]': 'disable() || loading() || null',
2627
+ '[attr.aria-busy]': "loading() ? 'true' : null",
2628
+ '[attr.aria-label]': 'ariaLabel() || null',
2629
+ '[class]': 'className()',
2630
+ },
2631
+ hostDirectives: [{ directive: RippleDirective }],
2632
+ }]
2633
+ }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], iconName: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconName", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disable: [{ type: i0.Input, args: [{ isSignal: true, alias: "disable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], opticalSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "opticalSize", required: false }] }] } });
2634
+
2635
+ /**
2636
+ * Inline month calendar: single-date or start–end range selection, day
2637
+ * markers (availability dots), min/max fences and disabled dates. One tab
2638
+ * stop with a roving-tabindex `role="grid"`; every day is a real button
2639
+ * named with its full date. Values are plain ISO strings (`'YYYY-MM-DD'`),
2640
+ * never `Date` objects, so bindings are timezone-free and serializable.
2641
+ * Selection and today colours come from the theme's `primary` role pair;
2642
+ * geometry and glyphs come from the `calendar` theme entry.
2643
+ */
2644
+ class UniCalendarComponent extends BaseComponent {
2645
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
2646
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
2647
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
2648
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2649
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
2650
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
2651
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
2652
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
2653
+ // --- Configuration -------------------------------------------------------
2654
+ /** `'single'` reads/writes a `UniDate`; `'range'` a `UniDateRange`. */
2655
+ mode = input('single', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
2656
+ /** Shown month, `'YYYY-MM'` — two-way, so an app can drive "jump to June". */
2657
+ month = model(...(ngDevMode ? [undefined, { debugName: "month" }] : /* istanbul ignore next */ []));
2658
+ /** Earliest selectable date (inclusive). */
2659
+ minDate = input(...(ngDevMode ? [undefined, { debugName: "minDate" }] : /* istanbul ignore next */ []));
2660
+ /** Latest selectable date (inclusive). */
2661
+ maxDate = input(...(ngDevMode ? [undefined, { debugName: "maxDate" }] : /* istanbul ignore next */ []));
2662
+ /** Blocked days: a list of dates, or a predicate. */
2663
+ disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
2664
+ /** Availability dots; `label` extends the day's accessible name. */
2665
+ markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
2666
+ /** BCP 47 tag; defaults to the document language, then the browser's. */
2667
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
2668
+ /** First day of week, 0 = Sunday; defaults from the locale's week info. */
2669
+ weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
2670
+ /** Names the grid when it stands alone (otherwise the heading names it). */
2671
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2672
+ /** Day geometry token; `sm`/`md`/`lg` map to the theme's `calendar` sizes. */
2673
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2674
+ // --- Events (value/month changes flow through the models) -----------------
2675
+ /** Each committed day, including both ends of a range. */
2676
+ selected = output();
2677
+ host = inject(ElementRef);
2678
+ headingId = uniqueId('uni-calendar-heading');
2679
+ srOnly = css(visuallyHidden);
2680
+ /** Pending range start — set by the first commit, cleared by the second. */
2681
+ pendingStart = signal(null, ...(ngDevMode ? [{ debugName: "pendingStart" }] : /* istanbul ignore next */ []));
2682
+ /** Hover/focus candidate painting the preview band while a range is pending. */
2683
+ previewDate = signal(null, ...(ngDevMode ? [{ debugName: "previewDate" }] : /* istanbul ignore next */ []));
2684
+ /** Live-region text; selections are otherwise silent for a screen reader. */
2685
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
2686
+ resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
2687
+ resolvedWeekStart = computed(() => this.weekStart() ?? localeWeekStart(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedWeekStart" }] : /* istanbul ignore next */ []));
2688
+ /** The month on screen: the `month` model, else the value's month, else today's. */
2689
+ viewMonth = computed(() => this.month() ?? monthOf(this.anchorDate() ?? todayIso()), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
2690
+ anchorDate = computed(() => {
2691
+ const value = this.value();
2692
+ return this.mode() === 'range'
2693
+ ? value?.start
2694
+ : value;
2695
+ }, ...(ngDevMode ? [{ debugName: "anchorDate" }] : /* istanbul ignore next */ []));
2696
+ /**
2697
+ * The roving-tabindex day. Re-derives when the view or value changes
2698
+ * (selected day in view → today in view → first enabled day); keyboard
2699
+ * navigation writes it directly.
2700
+ */
2701
+ focusedDate = linkedSignal(() => this.pickFocus(), ...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
2702
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
2703
+ heading = computed(() => formatMonthHeading(this.viewMonth(), this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
2704
+ weekdays = computed(() => weekdayNames(this.resolvedLocale(), this.resolvedWeekStart(), this.componentOptions().weekdayFormat ?? 'short'), ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
2705
+ disabledDateSet = computed(() => {
2706
+ const dates = this.disabledDates();
2707
+ return Array.isArray(dates) ? new Set(dates) : null;
2708
+ }, ...(ngDevMode ? [{ debugName: "disabledDateSet" }] : /* istanbul ignore next */ []));
2709
+ markersByDate = computed(() => {
2710
+ const map = new Map();
2711
+ for (const marker of this.markers()) {
2712
+ const existing = map.get(marker.date);
2713
+ if (existing)
2714
+ existing.push(marker);
2715
+ else
2716
+ map.set(marker.date, [marker]);
2717
+ }
2718
+ return map;
2719
+ }, ...(ngDevMode ? [{ debugName: "markersByDate" }] : /* istanbul ignore next */ []));
2720
+ /** The committed range, or the pending preview band. */
2721
+ rangeBand = computed(() => {
2722
+ if (this.mode() !== 'range')
2723
+ return null;
2724
+ const pending = this.pendingStart();
2725
+ const preview = this.previewDate();
2726
+ if (pending && preview) {
2727
+ const [start, end] = preview < pending ? [preview, pending] : [pending, preview];
2728
+ return { start, end, preview: true };
2729
+ }
2730
+ if (pending)
2731
+ return { start: pending, end: pending, preview: true };
2732
+ const value = this.value();
2733
+ if (value?.start && value?.end)
2734
+ return { start: value.start, end: value.end, preview: false };
2735
+ return null;
2736
+ }, ...(ngDevMode ? [{ debugName: "rangeBand" }] : /* istanbul ignore next */ []));
2737
+ isDayDisabled(date) {
2738
+ const min = this.minDate();
2739
+ const max = this.maxDate();
2740
+ if ((min && date < min) || (max && date > max))
2741
+ return true;
2742
+ const dates = this.disabledDates();
2743
+ if (!dates)
2744
+ return false;
2745
+ const set = this.disabledDateSet();
2746
+ return set ? set.has(date) : dates(date);
2747
+ }
2748
+ isSelected(date) {
2749
+ if (this.mode() === 'single')
2750
+ return this.value() === date;
2751
+ const pending = this.pendingStart();
2752
+ if (pending)
2753
+ return pending === date;
2754
+ const value = this.value();
2755
+ return value?.start === date || value?.end === date;
2756
+ }
2757
+ /** The full render model: one precomputed cell per grid position. */
2758
+ gridWeeks = computed(() => {
2759
+ const locale = this.resolvedLocale();
2760
+ const band = this.rangeBand();
2761
+ const today = todayIso();
2762
+ const focused = this.focusedDate();
2763
+ const markers = this.markersByDate();
2764
+ const cellBase = this.cellClass();
2765
+ const dayBase = this.dayClass();
2766
+ const todayClass = this.todayClass();
2767
+ return buildMonthGrid(this.viewMonth(), this.resolvedWeekStart()).map((week) => week.map((cell) => {
2768
+ const date = cell.date;
2769
+ const selected = !cell.outside && this.isSelected(date);
2770
+ const inBand = !cell.outside && !!band && date >= band.start && date <= band.end;
2771
+ const dayMarkers = cell.outside ? [] : (markers.get(date) ?? []).slice(0, 3);
2772
+ const markerLabel = dayMarkers
2773
+ .filter((marker) => marker.label)
2774
+ .map((marker) => marker.label)
2775
+ .join(', ');
2776
+ const isToday = date === today;
2777
+ return {
2778
+ date,
2779
+ day: Number(date.slice(8, 10)),
2780
+ outside: cell.outside,
2781
+ disabled: this.isDayDisabled(date),
2782
+ today: isToday,
2783
+ selected,
2784
+ inBand,
2785
+ tabIndex: date === focused && !cell.outside ? 0 : -1,
2786
+ ariaLabel: formatDate(date, locale, { dateStyle: 'full' }) + (markerLabel ? `, ${markerLabel}` : ''),
2787
+ markers: dayMarkers,
2788
+ cellClass: [
2789
+ cellBase,
2790
+ inBand && (band.preview ? this.previewClass() : this.inRangeClass()),
2791
+ inBand && date === band.start && this.bandStartClass(),
2792
+ inBand && date === band.end && this.bandEndClass(),
2793
+ ]
2794
+ .filter(Boolean)
2795
+ .join(' '),
2796
+ dayClass: [dayBase, isToday && todayClass, selected && this.selectedClass()]
2797
+ .filter(Boolean)
2798
+ .join(' '),
2799
+ };
2800
+ }));
2801
+ }, ...(ngDevMode ? [{ debugName: "gridWeeks" }] : /* istanbul ignore next */ []));
2802
+ // --- Selection -------------------------------------------------------------
2803
+ select(date) {
2804
+ if (this.disabled() || this.isDayDisabled(date))
2805
+ return;
2806
+ this.setViewMonth(monthOf(date));
2807
+ this.focusedDate.set(date);
2808
+ const locale = this.resolvedLocale();
2809
+ const full = (d) => formatDate(d, locale, { dateStyle: 'full' });
2810
+ if (this.mode() === 'single') {
2811
+ this.value.set(date);
2812
+ this.announce(`${full(date)} selected.`);
2813
+ }
2814
+ else if (!this.pendingStart()) {
2815
+ this.pendingStart.set(date);
2816
+ this.previewDate.set(date);
2817
+ this.announce(`Start date ${full(date)}. Choose an end date.`);
2818
+ }
2819
+ else {
2820
+ let [start, end] = [this.pendingStart(), date];
2821
+ if (end < start)
2822
+ [start, end] = [end, start]; // backwards commit swaps
2823
+ this.pendingStart.set(null);
2824
+ this.previewDate.set(null);
2825
+ this.value.set({ start, end });
2826
+ const days = inclusiveDayCount(start, end);
2827
+ this.announce(`Range selected, ${full(start)} to ${full(end)}. ${days} ${days === 1 ? 'day' : 'days'}.`);
2828
+ }
2829
+ this.selected.emit(date);
2830
+ }
2831
+ cancelPending() {
2832
+ this.pendingStart.set(null);
2833
+ this.previewDate.set(null);
2834
+ this.announce('Range selection cancelled.');
2835
+ }
2836
+ // --- Navigation ------------------------------------------------------------
2837
+ onNav(direction) {
2838
+ this.setViewMonth(monthOf(addMonths(`${this.viewMonth()}-01`, direction)));
2839
+ }
2840
+ setViewMonth(month) {
2841
+ if (this.month() !== month)
2842
+ this.month.set(month);
2843
+ }
2844
+ /**
2845
+ * Move the roving focus. A landing on a disabled day keeps going in the
2846
+ * same direction until an enabled day; the min/max fence stops the caret,
2847
+ * it never wraps. Month edges never block — the grid follows.
2848
+ */
2849
+ moveFocus(target, direction) {
2850
+ const min = this.minDate();
2851
+ const max = this.maxDate();
2852
+ if ((min && target < min) || (max && target > max))
2853
+ return;
2854
+ let date = target;
2855
+ let guard = 0;
2856
+ while (this.isDayDisabled(date)) {
2857
+ date = addDays(date, direction);
2858
+ if ((min && date < min) || (max && date > max) || ++guard > 500)
2859
+ return;
2860
+ }
2861
+ this.setViewMonth(monthOf(date));
2862
+ this.focusedDate.set(date);
2863
+ if (this.pendingStart())
2864
+ this.previewDate.set(date);
2865
+ this.focusDay(date);
2866
+ }
2867
+ onGridKeydown(event) {
2868
+ const focused = this.focusedDate();
2869
+ const week = (dayOfWeek(focused) - this.resolvedWeekStart() + 7) % 7;
2870
+ const handlers = {
2871
+ ArrowLeft: () => this.moveFocus(addDays(focused, -1), -1),
2872
+ ArrowRight: () => this.moveFocus(addDays(focused, 1), 1),
2873
+ ArrowUp: () => this.moveFocus(addDays(focused, -7), -1),
2874
+ ArrowDown: () => this.moveFocus(addDays(focused, 7), 1),
2875
+ Home: () => this.moveFocus(addDays(focused, -week), 1),
2876
+ End: () => this.moveFocus(addDays(focused, 6 - week), -1),
2877
+ PageUp: () => this.moveFocus(addMonths(focused, event.shiftKey ? -12 : -1), -1),
2878
+ PageDown: () => this.moveFocus(addMonths(focused, event.shiftKey ? 12 : 1), 1),
2879
+ Enter: () => this.select(focused),
2880
+ ' ': () => this.select(focused),
2881
+ };
2882
+ if (event.key === 'Escape') {
2883
+ // Only a pending range consumes Escape; otherwise it bubbles so a
2884
+ // hosting popover can light-dismiss.
2885
+ if (this.pendingStart()) {
2886
+ event.preventDefault();
2887
+ event.stopPropagation();
2888
+ this.cancelPending();
2889
+ }
2890
+ return;
2891
+ }
2892
+ const handler = handlers[event.key];
2893
+ if (handler) {
2894
+ event.preventDefault();
2895
+ handler();
2896
+ }
2897
+ }
2898
+ onDayHover(date) {
2899
+ if (this.pendingStart() && !this.isDayDisabled(date))
2900
+ this.previewDate.set(date);
2901
+ }
2902
+ /** Focus the roving day — used by popup hosts when the calendar opens. */
2903
+ focusActiveDay() {
2904
+ this.focusDay(this.focusedDate());
2905
+ }
2906
+ onHostFocusOut(event) {
2907
+ const next = event.relatedTarget;
2908
+ if (!next || !this.host.nativeElement.contains(next))
2909
+ this.touched.set(true);
2910
+ }
2911
+ focusDay(date) {
2912
+ queueMicrotask(() => this.host.nativeElement.querySelector(`[data-date="${date}"]`)?.focus());
2913
+ }
2914
+ pickFocus() {
2915
+ const view = this.viewMonth();
2916
+ const anchor = this.anchorDate();
2917
+ if (anchor && monthOf(anchor) === view && !this.isDayDisabled(anchor))
2918
+ return anchor;
2919
+ const today = todayIso();
2920
+ if (monthOf(today) === view && !this.isDayDisabled(today))
2921
+ return today;
2922
+ return this.firstEnabledInView(view);
2923
+ }
2924
+ firstEnabledInView(view) {
2925
+ const first = `${view}-01`;
2926
+ let date = first;
2927
+ for (let i = 0; i < 31 && monthOf(date) === view; i++) {
2928
+ if (!this.isDayDisabled(date))
2929
+ return date;
2930
+ date = addDays(date, 1);
2931
+ }
2932
+ return first;
2933
+ }
2934
+ announce(message) {
2935
+ // Re-announce identical text by breaking the string equality.
2936
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
2937
+ }
2938
+ // --- Styling ---------------------------------------------------------------
2939
+ daySize = computed(() => (this.componentTheme().sizes?.[this.size()] ?? {}), ...(ngDevMode ? [{ debugName: "daySize" }] : /* istanbul ignore next */ []));
2940
+ className = computed(() => css([(this.componentTheme().fixed ?? { display: 'inline-block' })]), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2941
+ navClass = computed(() => css({
2942
+ display: 'flex',
2943
+ alignItems: 'center',
2944
+ justifyContent: 'space-between',
2945
+ ...this.theme.gap('xs'),
2946
+ marginBottom: 4,
2947
+ }), ...(ngDevMode ? [{ debugName: "navClass" }] : /* istanbul ignore next */ []));
2948
+ headingClass = computed(() => css({ flex: 1, textAlign: 'center', ...this.theme.typeface('title-small') }), ...(ngDevMode ? [{ debugName: "headingClass" }] : /* istanbul ignore next */ []));
2949
+ gridClass = computed(() => css({ display: 'grid', ...this.theme.gap(this.componentOptions().gap ?? 'xxs') }), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
2950
+ rowClass = computed(() => css({
2951
+ display: 'grid',
2952
+ gridTemplateColumns: 'repeat(7, 1fr)',
2953
+ justifyItems: 'center',
2954
+ ...this.theme.gap(this.componentOptions().gap ?? 'xxs'),
2955
+ }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
2956
+ weekdayClass = computed(() => css({
2957
+ display: 'flex',
2958
+ alignItems: 'center',
2959
+ justifyContent: 'center',
2960
+ ...this.daySize(),
2961
+ height: 'auto',
2962
+ ...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
2963
+ ...this.theme.color('on-background-variant'),
2964
+ '& abbr': { textDecoration: 'none' },
2965
+ }), ...(ngDevMode ? [{ debugName: "weekdayClass" }] : /* istanbul ignore next */ []));
2966
+ /** The gridcell — range bands paint here so they read as one bar. */
2967
+ cellClass = computed(() => css({ position: 'relative', display: 'flex' }), ...(ngDevMode ? [{ debugName: "cellClass" }] : /* istanbul ignore next */ []));
2968
+ inRangeClass = computed(() => css({ ...this.theme.backgroundColor('primary-container'), borderRadius: 0 }), ...(ngDevMode ? [{ debugName: "inRangeClass" }] : /* istanbul ignore next */ []));
2969
+ previewClass = computed(() => {
2970
+ const colors = this.theme.colorPalette();
2971
+ return css({
2972
+ backgroundColor: `color-mix(in srgb, ${colors['primary-container']} 55%, transparent)`,
2973
+ outline: `1px dashed ${colors['primary']}`,
2974
+ outlineOffset: -1,
2975
+ });
2976
+ }, ...(ngDevMode ? [{ debugName: "previewClass" }] : /* istanbul ignore next */ []));
2977
+ bandStartClass = computed(() => css({ ...this.theme.getRadiusLeft(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandStartClass" }] : /* istanbul ignore next */ []));
2978
+ bandEndClass = computed(() => css({ ...this.theme.getRadiusRight(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandEndClass" }] : /* istanbul ignore next */ []));
2979
+ dayClass = computed(() => {
2980
+ const options = this.componentOptions();
2981
+ const colors = this.theme.colorPalette();
2982
+ return css({
2983
+ position: 'relative',
2984
+ display: 'flex',
2985
+ alignItems: 'center',
2986
+ justifyContent: 'center',
2987
+ border: 0,
2988
+ padding: 0,
2989
+ background: 'transparent',
2990
+ color: 'inherit',
2991
+ cursor: 'pointer',
2992
+ ...this.theme.typeface(options.typeface ?? 'label'),
2993
+ ...this.daySize(),
2994
+ ...this.theme.radius(options.dayBorderRadius ?? 'max'),
2995
+ '&:hover:not(:disabled)': { ...this.theme.colorPair('primary-container') },
2996
+ ...this.theme.focusRing(),
2997
+ '&:disabled': {
2998
+ color: colors['on-disabled'],
2999
+ cursor: 'default',
3000
+ pointerEvents: 'none',
3001
+ },
3002
+ });
3003
+ }, ...(ngDevMode ? [{ debugName: "dayClass" }] : /* istanbul ignore next */ []));
3004
+ todayClass = computed(() => {
3005
+ const colors = this.theme.colorPalette();
3006
+ // Outline (or dot), never fill — so today and selected can coincide and
3007
+ // both stay legible (WCAG 1.4.1: no state carried by colour alone).
3008
+ return (this.componentOptions().todayStyle ?? 'outline') === 'outline'
3009
+ ? css({ boxShadow: `inset 0 0 0 1.5px ${colors['primary']}` })
3010
+ : css({
3011
+ '&::after': {
3012
+ content: '""',
3013
+ position: 'absolute',
3014
+ bottom: 3,
3015
+ left: '50%',
3016
+ transform: 'translateX(-50%)',
3017
+ width: 4,
3018
+ height: 4,
3019
+ borderRadius: 999,
3020
+ backgroundColor: colors['primary'],
3021
+ },
3022
+ });
3023
+ }, ...(ngDevMode ? [{ debugName: "todayClass" }] : /* istanbul ignore next */ []));
3024
+ selectedClass = computed(() => {
3025
+ const colors = this.theme.colorPalette();
3026
+ return css({
3027
+ ...this.theme.colorPair('primary'),
3028
+ // A marker dot survives selection by switching to the on-colour.
3029
+ '& [data-dot]': { backgroundColor: colors['on-primary'] },
3030
+ });
3031
+ }, ...(ngDevMode ? [{ debugName: "selectedClass" }] : /* istanbul ignore next */ []));
3032
+ outsideDayClass = computed(() => {
3033
+ const colors = this.theme.colorPalette();
3034
+ return css({
3035
+ display: 'flex',
3036
+ alignItems: 'center',
3037
+ justifyContent: 'center',
3038
+ ...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
3039
+ ...this.daySize(),
3040
+ color: colors['on-disabled'],
3041
+ });
3042
+ }, ...(ngDevMode ? [{ debugName: "outsideDayClass" }] : /* istanbul ignore next */ []));
3043
+ dotsClass = computed(() => css({
3044
+ position: 'absolute',
3045
+ bottom: 2,
3046
+ left: 0,
3047
+ right: 0,
3048
+ display: 'flex',
3049
+ justifyContent: 'center',
3050
+ gap: 2,
3051
+ pointerEvents: 'none',
3052
+ }), ...(ngDevMode ? [{ debugName: "dotsClass" }] : /* istanbul ignore next */ []));
3053
+ /** One dot class per marker variant present, resolved from the palette. */
3054
+ dotClasses = computed(() => {
3055
+ const colors = this.theme.colorPalette();
3056
+ const classes = new Map();
3057
+ for (const marker of this.markers()) {
3058
+ const variant = marker.variant ?? 'primary';
3059
+ if (!classes.has(variant)) {
3060
+ classes.set(variant, css({ width: 4, height: 4, borderRadius: 999, backgroundColor: colors[variant] }));
3061
+ }
3062
+ }
3063
+ return classes;
3064
+ }, ...(ngDevMode ? [{ debugName: "dotClasses" }] : /* istanbul ignore next */ []));
3065
+ dotClassFor(variant) {
3066
+ return this.dotClasses().get(variant ?? 'primary') ?? '';
3067
+ }
3068
+ showOutside = computed(() => this.componentOptions().showOutsideDays ?? false, ...(ngDevMode ? [{ debugName: "showOutside" }] : /* istanbul ignore next */ []));
3069
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3070
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniCalendarComponent, isStandalone: true, selector: "uni-calendar, Calendar", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, month: { classPropertyName: "month", publicName: "month", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", month: "monthChange", selected: "selected" }, host: { listeners: { "focusout": "onHostFocusOut($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'calendar' }], usesInheritance: true, ngImport: i0, template: "<div [class]=\"navClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navPrevSymbol ?? 'chevron_left'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(-1)\"\n >\n Previous month\n </button>\n <!-- aria-live so PageUp/Down narrates the new month without refocusing. -->\n <div [class]=\"headingClass()\" [id]=\"headingId\" aria-live=\"polite\">{{ heading() }}</div>\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navNextSymbol ?? 'chevron_right'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(1)\"\n >\n Next month\n </button>\n</div>\n\n<!-- One tab stop: the roving tabindex lives on the day buttons, and the grid\n itself only relays their bubbling keys (same delegation as tag-input). -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div\n role=\"grid\"\n [class]=\"gridClass()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : headingId\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div role=\"row\" [class]=\"rowClass()\">\n @for (weekday of weekdays(); track weekday.full) {\n <div role=\"columnheader\" [class]=\"weekdayClass()\">\n <abbr [attr.title]=\"weekday.full\">{{ weekday.label }}</abbr>\n </div>\n }\n </div>\n\n @for (week of gridWeeks(); track week[0].date) {\n <div role=\"row\" [class]=\"rowClass()\">\n @for (cell of week; track cell.date) {\n @if (cell.outside) {\n <!-- Outside days: hidden placeholders by default (geometry kept),\n muted and non-interactive under the showOutsideDays option. -->\n <div role=\"gridcell\" aria-hidden=\"true\" [class]=\"cell.cellClass\">\n @if (showOutside()) {\n <span [class]=\"outsideDayClass()\">{{ cell.day }}</span>\n }\n </div>\n } @else {\n <div\n role=\"gridcell\"\n [class]=\"cell.cellClass\"\n [attr.aria-selected]=\"cell.selected || cell.inBand ? true : null\"\n >\n <button\n type=\"button\"\n [class]=\"cell.dayClass\"\n [attr.data-date]=\"cell.date\"\n [tabindex]=\"cell.tabIndex\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n [disabled]=\"cell.disabled || disabled()\"\n (click)=\"select(cell.date)\"\n (mouseenter)=\"onDayHover(cell.date)\"\n >\n {{ cell.day }}\n @if (cell.markers.length) {\n <span [class]=\"dotsClass()\">\n @for (marker of cell.markers; track $index) {\n <span data-dot [class]=\"dotClassFor(marker.variant)\"></span>\n }\n </span>\n }\n </button>\n </div>\n }\n }\n </div>\n }\n</div>\n\n<!-- Selections and range progress are otherwise silent for a screen reader. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3071
+ }
3072
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, decorators: [{
3073
+ type: Component,
3074
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-calendar, Calendar', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'calendar' }], host: { '[class]': 'className()', '(focusout)': 'onHostFocusOut($event)' }, template: "<div [class]=\"navClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navPrevSymbol ?? 'chevron_left'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(-1)\"\n >\n Previous month\n </button>\n <!-- aria-live so PageUp/Down narrates the new month without refocusing. -->\n <div [class]=\"headingClass()\" [id]=\"headingId\" aria-live=\"polite\">{{ heading() }}</div>\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navNextSymbol ?? 'chevron_right'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(1)\"\n >\n Next month\n </button>\n</div>\n\n<!-- One tab stop: the roving tabindex lives on the day buttons, and the grid\n itself only relays their bubbling keys (same delegation as tag-input). -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div\n role=\"grid\"\n [class]=\"gridClass()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : headingId\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div role=\"row\" [class]=\"rowClass()\">\n @for (weekday of weekdays(); track weekday.full) {\n <div role=\"columnheader\" [class]=\"weekdayClass()\">\n <abbr [attr.title]=\"weekday.full\">{{ weekday.label }}</abbr>\n </div>\n }\n </div>\n\n @for (week of gridWeeks(); track week[0].date) {\n <div role=\"row\" [class]=\"rowClass()\">\n @for (cell of week; track cell.date) {\n @if (cell.outside) {\n <!-- Outside days: hidden placeholders by default (geometry kept),\n muted and non-interactive under the showOutsideDays option. -->\n <div role=\"gridcell\" aria-hidden=\"true\" [class]=\"cell.cellClass\">\n @if (showOutside()) {\n <span [class]=\"outsideDayClass()\">{{ cell.day }}</span>\n }\n </div>\n } @else {\n <div\n role=\"gridcell\"\n [class]=\"cell.cellClass\"\n [attr.aria-selected]=\"cell.selected || cell.inBand ? true : null\"\n >\n <button\n type=\"button\"\n [class]=\"cell.dayClass\"\n [attr.data-date]=\"cell.date\"\n [tabindex]=\"cell.tabIndex\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n [disabled]=\"cell.disabled || disabled()\"\n (click)=\"select(cell.date)\"\n (mouseenter)=\"onDayHover(cell.date)\"\n >\n {{ cell.day }}\n @if (cell.markers.length) {\n <span [class]=\"dotsClass()\">\n @for (marker of cell.markers; track $index) {\n <span data-dot [class]=\"dotClassFor(marker.variant)\"></span>\n }\n </span>\n }\n </button>\n </div>\n }\n }\n </div>\n }\n</div>\n\n<!-- Selections and range progress are otherwise silent for a screen reader. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n" }]
3075
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], month: [{ type: i0.Input, args: [{ isSignal: true, alias: "month", required: false }] }, { type: i0.Output, args: ["monthChange"] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
3076
+
3077
+ class UniCardContentComponent extends BaseComponent {
3078
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3079
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: UniCardContentComponent, isStandalone: true, selector: "uni-card-content", providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], usesInheritance: true, ngImport: i0, template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3080
+ }
3081
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, decorators: [{
3082
+ type: Component,
3083
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-card-content', imports: [], providers: [{ provide: COMPONENT_NAME, useValue: 'cardContent' }], template: "<div [style]=\"style()\">\n <ng-content></ng-content>\n</div>\n" }]
3084
+ }] });
3085
+
3086
+ /**
3087
+ * Typeface defaults by host element, so semantic HTML and the type scale
3088
+ * reinforce each other: `<h1 uni-text>` is already headline-large.
3089
+ */
3090
+ const TagTypefaces = {
3091
+ h1: 'headline-large',
3092
+ h2: 'headline-medium',
3093
+ h3: 'headline-small',
3094
+ h4: 'title-large',
3095
+ h5: 'title-medium',
3096
+ h6: 'title-small',
3097
+ p: 'body-1-long',
3098
+ small: 'caption',
3099
+ figcaption: 'caption',
3100
+ blockquote: 'quote',
3101
+ label: 'label',
3102
+ };
3103
+ /**
3104
+ * The typography primitive, applied as an attribute to any element so
3105
+ * semantics stay yours. The attribute value is the typeface:
3106
+ * `<h1 uni-text="display-small">`, `<span uni-text="caption">`, dynamic via
3107
+ * `[uni-text]="role()"`. With no value, the typeface is inferred from the
3108
+ * host element (h1 → headline-large, p → body-1-long, …), falling back to
3109
+ * `title-small`; the `typeface` input remains as an explicit override.
3110
+ */
3111
+ class UniTextComponent {
3112
+ theme = inject(ThemeService);
3113
+ tag = inject(ElementRef).nativeElement.tagName.toLowerCase();
3114
+ /** Typeface via the selector attribute: `uni-text="headline-large"`. */
3115
+ uniText = input('', { ...(ngDevMode ? { debugName: "uniText" } : /* istanbul ignore next */ {}), alias: 'uni-text' });
3116
+ /** Explicit typeface; the attribute value wins when both are set. */
3117
+ typeface = input(undefined, ...(ngDevMode ? [{ debugName: "typeface" }] : /* istanbul ignore next */ []));
3118
+ color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
3119
+ display = input(...(ngDevMode ? [undefined, { debugName: "display" }] : /* istanbul ignore next */ []));
3120
+ align = input(...(ngDevMode ? [undefined, { debugName: "align" }] : /* istanbul ignore next */ []));
3121
+ nowrap = input(...(ngDevMode ? [undefined, { debugName: "nowrap" }] : /* istanbul ignore next */ []));
3122
+ maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
3123
+ ellipsis = input(false, ...(ngDevMode ? [{ debugName: "ellipsis" }] : /* istanbul ignore next */ []));
3124
+ resolvedTypeface = computed(() => this.uniText() || this.typeface() || TagTypefaces[this.tag] || 'title-small', ...(ngDevMode ? [{ debugName: "resolvedTypeface" }] : /* istanbul ignore next */ []));
3125
+ className = computed(() => {
3126
+ return css([
3127
+ {
3128
+ ...this.theme.typeface(this.resolvedTypeface()),
3129
+ ...this.theme.color(this.color()),
3130
+ display: this.display(),
3131
+ },
3132
+ this.align() && {
3133
+ textAlign: this.align(),
3134
+ },
3135
+ this.nowrap() && {
3136
+ whiteSpace: 'nowrap',
3137
+ },
3138
+ this.maxWidth() && {
3139
+ maxWidth: this.maxWidth(),
3140
+ overflow: 'hidden',
3141
+ whiteSpace: 'nowrap',
3142
+ textOverflow: 'ellipsis',
3143
+ display: 'inline-block',
2209
3144
  },
2210
3145
  this.ellipsis() && {
2211
3146
  whiteSpace: 'nowrap',
@@ -2376,19 +3311,13 @@ class UniCheckboxComponent extends BaseComponent {
2376
3311
  '&:disabled + .checkbox': {
2377
3312
  cursor: 'not-allowed',
2378
3313
  },
3314
+ // The shared, themable focus indicator, keyed off the hidden input's
3315
+ // focus. The ring sits out from the box, so its radius carries an extra
3316
+ // 4px to round proportionally (as the original hand-drawn ring did) —
3317
+ // without it the corners gap away from the box.
2379
3318
  '&:focus + .checkbox': {
2380
- position: 'relative',
2381
- '&::after': {
2382
- content: '""',
2383
- position: 'absolute',
2384
- top: '-4px',
2385
- left: '-4px',
2386
- right: '-4px',
2387
- bottom: '-4px',
2388
- border: `2px solid ${this.getThemeColor(this.variant())}`,
2389
- borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 4}px`,
2390
- pointerEvents: 'none',
2391
- },
3319
+ ...this.theme.focusRingStyle(this.getThemeColor(this.variant()), this.componentOptions().focusRingGap),
3320
+ borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 2}px`,
2392
3321
  },
2393
3322
  }), ...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
2394
3323
  getThemeColor(token) {
@@ -2408,196 +3337,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2408
3337
  args: [{ selector: 'uni-checkbox', imports: [UniTextComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n" }]
2409
3338
  }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }] } });
2410
3339
 
2411
- class BodyRenderDirective {
2412
- el = inject(ElementRef);
2413
- renderer = inject(Renderer2);
2414
- ngOnInit() {
2415
- const element = this.el.nativeElement;
2416
- this.renderer.appendChild(document.body, element);
2417
- }
2418
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2419
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: BodyRenderDirective, isStandalone: true, selector: "[uniBodyRender]", ngImport: i0 });
2420
- }
2421
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BodyRenderDirective, decorators: [{
2422
- type: Directive,
2423
- args: [{
2424
- selector: '[uniBodyRender]',
2425
- }]
2426
- }] });
2427
-
2428
- class DragAndDropDirective {
2429
- // TODO(v4): rename to fileDropped — renaming is breaking
2430
- // eslint-disable-next-line @angular-eslint/no-output-on-prefix
2431
- onFileDropped = output();
2432
- workspaceOpacity = signal('1', ...(ngDevMode ? [{ debugName: "workspaceOpacity" }] : /* istanbul ignore next */ []));
2433
- // Dragover listener, when files are dragged over our host element
2434
- onDragOver(event) {
2435
- event.preventDefault();
2436
- event.stopPropagation();
2437
- this.workspaceOpacity.set('0.5');
2438
- }
2439
- // Dragleave listener, when files are dragged away from our host element
2440
- onDragLeave(event) {
2441
- event.preventDefault();
2442
- event.stopPropagation();
2443
- this.workspaceOpacity.set('1');
2444
- }
2445
- // Drop listener, when files are dropped on our host element
2446
- onDrop(event) {
2447
- event.preventDefault();
2448
- event.stopPropagation();
2449
- this.workspaceOpacity.set('1');
2450
- const files = event.dataTransfer?.files;
2451
- if (files && files.length > 0) {
2452
- this.onFileDropped.emit(files);
2453
- }
2454
- }
2455
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2456
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.12", type: DragAndDropDirective, isStandalone: true, selector: "[uni-drag-n-drop], [dragAndDrop]", outputs: { onFileDropped: "onFileDropped" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "style.opacity": "workspaceOpacity()" } }, ngImport: i0 });
2457
- }
2458
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: DragAndDropDirective, decorators: [{
2459
- type: Directive,
2460
- args: [{
2461
- selector: '[uni-drag-n-drop], [dragAndDrop]',
2462
- host: {
2463
- '[style.opacity]': 'workspaceOpacity()',
2464
- '(dragover)': 'onDragOver($event)',
2465
- '(dragleave)': 'onDragLeave($event)',
2466
- '(drop)': 'onDrop($event)',
2467
- },
2468
- }]
2469
- }], propDecorators: { onFileDropped: [{ type: i0.Output, args: ["onFileDropped"] }] } });
2470
-
2471
- class UniIconButtonComponent {
2472
- theme = inject(ThemeService);
2473
- config = this.theme.component('iconButton');
2474
- /**
2475
- * Accessible name for the button. Alternative to projecting text content
2476
- * (`<button icon-button>Close</button>`); one of the two is required for
2477
- * an icon-only button to be announced correctly.
2478
- */
2479
- ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2480
- iconName = input(...(ngDevMode ? [undefined, { debugName: "iconName" }] : /* istanbul ignore next */ []));
2481
- symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
2482
- variant = input('ghost', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
2483
- size = input('lg', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2484
- disable = input(...(ngDevMode ? [undefined, { debugName: "disable" }] : /* istanbul ignore next */ []));
2485
- loading = input(...(ngDevMode ? [undefined, { debugName: "loading" }] : /* istanbul ignore next */ []));
2486
- opticalSize = input(24, ...(ngDevMode ? [{ debugName: "opticalSize" }] : /* istanbul ignore next */ []));
2487
- srOnlyClass = css(visuallyHidden);
2488
- /**
2489
- * Sizes `iconName` from the size token's `fontSize`, the same value that sizes
2490
- * a `symbolName` ligature — so the two paths render the same glyph size and
2491
- * `symbolName` → `iconName` is a like-for-like swap. Without it a masked icon
2492
- * fills the whole button box, since the base size tokens carry no padding.
2493
- * Themes that do use padding (Carbon) set a matching `fontSize`, so they land
2494
- * on the same glyph either way.
2495
- */
2496
- glyphSize = computed(() => {
2497
- // Size tokens are Emotion style objects, so `fontSize` is typed wider than
2498
- // a CSS length; anything exotic falls back to filling the button as before.
2499
- const fontSize = this.config().sizes?.[this.size()]?.fontSize;
2500
- return typeof fontSize === 'number' || typeof fontSize === 'string' ? fontSize : undefined;
2501
- }, ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
2502
- className = computed(() => {
2503
- const { sizes, variants } = this.config();
2504
- const sizeConfig = sizes && sizes[this.size()];
2505
- const colorConfig = variants && variants[this.variant()];
2506
- return css([
2507
- {
2508
- position: 'relative',
2509
- overflow: 'hidden',
2510
- outline: 0,
2511
- border: 0,
2512
- cursor: 'pointer',
2513
- transition: 'all 0.28s ease',
2514
- // Token-driven radius (`max` = circle) with the legacy 999 fallback
2515
- // for hand-authored themes that predate iconButton options.
2516
- ...(this.theme.radius(this.config().options?.borderRadius) ?? { borderRadius: 999 }),
2517
- // Block-level, but centring: the size tokens make the box bigger than
2518
- // the glyph (an `sm` button is 22px around an 18px icon), so a plain
2519
- // `display: block` parks the glyph in the top-left corner. Flex is
2520
- // still block-level, so nothing about the button's own layout changes.
2521
- // The accessible-name span is absolutely positioned and so stays out
2522
- // of the flex flow.
2523
- display: 'flex',
2524
- alignItems: 'center',
2525
- justifyContent: 'center',
2526
- '&:disabled': {
2527
- cursor: 'not-allowed !important',
2528
- },
2529
- '& symbol': {
2530
- fontSize: 'inherit',
2531
- lineHeight: 'inherit',
2532
- },
2533
- },
2534
- sizeConfig && {
2535
- ...sizeConfig,
2536
- },
2537
- colorConfig && {
2538
- ...colorConfig,
2539
- },
2540
- this.symbolName() &&
2541
- !this.loading() && {
2542
- padding: 0,
2543
- },
2544
- this.variant() !== 'ghost' && {
2545
- '&:hover': {
2546
- ...this.theme.boxShadow('raised'),
2547
- },
2548
- },
2549
- this.variant() === 'ghost' && {
2550
- '&:hover': {
2551
- backgroundColor: 'rgba(0,0,0,0.1)',
2552
- },
2553
- },
2554
- !this.loading() && {
2555
- '&:disabled': {
2556
- ...this.config().variants?.disabled,
2557
- },
2558
- },
2559
- ]);
2560
- }, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2561
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2562
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniIconButtonComponent, isStandalone: true, selector: "button[uni-icon-button], button[icon-button]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, iconName: { classPropertyName: "iconName", publicName: "iconName", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disable: { classPropertyName: "disable", publicName: "disable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, opticalSize: { classPropertyName: "opticalSize", publicName: "opticalSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.disabled": "disable() || loading() || null", "attr.aria-busy": "loading() ? 'true' : null", "attr.aria-label": "ariaLabel() || null", "class": "className()" } }, hostDirectives: [{ directive: RippleDirective }], ngImport: i0, template: `
2563
- @if (loading()) {
2564
- <uni-icon name="spinner" />
2565
- } @else if (symbolName()) {
2566
- <uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
2567
- } @else if (iconName()) {
2568
- <uni-icon [name]="iconName()!" [size]="glyphSize()" />
2569
- }
2570
- <!-- Projected text is the button's accessible name (visually hidden) -->
2571
- <span [class]="srOnlyClass"><ng-content /></span>
2572
- `, isInline: true, dependencies: [{ kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2573
- }
2574
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniIconButtonComponent, decorators: [{
2575
- type: Component,
2576
- args: [{
2577
- selector: 'button[uni-icon-button], button[icon-button]',
2578
- imports: [UniSymbolComponent, UniIconComponent],
2579
- template: `
2580
- @if (loading()) {
2581
- <uni-icon name="spinner" />
2582
- } @else if (symbolName()) {
2583
- <uni-symbol [name]="symbolName()!" [opticalSize]="opticalSize()" />
2584
- } @else if (iconName()) {
2585
- <uni-icon [name]="iconName()!" [size]="glyphSize()" />
2586
- }
2587
- <!-- Projected text is the button's accessible name (visually hidden) -->
2588
- <span [class]="srOnlyClass"><ng-content /></span>
2589
- `,
2590
- changeDetection: ChangeDetectionStrategy.OnPush,
2591
- host: {
2592
- '[attr.disabled]': 'disable() || loading() || null',
2593
- '[attr.aria-busy]': "loading() ? 'true' : null",
2594
- '[attr.aria-label]': 'ariaLabel() || null',
2595
- '[class]': 'className()',
2596
- },
2597
- hostDirectives: [{ directive: RippleDirective }],
2598
- }]
2599
- }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], iconName: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconName", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disable: [{ type: i0.Input, args: [{ isSignal: true, alias: "disable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], opticalSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "opticalSize", required: false }] }] } });
2600
-
2601
3340
  class UniDataSearchComponent extends BaseComponent {
2602
3341
  datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
2603
3342
  placeholder = input('Search', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
@@ -3077,6 +3816,1021 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3077
3816
  * This file exports all public-facing elements of the data-table component.
3078
3817
  */
3079
3818
 
3819
+ class UniDropdownComponent extends BaseComponent {
3820
+ renderer = inject(Renderer2);
3821
+ delay = 100;
3822
+ // Reactively track visibility status using Signals
3823
+ showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
3824
+ trigger = input.required(...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
3825
+ placement = input('bottom-start', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
3826
+ offset = input({ mainAxis: 4, alignmentAxis: 12 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
3827
+ /** CSS anchor-name linking the trigger to the popover panel. */
3828
+ anchorName = newAnchorName();
3829
+ /**
3830
+ * Value for aria-haspopup on the trigger, describing what the popover
3831
+ * contains (e.g. 'menu' for Menu, 'dialog' for rich content). When unset,
3832
+ * only aria-expanded/aria-controls are managed.
3833
+ */
3834
+ ariaHasPopup = input(null, ...(ngDevMode ? [{ debugName: "ariaHasPopup" }] : /* istanbul ignore next */ []));
3835
+ /** Document-unique id of the popover element, for aria-controls wiring. */
3836
+ popoverId = uniqueId('uni-dropdown');
3837
+ paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
3838
+ paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
3839
+ // Per-instance panel-chrome overrides; undefined falls back to the theme's
3840
+ // `dropdown` options, so hosts like uni-menu can restyle their panel
3841
+ // without forking the shared dropdown entry.
3842
+ border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
3843
+ borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
3844
+ shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
3845
+ color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
3846
+ dropdownShowing = output();
3847
+ dropdownHiding = output();
3848
+ dropdownRef;
3849
+ get _trigger() {
3850
+ return this.trigger();
3851
+ }
3852
+ get _dropdown() {
3853
+ return this.dropdownRef.nativeElement;
3854
+ }
3855
+ transformOriginMap = {
3856
+ top: 'bottom center',
3857
+ right: 'center left',
3858
+ bottom: 'top center',
3859
+ left: 'center right',
3860
+ 'top-start': 'bottom left',
3861
+ 'top-end': 'bottom right',
3862
+ 'right-start': 'top left',
3863
+ 'right-end': 'bottom left',
3864
+ 'bottom-start': 'top left',
3865
+ 'bottom-end': 'top right',
3866
+ 'left-start': 'top right',
3867
+ 'left-end': 'bottom right',
3868
+ };
3869
+ dropdownClass = computed(() => {
3870
+ const currentPlacement = this.placement();
3871
+ return css([
3872
+ {
3873
+ // Reset browser agent default popover styles
3874
+ border: 'none',
3875
+ background: 'transparent',
3876
+ padding: 0,
3877
+ overflow: 'visible',
3878
+ width: 'max-content',
3879
+ // Native anchor positioning: the browser keeps the panel attached to
3880
+ // the trigger (no scroll/resize listeners needed)
3881
+ ...anchorStyles(this.anchorName, currentPlacement, this.offset()),
3882
+ // 2. Animate discrete properties across top layer layout contexts
3883
+ transitionProperty: 'transform, opacity, display, overlay',
3884
+ transitionDuration: `${this.delay}ms`,
3885
+ transitionTimingFunction: 'linear',
3886
+ transitionBehavior: 'allow-discrete',
3887
+ // Hidden State (Closed)
3888
+ opacity: 0,
3889
+ transform: 'scale(0.8)',
3890
+ transformOrigin: this.transformOriginMap[currentPlacement],
3891
+ // 3. Active state styling controlled via the native browser pseudo-class
3892
+ ['&:popover-open']: {
3893
+ opacity: 1,
3894
+ transform: 'scale(1)',
3895
+ },
3896
+ // 4. Starting-style rules what properties animate *from* when transitioning in
3897
+ ['@starting-style']: {
3898
+ ['&:popover-open']: {
3899
+ opacity: 0,
3900
+ transform: 'scale(0.8)',
3901
+ },
3902
+ },
3903
+ },
3904
+ ]);
3905
+ }, ...(ngDevMode ? [{ debugName: "dropdownClass" }] : /* istanbul ignore next */ []));
3906
+ /** The element that receives focus and carries the ARIA popup state. */
3907
+ get _focusTarget() {
3908
+ return resolveFocusTarget(this._trigger);
3909
+ }
3910
+ ngOnInit() {
3911
+ // Single native click binding to manage open/close commands
3912
+ this.renderer.listen(this._trigger, 'click', (e) => {
3913
+ e.stopPropagation();
3914
+ this.toggleDropdown();
3915
+ });
3916
+ // Anchor the popover panel to the trigger element
3917
+ this.renderer.setStyle(this._trigger, 'anchor-name', this.anchorName);
3918
+ // Wire the ARIA popup contract onto the focusable trigger element
3919
+ const focusTarget = this._focusTarget;
3920
+ this.renderer.setAttribute(focusTarget, 'aria-expanded', 'false');
3921
+ this.renderer.setAttribute(focusTarget, 'aria-controls', this.popoverId);
3922
+ if (this.ariaHasPopup()) {
3923
+ this.renderer.setAttribute(focusTarget, 'aria-haspopup', this.ariaHasPopup());
3924
+ }
3925
+ // Sync state if user invokes light-dismiss via outside click or Escape key
3926
+ this.renderer.listen(this._dropdown, 'toggle', (event) => {
3927
+ const isOpened = event.newState === 'open';
3928
+ this.showing.set(isOpened);
3929
+ this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
3930
+ if (isOpened) {
3931
+ this.dropdownShowing.emit(true);
3932
+ }
3933
+ else {
3934
+ this.dropdownHiding.emit(true);
3935
+ this.restoreFocus();
3936
+ }
3937
+ });
3938
+ }
3939
+ /**
3940
+ * Returns focus to the trigger when the popover closes while focus was
3941
+ * inside it (or was dropped on <body> by the top layer closing), so
3942
+ * keyboard users are never stranded (WCAG 2.4.3).
3943
+ */
3944
+ restoreFocus() {
3945
+ const active = document.activeElement;
3946
+ if (active === document.body || (active && this._dropdown.contains(active))) {
3947
+ this._focusTarget.focus();
3948
+ }
3949
+ }
3950
+ toggleDropdown() {
3951
+ if (this.showing()) {
3952
+ this._dropdown.hidePopover();
3953
+ }
3954
+ else {
3955
+ this._dropdown.showPopover();
3956
+ }
3957
+ }
3958
+ hideDropdown() {
3959
+ this._dropdown.hidePopover();
3960
+ }
3961
+ ngOnDestroy() {
3962
+ try {
3963
+ this._dropdown.hidePopover();
3964
+ }
3965
+ catch {
3966
+ // popover was already closed or detached
3967
+ }
3968
+ }
3969
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3970
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDropdownComponent, isStandalone: true, selector: "uni-dropdown", inputs: { trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, ariaHasPopup: { classPropertyName: "ariaHasPopup", publicName: "ariaHasPopup", isSignal: true, isRequired: false, transformFunction: null }, paddingVertical: { classPropertyName: "paddingVertical", publicName: "paddingVertical", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null }, border: { classPropertyName: "border", publicName: "border", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dropdownShowing: "dropdownShowing", dropdownHiding: "dropdownHiding" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }], viewQueries: [{ propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
3971
+ <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
3972
+ <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3973
+ <div
3974
+ box-layout
3975
+ [border]="border() ?? componentOptions().border"
3976
+ [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
3977
+ [paddingVertical]="paddingVertical()"
3978
+ [paddingHorizontal]="paddingHorizontal()"
3979
+ [color]="color() ?? componentOptions().color"
3980
+ [shadow]="shadow() ?? componentOptions().shadow"
3981
+ >
3982
+ <ng-content></ng-content>
3983
+ </div>
3984
+ </div>
3985
+ `, isInline: true, dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3986
+ }
3987
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, decorators: [{
3988
+ type: Component,
3989
+ args: [{
3990
+ changeDetection: ChangeDetectionStrategy.OnPush,
3991
+ selector: 'uni-dropdown',
3992
+ imports: [UniBoxComponent],
3993
+ template: `
3994
+ <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
3995
+ <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3996
+ <div
3997
+ box-layout
3998
+ [border]="border() ?? componentOptions().border"
3999
+ [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
4000
+ [paddingVertical]="paddingVertical()"
4001
+ [paddingHorizontal]="paddingHorizontal()"
4002
+ [color]="color() ?? componentOptions().color"
4003
+ [shadow]="shadow() ?? componentOptions().shadow"
4004
+ >
4005
+ <ng-content></ng-content>
4006
+ </div>
4007
+ </div>
4008
+ `,
4009
+ providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
4010
+ }]
4011
+ }], propDecorators: { trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaHasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaHasPopup", required: false }] }], paddingVertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingVertical", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], border: [{ type: i0.Input, args: [{ isSignal: true, alias: "border", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], shadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shadow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dropdownShowing: [{ type: i0.Output, args: ["dropdownShowing"] }], dropdownHiding: [{ type: i0.Output, args: ["dropdownHiding"] }], dropdownRef: [{
4012
+ type: ViewChild,
4013
+ args: ['dropdown', { static: true }]
4014
+ }] } });
4015
+
4016
+ class UniInputBoxComponent extends BaseComponent {
4017
+ className = css({ display: 'contents' });
4018
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4019
+ error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
4020
+ minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
4021
+ /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
4022
+ height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4023
+ color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4024
+ border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
4025
+ shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4026
+ inputBoxClass = computed(() => css([
4027
+ this.disabled() && {
4028
+ ...this.theme.color(this.componentOptions().disabledTextColor),
4029
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4030
+ cursor: 'not-allowed !important',
4031
+ },
4032
+ {
4033
+ '& input, select, textarea': {
4034
+ ...removeInputPlatformStyling,
4035
+ height: '100%',
4036
+ ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4037
+ ...this.theme.color(this.componentOptions().textColor),
4038
+ ...this.theme.typeface(this.componentOptions().typeFace),
4039
+ },
4040
+ // Multi-line fields size themselves (rows/resize), not from the box.
4041
+ '& textarea': {
4042
+ height: 'auto',
4043
+ ...this.theme.paddingTop('xs'),
4044
+ ...this.theme.paddingBottom('xs'),
4045
+ },
4046
+ '&:has(input:disabled, select:disabled, textarea:disabled)': {
4047
+ ...this.theme.color(this.componentOptions().disabledTextColor),
4048
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4049
+ },
4050
+ '& input:disabled, select:disabled, textarea:disabled': {
4051
+ cursor: 'not-allowed !important',
4052
+ },
4053
+ '&:has(input:focus, select:focus, textarea:focus)': {
4054
+ outline: this.componentOptions().focusOutline,
4055
+ outlineOffset: this.componentOptions().focusOutlineOffset,
4056
+ // Optional focus chrome (border/ring/background). It yields to the
4057
+ // error state, so a flagged field stays visibly flagged while the
4058
+ // user is in it correcting the value.
4059
+ ...(this.error()
4060
+ ? {}
4061
+ : {
4062
+ ...this.theme.border(this.componentOptions().focusBorder),
4063
+ ...this.theme.boxShadow(this.componentOptions().focusShadow),
4064
+ ...this.theme.backgroundColor(this.componentOptions().focusColor),
4065
+ }),
4066
+ },
4067
+ },
4068
+ ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4069
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4070
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4071
+ }
4072
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4073
+ type: Component,
4074
+ args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
4075
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4076
+
4077
+ /**
4078
+ * Date field with free-typed parsing and a popup calendar. Type `aug 20`,
4079
+ * `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
4080
+ * same canonical `'YYYY-MM-DD'` string either way. Parsing is `Intl`-driven
4081
+ * (locale digit order and month names, never hardcoded); unreadable text
4082
+ * stays in the field, flagged, with a `rejected` event. The popup is a
4083
+ * native popover hosting the same `uni-calendar` an app could render inline.
4084
+ */
4085
+ class UniDateInputComponent extends BaseComponent {
4086
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4087
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
4088
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4089
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4090
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4091
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4092
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4093
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4094
+ // --- Configuration -------------------------------------------------------
4095
+ /** Accessible name for the field, e.g. "Appointment date". */
4096
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4097
+ /** Defaults to the locale's digit pattern, e.g. `MM/DD/YYYY`. */
4098
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
4099
+ /** How the committed value renders, e.g. `{ dateStyle: 'long' }`. */
4100
+ displayFormat = input({ dateStyle: 'medium' }, ...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
4101
+ /** BCP 47 tag; defaults to the document language, then the browser's. */
4102
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
4103
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
4104
+ /** Custom parser, replacing the built-in ISO/locale/month-name parsing. */
4105
+ parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
4106
+ /** Renders without its own input-box chrome, for composers like uni-date-time-input. */
4107
+ embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
4108
+ // --- Forwarded to the popup calendar --------------------------------------
4109
+ minDate = input(...(ngDevMode ? [undefined, { debugName: "minDate" }] : /* istanbul ignore next */ []));
4110
+ maxDate = input(...(ngDevMode ? [undefined, { debugName: "maxDate" }] : /* istanbul ignore next */ []));
4111
+ disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
4112
+ markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
4113
+ weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
4114
+ // --- Events ----------------------------------------------------------------
4115
+ /** Popup shown. */
4116
+ opened = output();
4117
+ /** Popup hidden. */
4118
+ closed = output();
4119
+ /** A typed commit was refused; the raw text stays in the field. */
4120
+ rejected = output();
4121
+ host = inject(ElementRef);
4122
+ inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
4123
+ // #toggle sits on the icon-button component, so read the element explicitly.
4124
+ toggleRef = viewChild('toggle', { ...(ngDevMode ? { debugName: "toggleRef" } : /* istanbul ignore next */ {}), read: ElementRef });
4125
+ popupRef = viewChild('popupDialog', ...(ngDevMode ? [{ debugName: "popupRef" }] : /* istanbul ignore next */ []));
4126
+ dropdown = viewChild(UniDropdownComponent, ...(ngDevMode ? [{ debugName: "dropdown" }] : /* istanbul ignore next */ []));
4127
+ calendar = viewChild(UniCalendarComponent, ...(ngDevMode ? [{ debugName: "calendar" }] : /* istanbul ignore next */ []));
4128
+ srOnly = css(visuallyHidden);
4129
+ /** A refused commit — styles the field and sets aria-invalid until edited. */
4130
+ draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
4131
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
4132
+ toggleElement = computed(() => this.toggleRef()?.nativeElement, ...(ngDevMode ? [{ debugName: "toggleElement" }] : /* istanbul ignore next */ []));
4133
+ popupOpen = computed(() => this.dropdown()?.showing() ?? false, ...(ngDevMode ? [{ debugName: "popupOpen" }] : /* istanbul ignore next */ []));
4134
+ resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
4135
+ displayText = computed(() => {
4136
+ const value = this.value();
4137
+ return value ? formatDate(value, this.resolvedLocale(), this.displayFormat()) : '';
4138
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
4139
+ resolvedPlaceholder = computed(() => this.placeholder() ?? localeDatePlaceholder(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
4140
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4141
+ toggleLabel = computed(() => {
4142
+ const value = this.value();
4143
+ return value
4144
+ ? `Change date, ${formatDate(value, this.resolvedLocale(), { dateStyle: 'full' })}`
4145
+ : 'Choose date';
4146
+ }, ...(ngDevMode ? [{ debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
4147
+ // --- Committing -------------------------------------------------------------
4148
+ fullDate(date) {
4149
+ return formatDate(date, this.resolvedLocale(), { dateStyle: 'full' });
4150
+ }
4151
+ isDayBlocked(date) {
4152
+ const dates = this.disabledDates();
4153
+ if (!dates)
4154
+ return false;
4155
+ return Array.isArray(dates) ? dates.includes(date) : dates(date);
4156
+ }
4157
+ setValue(date, silent = false) {
4158
+ this.value.set(date);
4159
+ this.draftInvalid.set(false);
4160
+ this.setFieldText(this.displayText());
4161
+ if (!silent)
4162
+ this.announce(date ? `${this.fullDate(date)}.` : 'Date cleared.');
4163
+ }
4164
+ refuse(raw, reason) {
4165
+ this.draftInvalid.set(true);
4166
+ const message = {
4167
+ unparseable: `Couldn't read “${raw}” as a date.`,
4168
+ 'out-of-range': `${raw} is outside the allowed dates.`,
4169
+ disabled: `${raw} isn't available.`,
4170
+ }[reason];
4171
+ this.announce(message);
4172
+ this.rejected.emit({ raw, reason });
4173
+ }
4174
+ commit(raw) {
4175
+ const trimmed = raw.trim();
4176
+ if (!trimmed) {
4177
+ this.setValue(undefined);
4178
+ return true;
4179
+ }
4180
+ const custom = this.parse();
4181
+ const parsed = custom
4182
+ ? custom(trimmed, this.resolvedLocale())
4183
+ : parseDateText(trimmed, this.resolvedLocale());
4184
+ if (!parsed) {
4185
+ this.refuse(trimmed, 'unparseable');
4186
+ return false;
4187
+ }
4188
+ const min = this.minDate();
4189
+ const max = this.maxDate();
4190
+ if ((min && parsed < min) || (max && parsed > max)) {
4191
+ this.refuse(trimmed, 'out-of-range');
4192
+ return false;
4193
+ }
4194
+ if (this.isDayBlocked(parsed)) {
4195
+ this.refuse(trimmed, 'disabled');
4196
+ return false;
4197
+ }
4198
+ this.setValue(parsed);
4199
+ return true;
4200
+ }
4201
+ /** Step a committed value ±1 day, skipping blocked days, stopping at fences. */
4202
+ step(direction) {
4203
+ const committed = this.value();
4204
+ if (!committed)
4205
+ return;
4206
+ let date = addDays(committed, direction);
4207
+ let guard = 0;
4208
+ while (this.isDayBlocked(date) && guard++ < 400)
4209
+ date = addDays(date, direction);
4210
+ const min = this.minDate();
4211
+ const max = this.maxDate();
4212
+ if ((min && date < min) || (max && date > max))
4213
+ return; // fence
4214
+ this.setValue(date);
4215
+ }
4216
+ // --- Keyboard ----------------------------------------------------------------
4217
+ onInputKeydown(event) {
4218
+ const element = this.inputRef().nativeElement;
4219
+ switch (event.key) {
4220
+ case 'Enter':
4221
+ event.preventDefault();
4222
+ this.commit(element.value);
4223
+ break;
4224
+ case 'Escape':
4225
+ if (this.popupOpen()) {
4226
+ this.closePopup();
4227
+ }
4228
+ else {
4229
+ this.setFieldText(this.displayText());
4230
+ this.draftInvalid.set(false);
4231
+ }
4232
+ break;
4233
+ case 'ArrowDown':
4234
+ event.preventDefault();
4235
+ // Alt or an empty field opens the popup; on a committed value the
4236
+ // caret has nowhere to go, so stepping is what a spinner would do.
4237
+ if (event.altKey || element.value.trim() === '')
4238
+ this.openPopup();
4239
+ else if (this.value() && element.value === this.displayText())
4240
+ this.step(-1);
4241
+ break;
4242
+ case 'ArrowUp':
4243
+ event.preventDefault();
4244
+ if (this.value() && element.value === this.displayText())
4245
+ this.step(1);
4246
+ break;
4247
+ }
4248
+ }
4249
+ onInput() {
4250
+ this.draftInvalid.set(false);
4251
+ }
4252
+ onFocusOut(event) {
4253
+ const next = event.relatedTarget;
4254
+ if (next && this.host.nativeElement.contains(next))
4255
+ return;
4256
+ this.touched.set(true);
4257
+ if (this.popupOpen())
4258
+ return;
4259
+ const element = this.inputRef()?.nativeElement;
4260
+ if (element && this.commitOnBlur() && element.value !== this.displayText())
4261
+ this.commit(element.value);
4262
+ }
4263
+ // --- Popup ---------------------------------------------------------------------
4264
+ openPopup() {
4265
+ if (this.disabled() || this.popupOpen())
4266
+ return;
4267
+ this.dropdown()?.toggleDropdown();
4268
+ }
4269
+ closePopup() {
4270
+ if (this.popupOpen())
4271
+ this.dropdown()?.hideDropdown();
4272
+ }
4273
+ onPopupShowing() {
4274
+ // The grid opens on the committed value's month (or today's).
4275
+ this.calendar()?.month.set(monthOf(this.value() ?? todayIso()));
4276
+ this.calendar()?.focusActiveDay();
4277
+ this.opened.emit();
4278
+ }
4279
+ onPopupHiding() {
4280
+ this.closed.emit();
4281
+ // Focus returns to the field (not the toggle) when it was in the popup —
4282
+ // this runs before the dropdown's own restoreFocus, which then no-ops.
4283
+ const active = document.activeElement;
4284
+ const popup = this.popupRef()?.nativeElement;
4285
+ if (!active || active === document.body || (popup && popup.contains(active)))
4286
+ this.inputRef()?.nativeElement.focus();
4287
+ }
4288
+ onCalendarPick(date) {
4289
+ this.setValue(date);
4290
+ this.closePopup();
4291
+ }
4292
+ /** The popup is a focus-holding dialog: Tab cycles inside it (APG pattern). */
4293
+ onPopupKeydown(event) {
4294
+ if (event.key === 'Escape') {
4295
+ event.preventDefault();
4296
+ this.closePopup();
4297
+ return;
4298
+ }
4299
+ if (event.key !== 'Tab')
4300
+ return;
4301
+ const popup = this.popupRef()?.nativeElement;
4302
+ if (!popup)
4303
+ return;
4304
+ const focusables = Array.from(popup.querySelectorAll('button:not(:disabled)')).filter((button) => button.tabIndex >= 0);
4305
+ if (!focusables.length)
4306
+ return;
4307
+ const index = focusables.indexOf(document.activeElement);
4308
+ const next = focusables[(index + (event.shiftKey ? -1 : 1) + focusables.length) % focusables.length];
4309
+ event.preventDefault();
4310
+ next.focus();
4311
+ }
4312
+ // --- Internals -------------------------------------------------------------------
4313
+ setFieldText(text) {
4314
+ const element = this.inputRef()?.nativeElement;
4315
+ if (element)
4316
+ element.value = text;
4317
+ }
4318
+ announce(message) {
4319
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
4320
+ }
4321
+ // --- Styling -----------------------------------------------------------------------
4322
+ className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4323
+ rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
4324
+ inputClass = computed(() => {
4325
+ const colors = this.theme.colorPalette();
4326
+ return css([
4327
+ {
4328
+ flex: 1,
4329
+ minWidth: 0,
4330
+ border: 0,
4331
+ outline: 'none',
4332
+ background: 'transparent',
4333
+ color: 'inherit',
4334
+ font: 'inherit',
4335
+ },
4336
+ this.draftInvalid() && {
4337
+ color: colors['warn'],
4338
+ // Shape and colour, not colour alone (WCAG 1.4.1).
4339
+ textDecoration: `underline dashed ${colors['warn']} 1.5px`,
4340
+ textUnderlineOffset: 3,
4341
+ },
4342
+ ]);
4343
+ }, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
4344
+ toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
4345
+ /** Embedded mode: the composer owns the box; keep only the flex row. */
4346
+ embeddedClass = computed(() => {
4347
+ const colors = this.theme.colorPalette();
4348
+ return css([
4349
+ { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 },
4350
+ this.draftInvalid() && { color: colors['warn'] },
4351
+ ]);
4352
+ }, ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4353
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4354
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDateInputComponent, isStandalone: true, selector: "uni-date-input, DateInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", opened: "opened", closed: "closed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "toggleRef", first: true, predicate: ["toggle"], descendants: true, read: ElementRef, isSignal: true }, { propertyName: "popupRef", first: true, predicate: ["popupDialog"], descendants: true, isSignal: true }, { propertyName: "dropdown", first: true, predicate: UniDropdownComponent, descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: UniCalendarComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so commits happen only when focus\n leaves the whole field, not while moving into the popup. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <!-- The dropdown wires aria-haspopup/aria-expanded/aria-controls onto\n this button; its name tracks the value (\"Change date, \u2026\"). -->\n <button\n #toggle\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'calendar_month'\"\n [disable]=\"disabled()\"\n >\n {{ toggleLabel() }}\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (toggleElement()) {\n <uni-dropdown\n [trigger]=\"toggleElement()!\"\n ariaHasPopup=\"dialog\"\n placement=\"bottom-end\"\n [color]=\"componentOptions().popupColor ?? 'primary-surface'\"\n [shadow]=\"componentOptions().popupShadow ?? 'menu'\"\n [borderRadius]=\"componentOptions().popupBorderRadius ?? 'xs'\"\n paddingVertical=\"xs\"\n paddingHorizontal=\"xs\"\n (dropdownShowing)=\"onPopupShowing()\"\n (dropdownHiding)=\"onPopupHiding()\"\n >\n <!-- A focus-holding dialog per the APG date-picker pattern: Tab cycles\n inside, Escape closes and returns focus to the field. -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div #popupDialog role=\"dialog\" aria-label=\"Choose date\" (keydown)=\"onPopupKeydown($event)\">\n <uni-calendar\n mode=\"single\"\n [value]=\"value()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n (selected)=\"onCalendarPick($event)\"\n />\n </div>\n </uni-dropdown>\n }\n\n <!-- Commits, steps and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniCalendarComponent, selector: "uni-calendar, Calendar", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "mode", "month", "minDate", "maxDate", "disabledDates", "markers", "locale", "weekStart", "ariaLabel", "size"], outputs: ["valueChange", "touchedChange", "monthChange", "selected"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "color"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4355
+ }
4356
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, decorators: [{
4357
+ type: Component,
4358
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-date-input, DateInput', imports: [
4359
+ NgTemplateOutlet,
4360
+ UniCalendarComponent,
4361
+ UniDropdownComponent,
4362
+ UniIconButtonComponent,
4363
+ UniInputBoxComponent,
4364
+ ], providers: [{ provide: COMPONENT_NAME, useValue: 'dateInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so commits happen only when focus\n leaves the whole field, not while moving into the popup. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <!-- The dropdown wires aria-haspopup/aria-expanded/aria-controls onto\n this button; its name tracks the value (\"Change date, \u2026\"). -->\n <button\n #toggle\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'calendar_month'\"\n [disable]=\"disabled()\"\n >\n {{ toggleLabel() }}\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (toggleElement()) {\n <uni-dropdown\n [trigger]=\"toggleElement()!\"\n ariaHasPopup=\"dialog\"\n placement=\"bottom-end\"\n [color]=\"componentOptions().popupColor ?? 'primary-surface'\"\n [shadow]=\"componentOptions().popupShadow ?? 'menu'\"\n [borderRadius]=\"componentOptions().popupBorderRadius ?? 'xs'\"\n paddingVertical=\"xs\"\n paddingHorizontal=\"xs\"\n (dropdownShowing)=\"onPopupShowing()\"\n (dropdownHiding)=\"onPopupHiding()\"\n >\n <!-- A focus-holding dialog per the APG date-picker pattern: Tab cycles\n inside, Escape closes and returns focus to the field. -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div #popupDialog role=\"dialog\" aria-label=\"Choose date\" (keydown)=\"onPopupKeydown($event)\">\n <uni-calendar\n mode=\"single\"\n [value]=\"value()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n (selected)=\"onCalendarPick($event)\"\n />\n </div>\n </uni-dropdown>\n }\n\n <!-- Commits, steps and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
4365
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], parse: [{ type: i0.Input, args: [{ isSignal: true, alias: "parse", required: false }] }], embedded: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedded", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], toggleRef: [{ type: i0.ViewChild, args: ['toggle', { ...{ read: ElementRef }, isSignal: true }] }], popupRef: [{ type: i0.ViewChild, args: ['popupDialog', { isSignal: true }] }], dropdown: [{ type: i0.ViewChild, args: [i0.forwardRef(() => UniDropdownComponent), { isSignal: true }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => UniCalendarComponent), { isSignal: true }] }] } });
4366
+
4367
+ const toMinutes = (time) => {
4368
+ const [h, m] = time.split(':').map(Number);
4369
+ return h * 60 + m;
4370
+ };
4371
+ const fromMinutes = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
4372
+ /**
4373
+ * Time field: a combobox over time options — the same listbox contract as
4374
+ * uni-search-input and uni-tag-input. Type `3p`, `930` or `15:00`, or pick
4375
+ * `3:00 PM` from the list; the form always gets 24-hour `'HH:mm'` (`hour12`
4376
+ * affects display only). The list is assistive, not exhaustive: any
4377
+ * parseable time commits, unless `slots` pins the choices (a slot picker) —
4378
+ * then a typed time must match one. Unreadable or unavailable text stays in
4379
+ * the field, flagged, with a `rejected` event.
4380
+ */
4381
+ class UniTimeInputComponent extends BaseComponent {
4382
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4383
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
4384
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4385
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4386
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4387
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4388
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4389
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4390
+ // --- Configuration -------------------------------------------------------
4391
+ /** Accessible name for the field, e.g. "Start time". */
4392
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4393
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
4394
+ /** Generated list granularity, in minutes. */
4395
+ minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
4396
+ /** Earliest allowed time (inclusive), `'09:00'`. */
4397
+ minTime = input(...(ngDevMode ? [undefined, { debugName: "minTime" }] : /* istanbul ignore next */ []));
4398
+ /** Latest allowed time (inclusive), `'17:00'`. */
4399
+ maxTime = input(...(ngDevMode ? [undefined, { debugName: "maxTime" }] : /* istanbul ignore next */ []));
4400
+ /** Exact allowed times (scheduling). When set, typed entry must match one. */
4401
+ slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
4402
+ /** 12-hour display; defaults from the locale. The value stays 24-hour. */
4403
+ hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
4404
+ /** BCP 47 tag for the display format; defaults to the document language. */
4405
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
4406
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
4407
+ /** Renders without its own input-box chrome, for composers like uni-date-time-input. */
4408
+ embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
4409
+ // --- Events ----------------------------------------------------------------
4410
+ /** A typed commit was refused; the raw text stays in the field. */
4411
+ rejected = output();
4412
+ host = inject(ElementRef);
4413
+ inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
4414
+ listRef = viewChild('listbox', ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
4415
+ srOnly = css(visuallyHidden);
4416
+ /** A refused commit — styles the field and sets aria-invalid until edited. */
4417
+ draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
4418
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
4419
+ resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
4420
+ resolvedHour12 = computed(() => this.hour12() ?? localeDefaultHour12(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedHour12" }] : /* istanbul ignore next */ []));
4421
+ /** The listed times: pinned `slots` verbatim, else the generated step grid. */
4422
+ options = computed(() => this.slots() ?? timeSlots(this.minuteStep(), this.minTime(), this.maxTime()), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4423
+ optionLabels = computed(() => this.options().map((time) => this.formatValue(time)), ...(ngDevMode ? [{ debugName: "optionLabels" }] : /* istanbul ignore next */ []));
4424
+ /** Shared combobox bookkeeping — identical contract to uni-search-input. */
4425
+ list = createListboxNavigation({
4426
+ count: () => this.options().length,
4427
+ idPrefix: 'uni-time-listbox',
4428
+ });
4429
+ displayText = computed(() => {
4430
+ const value = this.value();
4431
+ return value ? this.formatValue(value) : '';
4432
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
4433
+ resolvedPlaceholder = computed(() => this.placeholder() ?? this.formatValue('09:00'), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
4434
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4435
+ formatValue(time) {
4436
+ return formatTime(time, this.resolvedLocale(), this.resolvedHour12());
4437
+ }
4438
+ // --- Committing -------------------------------------------------------------
4439
+ setValue(time, silent = false) {
4440
+ this.value.set(time);
4441
+ this.draftInvalid.set(false);
4442
+ this.setFieldText(this.displayText());
4443
+ if (!silent)
4444
+ this.announce(time ? `${this.formatValue(time)}.` : 'Time cleared.');
4445
+ }
4446
+ refuse(raw, reason, shown = raw) {
4447
+ this.draftInvalid.set(true);
4448
+ const message = {
4449
+ unparseable: `Couldn't read “${shown}” as a time.`,
4450
+ 'out-of-range': `${shown} is outside the allowed times.`,
4451
+ unavailable: `${shown} isn't available.`,
4452
+ }[reason];
4453
+ this.announce(message);
4454
+ this.rejected.emit({ raw, reason });
4455
+ }
4456
+ commit(raw) {
4457
+ const trimmed = raw.trim();
4458
+ if (!trimmed) {
4459
+ this.setValue(undefined);
4460
+ return true;
4461
+ }
4462
+ let parsed = parseTimeText(trimmed, this.resolvedHour12());
4463
+ if (!parsed) {
4464
+ this.refuse(trimmed, 'unparseable');
4465
+ return false;
4466
+ }
4467
+ const min = this.minTime();
4468
+ const max = this.maxTime();
4469
+ const inBounds = (time) => !(min && time < min) && !(max && time > max);
4470
+ const slots = this.slots();
4471
+ if (!slots && !inBounds(parsed)) {
4472
+ // The PM bias yields if it pushed the time out of bounds.
4473
+ const unbiased = parseTimeText(trimmed, false);
4474
+ if (unbiased && inBounds(unbiased))
4475
+ parsed = unbiased;
4476
+ else {
4477
+ this.refuse(trimmed, 'out-of-range');
4478
+ return false;
4479
+ }
4480
+ }
4481
+ if (slots && !slots.includes(parsed)) {
4482
+ // Announce the formatted time — "5:00 PM isn't available."
4483
+ this.refuse(trimmed, 'unavailable', this.formatValue(parsed));
4484
+ return false;
4485
+ }
4486
+ this.setValue(parsed);
4487
+ return true;
4488
+ }
4489
+ /** Step a committed value ±minuteStep (±1 slot when pinned), clamped. */
4490
+ stepValue(direction) {
4491
+ const committed = this.value();
4492
+ if (!committed)
4493
+ return;
4494
+ const slots = this.slots();
4495
+ if (slots?.length) {
4496
+ const sorted = [...slots].sort();
4497
+ const index = sorted.indexOf(committed);
4498
+ const next = index >= 0
4499
+ ? sorted[index + direction]
4500
+ : direction > 0
4501
+ ? sorted.find((slot) => slot > committed)
4502
+ : [...sorted].reverse().find((slot) => slot < committed);
4503
+ if (next)
4504
+ this.setValue(next);
4505
+ return;
4506
+ }
4507
+ const step = this.minuteStep();
4508
+ const current = toMinutes(committed);
4509
+ const snapped = direction > 0
4510
+ ? Math.floor(current / step) * step + step
4511
+ : Math.ceil(current / step) * step - step;
4512
+ if (snapped < 0 || snapped >= 24 * 60)
4513
+ return;
4514
+ const candidate = fromMinutes(snapped);
4515
+ const min = this.minTime();
4516
+ const max = this.maxTime();
4517
+ if ((min && candidate < min) || (max && candidate > max))
4518
+ return; // fence
4519
+ this.setValue(candidate);
4520
+ }
4521
+ // --- Keyboard ----------------------------------------------------------------
4522
+ onInputKeydown(event) {
4523
+ const element = this.inputRef().nativeElement;
4524
+ switch (event.key) {
4525
+ case 'ArrowDown':
4526
+ case 'ArrowUp': {
4527
+ event.preventDefault();
4528
+ const direction = event.key === 'ArrowDown' ? 1 : -1;
4529
+ // A committed value with the list closed steps like a spinner —
4530
+ // ArrowUp means later, like the date field's ArrowUp means tomorrow —
4531
+ // while the list opens from an empty or edited field (or the toggle).
4532
+ if (!this.list.open() && this.value() && element.value === this.displayText()) {
4533
+ this.stepValue(direction === 1 ? -1 : 1);
4534
+ break;
4535
+ }
4536
+ if (!this.list.open()) {
4537
+ this.openList();
4538
+ if (this.list.activeIndex() < 0)
4539
+ this.list.setActive(direction === 1 ? 0 : this.options().length - 1);
4540
+ }
4541
+ else {
4542
+ const count = this.options().length;
4543
+ this.list.setActive((this.list.activeIndex() + direction + count) % count);
4544
+ }
4545
+ this.scrollToActive();
4546
+ break;
4547
+ }
4548
+ case 'Enter': {
4549
+ event.preventDefault();
4550
+ const active = this.list.activeIndex();
4551
+ if (this.list.open() && active >= 0)
4552
+ this.setValue(this.options()[active]);
4553
+ else
4554
+ this.commit(element.value);
4555
+ this.list.hide();
4556
+ break;
4557
+ }
4558
+ case 'Escape':
4559
+ if (this.list.open()) {
4560
+ this.list.hide();
4561
+ }
4562
+ else {
4563
+ this.setFieldText(this.displayText());
4564
+ this.draftInvalid.set(false);
4565
+ }
4566
+ break;
4567
+ case 'Tab':
4568
+ // Never trap: commit what is typed, then let focus move on.
4569
+ if (element.value.trim() && element.value !== this.displayText())
4570
+ this.commit(element.value);
4571
+ this.list.hide();
4572
+ break;
4573
+ }
4574
+ }
4575
+ onInput() {
4576
+ this.draftInvalid.set(false);
4577
+ // Typing never selects an option — Enter must commit the draft, not
4578
+ // whatever happens to sit nearest; the list only scrolls alongside.
4579
+ this.list.show();
4580
+ this.list.setActive(-1);
4581
+ const parsed = parseTimeText(this.inputRef().nativeElement.value, this.resolvedHour12());
4582
+ if (parsed) {
4583
+ const options = this.options();
4584
+ let nearest = options.findIndex((time) => time >= parsed);
4585
+ if (nearest < 0)
4586
+ nearest = options.length - 1;
4587
+ this.scrollToIndex(nearest);
4588
+ }
4589
+ }
4590
+ onToggle() {
4591
+ if (this.list.open()) {
4592
+ this.list.hide();
4593
+ }
4594
+ else {
4595
+ this.openList();
4596
+ this.inputRef()?.nativeElement.focus();
4597
+ }
4598
+ }
4599
+ selectOption(time) {
4600
+ this.setValue(time);
4601
+ this.list.hide();
4602
+ this.inputRef()?.nativeElement.focus();
4603
+ }
4604
+ onFocusOut(event) {
4605
+ const next = event.relatedTarget;
4606
+ if (next && this.host.nativeElement.contains(next))
4607
+ return;
4608
+ this.list.hide();
4609
+ this.touched.set(true);
4610
+ const element = this.inputRef()?.nativeElement;
4611
+ if (element && this.commitOnBlur() && element.value !== this.displayText())
4612
+ this.commit(element.value);
4613
+ }
4614
+ // --- Internals -------------------------------------------------------------------
4615
+ openList() {
4616
+ this.list.show();
4617
+ const committed = this.value();
4618
+ if (committed)
4619
+ this.list.setActive(this.options().indexOf(committed));
4620
+ this.scrollToActive();
4621
+ }
4622
+ scrollToActive() {
4623
+ this.scrollToIndex(this.list.activeIndex());
4624
+ }
4625
+ scrollToIndex(index) {
4626
+ if (index < 0)
4627
+ return;
4628
+ queueMicrotask(() => this.listRef()?.nativeElement.children[index]?.scrollIntoView?.({ block: 'nearest' }));
4629
+ }
4630
+ setFieldText(text) {
4631
+ const element = this.inputRef()?.nativeElement;
4632
+ if (element)
4633
+ element.value = text;
4634
+ }
4635
+ announce(message) {
4636
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
4637
+ }
4638
+ // --- Styling -----------------------------------------------------------------------
4639
+ className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4640
+ rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
4641
+ inputClass = computed(() => {
4642
+ const colors = this.theme.colorPalette();
4643
+ return css([
4644
+ {
4645
+ flex: 1,
4646
+ minWidth: 0,
4647
+ border: 0,
4648
+ outline: 'none',
4649
+ background: 'transparent',
4650
+ color: 'inherit',
4651
+ font: 'inherit',
4652
+ },
4653
+ this.draftInvalid() && {
4654
+ color: colors['warn'],
4655
+ // Shape and colour, not colour alone (WCAG 1.4.1).
4656
+ textDecoration: `underline dashed ${colors['warn']} 1.5px`,
4657
+ textUnderlineOffset: 3,
4658
+ },
4659
+ ]);
4660
+ }, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
4661
+ toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
4662
+ /** Embedded mode: the composer owns the box; keep only the flex row. */
4663
+ embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4664
+ listClass = computed(() => {
4665
+ const options = this.componentOptions();
4666
+ return css({
4667
+ position: 'absolute',
4668
+ top: '100%',
4669
+ left: 0,
4670
+ right: 0,
4671
+ zIndex: 20,
4672
+ margin: '4px 0 0',
4673
+ padding: 4,
4674
+ listStyle: 'none',
4675
+ maxHeight: (options.maxVisibleOptions ?? 7) * 36,
4676
+ overflowY: 'auto',
4677
+ ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
4678
+ ...this.theme.boxShadow(options.listShadow ?? 'menu'),
4679
+ ...this.theme.radius(options.listBorderRadius ?? 'xs'),
4680
+ '& [role="option"]': {
4681
+ padding: '8px 12px',
4682
+ cursor: 'pointer',
4683
+ ...this.theme.typeface('label'),
4684
+ ...this.theme.color('on-primary-surface'),
4685
+ ...this.theme.radius('xxs'),
4686
+ '&.active, &:hover': {
4687
+ ...this.theme.backgroundColor('primary-container'),
4688
+ ...this.theme.color('on-primary-container'),
4689
+ },
4690
+ },
4691
+ });
4692
+ }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4693
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4694
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTimeInputComponent, isStandalone: true, selector: "uni-time-input, TimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, minTime: { classPropertyName: "minTime", publicName: "minTime", isSignal: true, isRequired: false, transformFunction: null }, maxTime: { classPropertyName: "maxTime", publicName: "maxTime", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4695
+ }
4696
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, decorators: [{
4697
+ type: Component,
4698
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-time-input, TimeInput', imports: [NgTemplateOutlet, UniIconButtonComponent, UniInputBoxComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
4699
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], minTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTime", required: false }] }], maxTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxTime", required: false }] }], slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }], hour12: [{ type: i0.Input, args: [{ isSignal: true, alias: "hour12", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], embedded: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedded", required: false }] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ['listbox', { isSignal: true }] }] } });
4700
+
4701
+ /**
4702
+ * One field for a date and a time: a thin composer seating a uni-date-input
4703
+ * and a uni-time-input in one input-box chrome under one label, yielding one
4704
+ * combined `'YYYY-MM-DDTHH:mm'` value. The value emits only when both parts
4705
+ * are set — a time without a day is not an answer — and clearing the date
4706
+ * clears it. With `slotsFor`, the time part stays disabled until a day is
4707
+ * chosen and offers exactly that day's slots: the scheduling flow in one
4708
+ * attribute. Two honest tab stops (it is two questions); apps needing a
4709
+ * different arrangement compose the primitives directly.
4710
+ */
4711
+ class UniDateTimeInputComponent extends BaseComponent {
4712
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4713
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
4714
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4715
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4716
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4717
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4718
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4719
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4720
+ // --- Configuration -------------------------------------------------------
4721
+ /** Names the group; the parts are announced as "Date" and "Time" under it. */
4722
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4723
+ /** Earliest allowed moment; the date and time fences are split from it. */
4724
+ minDateTime = input(...(ngDevMode ? [undefined, { debugName: "minDateTime" }] : /* istanbul ignore next */ []));
4725
+ /** Latest allowed moment; the date and time fences are split from it. */
4726
+ maxDateTime = input(...(ngDevMode ? [undefined, { debugName: "maxDateTime" }] : /* istanbul ignore next */ []));
4727
+ // --- Forwarded wholesale ----------------------------------------------------
4728
+ disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
4729
+ markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
4730
+ /** Fixed time choices; superseded per-day by `slotsFor` when that is set. */
4731
+ slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
4732
+ minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
4733
+ hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
4734
+ weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
4735
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
4736
+ /** Scheduling: the day's available times. Gates the time part on a date. */
4737
+ slotsFor = input(...(ngDevMode ? [undefined, { debugName: "slotsFor" }] : /* istanbul ignore next */ []));
4738
+ host = inject(ElementRef);
4739
+ /**
4740
+ * The two part-values. An external `value` write re-derives both; an
4741
+ * internal partial state (a date without a time round-trips through
4742
+ * `undefined`) must not be wiped by its own echo.
4743
+ */
4744
+ parts = linkedSignal({ ...(ngDevMode ? { debugName: "parts" } : /* istanbul ignore next */ {}), source: this.value,
4745
+ computation: (value, previous) => {
4746
+ if (value)
4747
+ return splitDateTime(value);
4748
+ const kept = previous?.value;
4749
+ if (kept && joinDateTime(kept.date, kept.time) === undefined)
4750
+ return kept;
4751
+ return {};
4752
+ } });
4753
+ dateValue = computed(() => this.parts().date, ...(ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []));
4754
+ timeValue = computed(() => this.parts().time, ...(ngDevMode ? [{ debugName: "timeValue" }] : /* istanbul ignore next */ []));
4755
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4756
+ minParts = computed(() => splitDateTime(this.minDateTime()), ...(ngDevMode ? [{ debugName: "minParts" }] : /* istanbul ignore next */ []));
4757
+ maxParts = computed(() => splitDateTime(this.maxDateTime()), ...(ngDevMode ? [{ debugName: "maxParts" }] : /* istanbul ignore next */ []));
4758
+ dateMin = computed(() => this.minParts().date, ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
4759
+ dateMax = computed(() => this.maxParts().date, ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
4760
+ // The time fence applies only on the boundary date itself — 'after 9:00'
4761
+ // on the min date, any time on later days.
4762
+ timeMin = computed(() => {
4763
+ const { date, time } = this.minParts();
4764
+ return time && date && this.dateValue() === date ? time : undefined;
4765
+ }, ...(ngDevMode ? [{ debugName: "timeMin" }] : /* istanbul ignore next */ []));
4766
+ timeMax = computed(() => {
4767
+ const { date, time } = this.maxParts();
4768
+ return time && date && this.dateValue() === date ? time : undefined;
4769
+ }, ...(ngDevMode ? [{ debugName: "timeMax" }] : /* istanbul ignore next */ []));
4770
+ /** The chosen day's slots when `slotsFor` is set, else the fixed list. */
4771
+ effectiveSlots = computed(() => {
4772
+ const slotsFor = this.slotsFor();
4773
+ if (!slotsFor)
4774
+ return this.slots();
4775
+ const date = this.dateValue();
4776
+ return date ? slotsFor(date) : [];
4777
+ }, ...(ngDevMode ? [{ debugName: "effectiveSlots" }] : /* istanbul ignore next */ []));
4778
+ timeDisabled = computed(() => this.disabled() || (!!this.slotsFor() && !this.dateValue()), ...(ngDevMode ? [{ debugName: "timeDisabled" }] : /* istanbul ignore next */ []));
4779
+ // --- Part plumbing -----------------------------------------------------------
4780
+ onDatePartChange(date) {
4781
+ let time = this.parts().time;
4782
+ if (!date) {
4783
+ time = undefined; // clearing the date clears the combined value
4784
+ }
4785
+ else {
4786
+ const slotsFor = this.slotsFor();
4787
+ // Changing the day clears a slot that no longer exists.
4788
+ if (slotsFor && time && !slotsFor(date).includes(time))
4789
+ time = undefined;
4790
+ }
4791
+ this.setParts({ date, time });
4792
+ }
4793
+ onTimePartChange(time) {
4794
+ this.setParts({ date: this.parts().date, time });
4795
+ }
4796
+ setParts(parts) {
4797
+ this.parts.set(parts);
4798
+ this.value.set(joinDateTime(parts.date, parts.time));
4799
+ }
4800
+ onHostFocusOut(event) {
4801
+ const next = event.relatedTarget;
4802
+ if (!next || !this.host.nativeElement.contains(next))
4803
+ this.touched.set(true);
4804
+ }
4805
+ // --- Styling --------------------------------------------------------------------
4806
+ className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4807
+ groupClass = computed(() => css({
4808
+ display: 'flex',
4809
+ alignItems: 'stretch',
4810
+ flex: 1,
4811
+ width: '100%',
4812
+ minWidth: 0,
4813
+ ...this.theme.gap(this.componentOptions().partGap ?? 'sm'),
4814
+ }), ...(ngDevMode ? [{ debugName: "groupClass" }] : /* istanbul ignore next */ []));
4815
+ datePartClass = computed(() => css({ flex: 1.4, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "datePartClass" }] : /* istanbul ignore next */ []));
4816
+ timePartClass = computed(() => css({ flex: 1, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "timePartClass" }] : /* istanbul ignore next */ []));
4817
+ dividerClass = computed(() => {
4818
+ const colors = this.theme.colorPalette();
4819
+ return css({
4820
+ width: 1,
4821
+ alignSelf: 'stretch',
4822
+ flex: 'none',
4823
+ backgroundColor: colors[this.componentOptions().dividerColor ?? 'outline'],
4824
+ });
4825
+ }, ...(ngDevMode ? [{ debugName: "dividerClass" }] : /* istanbul ignore next */ []));
4826
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4827
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDateTimeInputComponent, isStandalone: true, selector: "uni-date-time-input, DateTimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, minDateTime: { classPropertyName: "minDateTime", publicName: "minDateTime", isSignal: true, isRequired: false, transformFunction: null }, maxDateTime: { classPropertyName: "maxDateTime", publicName: "maxDateTime", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, slotsFor: { classPropertyName: "slotsFor", publicName: "slotsFor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { listeners: { "focusout": "onHostFocusOut($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateTimeInput' }], usesInheritance: true, ngImport: i0, template: "<!-- One field chrome for both parts; the parts render embedded (no box of\n their own), so error/disabled/focus states stay consistent with every\n other field. Tab order: date, then time \u2014 two honest tab stops. -->\n<uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div role=\"group\" [attr.aria-label]=\"label()\" [class]=\"groupClass()\">\n <div [class]=\"datePartClass()\">\n <uni-date-input\n [embedded]=\"true\"\n label=\"Date\"\n [value]=\"dateValue()\"\n [minDate]=\"dateMin()\"\n [maxDate]=\"dateMax()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n [disabled]=\"disabled()\"\n [ariaDescribedBy]=\"ariaDescribedBy()\"\n (valueChange)=\"onDatePartChange($event)\"\n />\n </div>\n <div [class]=\"dividerClass()\"></div>\n <div [class]=\"timePartClass()\">\n <uni-time-input\n [embedded]=\"true\"\n label=\"Time\"\n [value]=\"timeValue()\"\n [minTime]=\"timeMin()\"\n [maxTime]=\"timeMax()\"\n [slots]=\"effectiveSlots()\"\n [minuteStep]=\"minuteStep()\"\n [hour12]=\"hour12()\"\n [locale]=\"locale()\"\n [disabled]=\"timeDisabled()\"\n (valueChange)=\"onTimePartChange($event)\"\n />\n </div>\n </div>\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniDateInputComponent, selector: "uni-date-input, DateInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "displayFormat", "locale", "commitOnBlur", "parse", "embedded", "minDate", "maxDate", "disabledDates", "markers", "weekStart"], outputs: ["valueChange", "touchedChange", "opened", "closed", "rejected"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniTimeInputComponent, selector: "uni-time-input, TimeInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "minuteStep", "minTime", "maxTime", "slots", "hour12", "locale", "commitOnBlur", "embedded"], outputs: ["valueChange", "touchedChange", "rejected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4828
+ }
4829
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, decorators: [{
4830
+ type: Component,
4831
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-date-time-input, DateTimeInput', imports: [UniDateInputComponent, UniInputBoxComponent, UniTimeInputComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'dateTimeInput' }], host: { '[class]': 'className()', '(focusout)': 'onHostFocusOut($event)' }, template: "<!-- One field chrome for both parts; the parts render embedded (no box of\n their own), so error/disabled/focus states stay consistent with every\n other field. Tab order: date, then time \u2014 two honest tab stops. -->\n<uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div role=\"group\" [attr.aria-label]=\"label()\" [class]=\"groupClass()\">\n <div [class]=\"datePartClass()\">\n <uni-date-input\n [embedded]=\"true\"\n label=\"Date\"\n [value]=\"dateValue()\"\n [minDate]=\"dateMin()\"\n [maxDate]=\"dateMax()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n [disabled]=\"disabled()\"\n [ariaDescribedBy]=\"ariaDescribedBy()\"\n (valueChange)=\"onDatePartChange($event)\"\n />\n </div>\n <div [class]=\"dividerClass()\"></div>\n <div [class]=\"timePartClass()\">\n <uni-time-input\n [embedded]=\"true\"\n label=\"Time\"\n [value]=\"timeValue()\"\n [minTime]=\"timeMin()\"\n [maxTime]=\"timeMax()\"\n [slots]=\"effectiveSlots()\"\n [minuteStep]=\"minuteStep()\"\n [hour12]=\"hour12()\"\n [locale]=\"locale()\"\n [disabled]=\"timeDisabled()\"\n (valueChange)=\"onTimePartChange($event)\"\n />\n </div>\n </div>\n</uni-input-box>\n" }]
4832
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], minDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDateTime", required: false }] }], maxDateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDateTime", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], slots: [{ type: i0.Input, args: [{ isSignal: true, alias: "slots", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], hour12: [{ type: i0.Input, args: [{ isSignal: true, alias: "hour12", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], slotsFor: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotsFor", required: false }] }] } });
4833
+
3080
4834
  class UniDialogComponent extends BaseComponent {
3081
4835
  elem = inject(ElementRef);
3082
4836
  /** Two-way bindable open state: [(show)]. */
@@ -3482,203 +5236,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3482
5236
  }]
3483
5237
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], contentTemplate: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }], overlay: [{ type: i0.ViewChild, args: ['overlay', { isSignal: true }] }] } });
3484
5238
 
3485
- class UniDropdownComponent extends BaseComponent {
3486
- renderer = inject(Renderer2);
3487
- delay = 100;
3488
- // Reactively track visibility status using Signals
3489
- showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
3490
- trigger = input.required(...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
3491
- placement = input('bottom-start', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
3492
- offset = input({ mainAxis: 4, alignmentAxis: 12 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
3493
- /** CSS anchor-name linking the trigger to the popover panel. */
3494
- anchorName = newAnchorName();
3495
- /**
3496
- * Value for aria-haspopup on the trigger, describing what the popover
3497
- * contains (e.g. 'menu' for Menu, 'dialog' for rich content). When unset,
3498
- * only aria-expanded/aria-controls are managed.
3499
- */
3500
- ariaHasPopup = input(null, ...(ngDevMode ? [{ debugName: "ariaHasPopup" }] : /* istanbul ignore next */ []));
3501
- /** Document-unique id of the popover element, for aria-controls wiring. */
3502
- popoverId = uniqueId('uni-dropdown');
3503
- paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
3504
- paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
3505
- // Per-instance panel-chrome overrides; undefined falls back to the theme's
3506
- // `dropdown` options, so hosts like uni-menu can restyle their panel
3507
- // without forking the shared dropdown entry.
3508
- border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
3509
- borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
3510
- shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
3511
- color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
3512
- dropdownShowing = output();
3513
- dropdownHiding = output();
3514
- dropdownRef;
3515
- get _trigger() {
3516
- return this.trigger();
3517
- }
3518
- get _dropdown() {
3519
- return this.dropdownRef.nativeElement;
3520
- }
3521
- transformOriginMap = {
3522
- top: 'bottom center',
3523
- right: 'center left',
3524
- bottom: 'top center',
3525
- left: 'center right',
3526
- 'top-start': 'bottom left',
3527
- 'top-end': 'bottom right',
3528
- 'right-start': 'top left',
3529
- 'right-end': 'bottom left',
3530
- 'bottom-start': 'top left',
3531
- 'bottom-end': 'top right',
3532
- 'left-start': 'top right',
3533
- 'left-end': 'bottom right',
3534
- };
3535
- dropdownClass = computed(() => {
3536
- const currentPlacement = this.placement();
3537
- return css([
3538
- {
3539
- // Reset browser agent default popover styles
3540
- border: 'none',
3541
- background: 'transparent',
3542
- padding: 0,
3543
- overflow: 'visible',
3544
- width: 'max-content',
3545
- // Native anchor positioning: the browser keeps the panel attached to
3546
- // the trigger (no scroll/resize listeners needed)
3547
- ...anchorStyles(this.anchorName, currentPlacement, this.offset()),
3548
- // 2. Animate discrete properties across top layer layout contexts
3549
- transitionProperty: 'transform, opacity, display, overlay',
3550
- transitionDuration: `${this.delay}ms`,
3551
- transitionTimingFunction: 'linear',
3552
- transitionBehavior: 'allow-discrete',
3553
- // Hidden State (Closed)
3554
- opacity: 0,
3555
- transform: 'scale(0.8)',
3556
- transformOrigin: this.transformOriginMap[currentPlacement],
3557
- // 3. Active state styling controlled via the native browser pseudo-class
3558
- ['&:popover-open']: {
3559
- opacity: 1,
3560
- transform: 'scale(1)',
3561
- },
3562
- // 4. Starting-style rules what properties animate *from* when transitioning in
3563
- ['@starting-style']: {
3564
- ['&:popover-open']: {
3565
- opacity: 0,
3566
- transform: 'scale(0.8)',
3567
- },
3568
- },
3569
- },
3570
- ]);
3571
- }, ...(ngDevMode ? [{ debugName: "dropdownClass" }] : /* istanbul ignore next */ []));
3572
- /** The element that receives focus and carries the ARIA popup state. */
3573
- get _focusTarget() {
3574
- return resolveFocusTarget(this._trigger);
3575
- }
3576
- ngOnInit() {
3577
- // Single native click binding to manage open/close commands
3578
- this.renderer.listen(this._trigger, 'click', (e) => {
3579
- e.stopPropagation();
3580
- this.toggleDropdown();
3581
- });
3582
- // Anchor the popover panel to the trigger element
3583
- this.renderer.setStyle(this._trigger, 'anchor-name', this.anchorName);
3584
- // Wire the ARIA popup contract onto the focusable trigger element
3585
- const focusTarget = this._focusTarget;
3586
- this.renderer.setAttribute(focusTarget, 'aria-expanded', 'false');
3587
- this.renderer.setAttribute(focusTarget, 'aria-controls', this.popoverId);
3588
- if (this.ariaHasPopup()) {
3589
- this.renderer.setAttribute(focusTarget, 'aria-haspopup', this.ariaHasPopup());
3590
- }
3591
- // Sync state if user invokes light-dismiss via outside click or Escape key
3592
- this.renderer.listen(this._dropdown, 'toggle', (event) => {
3593
- const isOpened = event.newState === 'open';
3594
- this.showing.set(isOpened);
3595
- this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
3596
- if (isOpened) {
3597
- this.dropdownShowing.emit(true);
3598
- }
3599
- else {
3600
- this.dropdownHiding.emit(true);
3601
- this.restoreFocus();
3602
- }
3603
- });
3604
- }
3605
- /**
3606
- * Returns focus to the trigger when the popover closes while focus was
3607
- * inside it (or was dropped on <body> by the top layer closing), so
3608
- * keyboard users are never stranded (WCAG 2.4.3).
3609
- */
3610
- restoreFocus() {
3611
- const active = document.activeElement;
3612
- if (active === document.body || (active && this._dropdown.contains(active))) {
3613
- this._focusTarget.focus();
3614
- }
3615
- }
3616
- toggleDropdown() {
3617
- if (this.showing()) {
3618
- this._dropdown.hidePopover();
3619
- }
3620
- else {
3621
- this._dropdown.showPopover();
3622
- }
3623
- }
3624
- hideDropdown() {
3625
- this._dropdown.hidePopover();
3626
- }
3627
- ngOnDestroy() {
3628
- try {
3629
- this._dropdown.hidePopover();
3630
- }
3631
- catch {
3632
- // popover was already closed or detached
3633
- }
3634
- }
3635
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3636
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDropdownComponent, isStandalone: true, selector: "uni-dropdown", inputs: { trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, ariaHasPopup: { classPropertyName: "ariaHasPopup", publicName: "ariaHasPopup", isSignal: true, isRequired: false, transformFunction: null }, paddingVertical: { classPropertyName: "paddingVertical", publicName: "paddingVertical", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null }, border: { classPropertyName: "border", publicName: "border", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dropdownShowing: "dropdownShowing", dropdownHiding: "dropdownHiding" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }], viewQueries: [{ propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
3637
- <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
3638
- <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3639
- <div
3640
- box-layout
3641
- [border]="border() ?? componentOptions().border"
3642
- [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
3643
- [paddingVertical]="paddingVertical()"
3644
- [paddingHorizontal]="paddingHorizontal()"
3645
- [color]="color() ?? componentOptions().color"
3646
- [shadow]="shadow() ?? componentOptions().shadow"
3647
- >
3648
- <ng-content></ng-content>
3649
- </div>
3650
- </div>
3651
- `, isInline: true, dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3652
- }
3653
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, decorators: [{
3654
- type: Component,
3655
- args: [{
3656
- changeDetection: ChangeDetectionStrategy.OnPush,
3657
- selector: 'uni-dropdown',
3658
- imports: [UniBoxComponent],
3659
- template: `
3660
- <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
3661
- <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3662
- <div
3663
- box-layout
3664
- [border]="border() ?? componentOptions().border"
3665
- [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
3666
- [paddingVertical]="paddingVertical()"
3667
- [paddingHorizontal]="paddingHorizontal()"
3668
- [color]="color() ?? componentOptions().color"
3669
- [shadow]="shadow() ?? componentOptions().shadow"
3670
- >
3671
- <ng-content></ng-content>
3672
- </div>
3673
- </div>
3674
- `,
3675
- providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
3676
- }]
3677
- }], propDecorators: { trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaHasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaHasPopup", required: false }] }], paddingVertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingVertical", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], border: [{ type: i0.Input, args: [{ isSignal: true, alias: "border", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], shadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shadow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dropdownShowing: [{ type: i0.Output, args: ["dropdownShowing"] }], dropdownHiding: [{ type: i0.Output, args: ["dropdownHiding"] }], dropdownRef: [{
3678
- type: ViewChild,
3679
- args: ['dropdown', { static: true }]
3680
- }] } });
3681
-
3682
5239
  class UniExpandComponent extends BaseComponent {
3683
5240
  collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
3684
5241
  /**
@@ -4169,57 +5726,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4169
5726
  * This file exports all public-facing elements of the file-drop-zone component.
4170
5727
  */
4171
5728
 
4172
- class UniInputBoxComponent extends BaseComponent {
4173
- className = css({ display: 'contents' });
4174
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4175
- error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
4176
- minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
4177
- /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
4178
- height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4179
- color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4180
- border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
4181
- shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4182
- inputBoxClass = computed(() => css([
4183
- this.disabled() && {
4184
- ...this.theme.color(this.componentOptions().disabledTextColor),
4185
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4186
- cursor: 'not-allowed !important',
4187
- },
4188
- {
4189
- '& input, select, textarea': {
4190
- ...removeInputPlatformStyling,
4191
- height: '100%',
4192
- ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4193
- ...this.theme.color(this.componentOptions().textColor),
4194
- ...this.theme.typeface(this.componentOptions().typeFace),
4195
- },
4196
- // Multi-line fields size themselves (rows/resize), not from the box.
4197
- '& textarea': {
4198
- height: 'auto',
4199
- ...this.theme.paddingTop('xs'),
4200
- ...this.theme.paddingBottom('xs'),
4201
- },
4202
- '&:has(input:disabled, select:disabled, textarea:disabled)': {
4203
- ...this.theme.color(this.componentOptions().disabledTextColor),
4204
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4205
- },
4206
- '& input:disabled, select:disabled, textarea:disabled': {
4207
- cursor: 'not-allowed !important',
4208
- },
4209
- '&:has(input:focus, select:focus, textarea:focus)': {
4210
- outline: this.componentOptions().focusOutline,
4211
- outlineOffset: this.componentOptions().focusOutlineOffset,
4212
- },
4213
- },
4214
- ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4215
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4216
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4217
- }
4218
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4219
- type: Component,
4220
- args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
4221
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4222
-
4223
5729
  /**
4224
5730
  * Text input that emits `change` only after the user pauses typing. Wears the
4225
5731
  * shared input chrome (themed color, border, typeface, focus ring) via
@@ -5533,6 +7039,13 @@ class UniRadioComponent extends BaseComponent {
5533
7039
  });
5534
7040
  radioOptionClass = computed(() => {
5535
7041
  const { outerCircleSize, innerCircleSize, innerCircleOffset } = this.metrics();
7042
+ // The dot's grow/retract is a token: 0.3s default, 0 = instant. The
7043
+ // transitions are scoped — never `all` — so the focus ring's outline and
7044
+ // shadow apply instantly instead of interpolating from a stale outline
7045
+ // color, which flashed a dark ring before the themed ring color landed.
7046
+ const speed = this.componentOptions().transitionSpeed ?? 0.3;
7047
+ const ringTransition = `border-color ${speed}s ease, background-color ${speed}s ease`;
7048
+ const dotTransition = `transform ${speed}s ease`;
5536
7049
  return css({
5537
7050
  userSelect: 'none',
5538
7051
  cursor: this.disabled() ? 'not-allowed' : 'pointer',
@@ -5548,7 +7061,7 @@ class UniRadioComponent extends BaseComponent {
5548
7061
  ? this.getThemeColor('on-disabled')
5549
7062
  : this.getThemeColor(this.componentOptions().ringColor ?? 'outline')}`,
5550
7063
  position: 'relative',
5551
- transition: 'all 0.3s ease',
7064
+ transition: ringTransition,
5552
7065
  backgroundColor: this.getThemeColor(this.componentOptions().fillColor ?? 'surface'),
5553
7066
  flexShrink: 0,
5554
7067
  },
@@ -5561,7 +7074,7 @@ class UniRadioComponent extends BaseComponent {
5561
7074
  top: innerCircleOffset,
5562
7075
  left: innerCircleOffset,
5563
7076
  transform: 'scale(0)',
5564
- transition: 'all 0.3s ease',
7077
+ transition: dotTransition,
5565
7078
  },
5566
7079
  '&:hover .radio-button': this.disabled()
5567
7080
  ? {}
@@ -5589,9 +7102,9 @@ class UniRadioComponent extends BaseComponent {
5589
7102
  '&:checked + .radio-button .radio-inner': {
5590
7103
  transform: 'scale(1)',
5591
7104
  },
7105
+ // The shared, themable focus indicator, keyed off the hidden input.
5592
7106
  '&:focus + .radio-button': {
5593
- outline: `2px solid ${this.getThemeColor(this.variant())}`,
5594
- outlineOffset: '2px',
7107
+ ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
5595
7108
  },
5596
7109
  }), ...(ngDevMode ? [{ debugName: "radioInputClass" }] : /* istanbul ignore next */ []));
5597
7110
  handleRadioChange(optionValue) {
@@ -5978,7 +7491,8 @@ class UniSliderComponent extends BaseComponent {
5978
7491
  '&::-moz-range-track': { height: trackHeight, borderRadius: radius, background: track },
5979
7492
  '&::-moz-range-progress': { height: trackHeight, borderRadius: radius, background: fill },
5980
7493
  '&::-moz-range-thumb': thumb,
5981
- '&:focus-visible': { outline: `2px solid ${fill}`, outlineOffset: 2 },
7494
+ // The shared, themable focus indicator, in the track's fill color.
7495
+ '&:focus-visible': { ...this.theme.focusRingStyle(fill) },
5982
7496
  '&:disabled': {
5983
7497
  cursor: 'not-allowed',
5984
7498
  opacity: 0.5,
@@ -8035,7 +9549,9 @@ class UniToggleComponent extends BaseComponent {
8035
9549
  : this.getThemeColor(this.componentOptions().trackColor ?? 'surface-variant'),
8036
9550
  borderRadius: toggleSize / 2,
8037
9551
  position: 'relative',
8038
- transition: 'all 0.3s ease',
9552
+ // Scoped, never `all`: the focus ring must apply instantly rather
9553
+ // than interpolating its outline color from a stale value.
9554
+ transition: 'background-color 0.3s ease, border-color 0.3s ease',
8039
9555
  },
8040
9556
  '& .toggle-slider': {
8041
9557
  width: sliderSize,
@@ -8045,7 +9561,7 @@ class UniToggleComponent extends BaseComponent {
8045
9561
  position: 'absolute',
8046
9562
  top: sliderOffset,
8047
9563
  left: sliderOffset,
8048
- transition: 'all 0.3s ease',
9564
+ transition: 'transform 0.3s ease, background-color 0.3s ease',
8049
9565
  ...this.theme.boxShadow('raised'),
8050
9566
  },
8051
9567
  // Hover darkens whatever the token resolves to — the button convention.
@@ -8074,9 +9590,9 @@ class UniToggleComponent extends BaseComponent {
8074
9590
  '&:disabled + .toggle-switch': {
8075
9591
  cursor: 'not-allowed',
8076
9592
  },
9593
+ // The shared, themable focus indicator, keyed off the hidden input.
8077
9594
  '&:focus + .toggle-switch': {
8078
- outline: `2px solid ${this.getThemeColor(this.variant())}`,
8079
- outlineOffset: '2px',
9595
+ ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
8080
9596
  },
8081
9597
  });
8082
9598
  }, ...(ngDevMode ? [{ debugName: "toggleInput" }] : /* istanbul ignore next */ []));
@@ -8298,5 +9814,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
8298
9814
  * Generated bundle index. Do not edit.
8299
9815
  */
8300
9816
 
8301
- export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, anchorArrowStyles, anchorStyles, createListboxNavigation, getFileExtension, isDivider, motionSafe, newAnchorName, resolveFocusTarget, uniqueId, useTimer, visuallyHidden };
9817
+ export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, createListboxNavigation, dayOfWeek, daysInMonth, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isValidDate, isoDate, joinDateTime, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveFocusTarget, splitDateTime, timeSlots, todayIso, uniqueId, useTimer, visuallyHidden, weekdayNames };
8302
9818
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map