@uni-design-system/uni-angular 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,710 @@ 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
+ /**
2689
+ * The month on screen: the `month` model, else the value's month, else
2690
+ * today's. Falsy guards on purpose: `''` — the only typeable empty for a
2691
+ * string-typed model — counts as unset, or it would reach the month math
2692
+ * and blow up the grid and heading.
2693
+ */
2694
+ viewMonth = computed(() => this.month() || monthOf(this.anchorDate() || todayIso()), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
2695
+ anchorDate = computed(() => {
2696
+ const value = this.value();
2697
+ return this.mode() === 'range'
2698
+ ? value?.start
2699
+ : value;
2700
+ }, ...(ngDevMode ? [{ debugName: "anchorDate" }] : /* istanbul ignore next */ []));
2701
+ /**
2702
+ * The roving-tabindex day. Re-derives when the view or value changes
2703
+ * (selected day in view → today in view → first enabled day); keyboard
2704
+ * navigation writes it directly.
2705
+ */
2706
+ focusedDate = linkedSignal(() => this.pickFocus(), ...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
2707
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
2708
+ heading = computed(() => formatMonthHeading(this.viewMonth(), this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
2709
+ weekdays = computed(() => weekdayNames(this.resolvedLocale(), this.resolvedWeekStart(), this.componentOptions().weekdayFormat ?? 'short'), ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
2710
+ disabledDateSet = computed(() => {
2711
+ const dates = this.disabledDates();
2712
+ return Array.isArray(dates) ? new Set(dates) : null;
2713
+ }, ...(ngDevMode ? [{ debugName: "disabledDateSet" }] : /* istanbul ignore next */ []));
2714
+ markersByDate = computed(() => {
2715
+ const map = new Map();
2716
+ for (const marker of this.markers()) {
2717
+ const existing = map.get(marker.date);
2718
+ if (existing)
2719
+ existing.push(marker);
2720
+ else
2721
+ map.set(marker.date, [marker]);
2722
+ }
2723
+ return map;
2724
+ }, ...(ngDevMode ? [{ debugName: "markersByDate" }] : /* istanbul ignore next */ []));
2725
+ /** The committed range, or the pending preview band. */
2726
+ rangeBand = computed(() => {
2727
+ if (this.mode() !== 'range')
2728
+ return null;
2729
+ const pending = this.pendingStart();
2730
+ const preview = this.previewDate();
2731
+ if (pending && preview) {
2732
+ const [start, end] = preview < pending ? [preview, pending] : [pending, preview];
2733
+ return { start, end, preview: true };
2734
+ }
2735
+ if (pending)
2736
+ return { start: pending, end: pending, preview: true };
2737
+ const value = this.value();
2738
+ if (value?.start && value?.end)
2739
+ return { start: value.start, end: value.end, preview: false };
2740
+ return null;
2741
+ }, ...(ngDevMode ? [{ debugName: "rangeBand" }] : /* istanbul ignore next */ []));
2742
+ isDayDisabled(date) {
2743
+ const min = this.minDate();
2744
+ const max = this.maxDate();
2745
+ if ((min && date < min) || (max && date > max))
2746
+ return true;
2747
+ const dates = this.disabledDates();
2748
+ if (!dates)
2749
+ return false;
2750
+ const set = this.disabledDateSet();
2751
+ return set ? set.has(date) : dates(date);
2752
+ }
2753
+ isSelected(date) {
2754
+ if (this.mode() === 'single')
2755
+ return this.value() === date;
2756
+ const pending = this.pendingStart();
2757
+ if (pending)
2758
+ return pending === date;
2759
+ const value = this.value();
2760
+ return value?.start === date || value?.end === date;
2761
+ }
2762
+ /** The full render model: one precomputed cell per grid position. */
2763
+ gridWeeks = computed(() => {
2764
+ const locale = this.resolvedLocale();
2765
+ const band = this.rangeBand();
2766
+ const today = todayIso();
2767
+ const focused = this.focusedDate();
2768
+ const markers = this.markersByDate();
2769
+ const cellBase = this.cellClass();
2770
+ const dayBase = this.dayClass();
2771
+ const todayClass = this.todayClass();
2772
+ return buildMonthGrid(this.viewMonth(), this.resolvedWeekStart()).map((week) => week.map((cell) => {
2773
+ const date = cell.date;
2774
+ const selected = !cell.outside && this.isSelected(date);
2775
+ const inBand = !cell.outside && !!band && date >= band.start && date <= band.end;
2776
+ const dayMarkers = cell.outside ? [] : (markers.get(date) ?? []).slice(0, 3);
2777
+ const markerLabel = dayMarkers
2778
+ .filter((marker) => marker.label)
2779
+ .map((marker) => marker.label)
2780
+ .join(', ');
2781
+ const isToday = date === today;
2782
+ return {
2783
+ date,
2784
+ day: Number(date.slice(8, 10)),
2785
+ outside: cell.outside,
2786
+ disabled: this.isDayDisabled(date),
2787
+ today: isToday,
2788
+ selected,
2789
+ inBand,
2790
+ tabIndex: date === focused && !cell.outside ? 0 : -1,
2791
+ ariaLabel: formatDate(date, locale, { dateStyle: 'full' }) + (markerLabel ? `, ${markerLabel}` : ''),
2792
+ markers: dayMarkers,
2793
+ cellClass: [
2794
+ cellBase,
2795
+ inBand && (band.preview ? this.previewClass() : this.inRangeClass()),
2796
+ inBand && date === band.start && this.bandStartClass(),
2797
+ inBand && date === band.end && this.bandEndClass(),
2798
+ ]
2799
+ .filter(Boolean)
2800
+ .join(' '),
2801
+ dayClass: [dayBase, isToday && todayClass, selected && this.selectedClass()]
2802
+ .filter(Boolean)
2803
+ .join(' '),
2804
+ };
2805
+ }));
2806
+ }, ...(ngDevMode ? [{ debugName: "gridWeeks" }] : /* istanbul ignore next */ []));
2807
+ // --- Selection -------------------------------------------------------------
2808
+ select(date) {
2809
+ if (this.disabled() || this.isDayDisabled(date))
2810
+ return;
2811
+ this.setViewMonth(monthOf(date));
2812
+ this.focusedDate.set(date);
2813
+ const locale = this.resolvedLocale();
2814
+ const full = (d) => formatDate(d, locale, { dateStyle: 'full' });
2815
+ if (this.mode() === 'single') {
2816
+ this.value.set(date);
2817
+ this.announce(`${full(date)} selected.`);
2818
+ }
2819
+ else if (!this.pendingStart()) {
2820
+ this.pendingStart.set(date);
2821
+ this.previewDate.set(date);
2822
+ this.announce(`Start date ${full(date)}. Choose an end date.`);
2823
+ }
2824
+ else {
2825
+ let [start, end] = [this.pendingStart(), date];
2826
+ if (end < start)
2827
+ [start, end] = [end, start]; // backwards commit swaps
2828
+ this.pendingStart.set(null);
2829
+ this.previewDate.set(null);
2830
+ this.value.set({ start, end });
2831
+ const days = inclusiveDayCount(start, end);
2832
+ this.announce(`Range selected, ${full(start)} to ${full(end)}. ${days} ${days === 1 ? 'day' : 'days'}.`);
2833
+ }
2834
+ this.selected.emit(date);
2835
+ }
2836
+ cancelPending() {
2837
+ this.pendingStart.set(null);
2838
+ this.previewDate.set(null);
2839
+ this.announce('Range selection cancelled.');
2840
+ }
2841
+ // --- Navigation ------------------------------------------------------------
2842
+ onNav(direction) {
2843
+ this.setViewMonth(monthOf(addMonths(`${this.viewMonth()}-01`, direction)));
2844
+ }
2845
+ setViewMonth(month) {
2846
+ if (this.month() !== month)
2847
+ this.month.set(month);
2848
+ }
2849
+ /**
2850
+ * Move the roving focus. A landing on a disabled day keeps going in the
2851
+ * same direction until an enabled day; the min/max fence stops the caret,
2852
+ * it never wraps. Month edges never block — the grid follows.
2853
+ */
2854
+ moveFocus(target, direction) {
2855
+ const min = this.minDate();
2856
+ const max = this.maxDate();
2857
+ if ((min && target < min) || (max && target > max))
2858
+ return;
2859
+ let date = target;
2860
+ let guard = 0;
2861
+ while (this.isDayDisabled(date)) {
2862
+ date = addDays(date, direction);
2863
+ if ((min && date < min) || (max && date > max) || ++guard > 500)
2864
+ return;
2865
+ }
2866
+ this.setViewMonth(monthOf(date));
2867
+ this.focusedDate.set(date);
2868
+ if (this.pendingStart())
2869
+ this.previewDate.set(date);
2870
+ this.focusDay(date);
2871
+ }
2872
+ onGridKeydown(event) {
2873
+ const focused = this.focusedDate();
2874
+ const week = (dayOfWeek(focused) - this.resolvedWeekStart() + 7) % 7;
2875
+ const handlers = {
2876
+ ArrowLeft: () => this.moveFocus(addDays(focused, -1), -1),
2877
+ ArrowRight: () => this.moveFocus(addDays(focused, 1), 1),
2878
+ ArrowUp: () => this.moveFocus(addDays(focused, -7), -1),
2879
+ ArrowDown: () => this.moveFocus(addDays(focused, 7), 1),
2880
+ Home: () => this.moveFocus(addDays(focused, -week), 1),
2881
+ End: () => this.moveFocus(addDays(focused, 6 - week), -1),
2882
+ PageUp: () => this.moveFocus(addMonths(focused, event.shiftKey ? -12 : -1), -1),
2883
+ PageDown: () => this.moveFocus(addMonths(focused, event.shiftKey ? 12 : 1), 1),
2884
+ Enter: () => this.select(focused),
2885
+ ' ': () => this.select(focused),
2886
+ };
2887
+ if (event.key === 'Escape') {
2888
+ // Only a pending range consumes Escape; otherwise it bubbles so a
2889
+ // hosting popover can light-dismiss.
2890
+ if (this.pendingStart()) {
2891
+ event.preventDefault();
2892
+ event.stopPropagation();
2893
+ this.cancelPending();
2894
+ }
2895
+ return;
2896
+ }
2897
+ const handler = handlers[event.key];
2898
+ if (handler) {
2899
+ event.preventDefault();
2900
+ handler();
2901
+ }
2902
+ }
2903
+ onDayHover(date) {
2904
+ if (this.pendingStart() && !this.isDayDisabled(date))
2905
+ this.previewDate.set(date);
2906
+ }
2907
+ /** Focus the roving day — used by popup hosts when the calendar opens. */
2908
+ focusActiveDay() {
2909
+ this.focusDay(this.focusedDate());
2910
+ }
2911
+ onHostFocusOut(event) {
2912
+ const next = event.relatedTarget;
2913
+ if (!next || !this.host.nativeElement.contains(next))
2914
+ this.touched.set(true);
2915
+ }
2916
+ focusDay(date) {
2917
+ queueMicrotask(() => this.host.nativeElement.querySelector(`[data-date="${date}"]`)?.focus());
2918
+ }
2919
+ pickFocus() {
2920
+ const view = this.viewMonth();
2921
+ const anchor = this.anchorDate();
2922
+ if (anchor && monthOf(anchor) === view && !this.isDayDisabled(anchor))
2923
+ return anchor;
2924
+ const today = todayIso();
2925
+ if (monthOf(today) === view && !this.isDayDisabled(today))
2926
+ return today;
2927
+ return this.firstEnabledInView(view);
2928
+ }
2929
+ firstEnabledInView(view) {
2930
+ const first = `${view}-01`;
2931
+ let date = first;
2932
+ for (let i = 0; i < 31 && monthOf(date) === view; i++) {
2933
+ if (!this.isDayDisabled(date))
2934
+ return date;
2935
+ date = addDays(date, 1);
2936
+ }
2937
+ return first;
2938
+ }
2939
+ announce(message) {
2940
+ // Re-announce identical text by breaking the string equality.
2941
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
2942
+ }
2943
+ // --- Styling ---------------------------------------------------------------
2944
+ daySize = computed(() => (this.componentTheme().sizes?.[this.size()] ?? {}), ...(ngDevMode ? [{ debugName: "daySize" }] : /* istanbul ignore next */ []));
2945
+ className = computed(() => css([(this.componentTheme().fixed ?? { display: 'inline-block' })]), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
2946
+ navClass = computed(() => css({
2947
+ display: 'flex',
2948
+ alignItems: 'center',
2949
+ justifyContent: 'space-between',
2950
+ ...this.theme.gap('xs'),
2951
+ marginBottom: 4,
2952
+ }), ...(ngDevMode ? [{ debugName: "navClass" }] : /* istanbul ignore next */ []));
2953
+ headingClass = computed(() => css({ flex: 1, textAlign: 'center', ...this.theme.typeface('title-small') }), ...(ngDevMode ? [{ debugName: "headingClass" }] : /* istanbul ignore next */ []));
2954
+ gridClass = computed(() => css({ display: 'grid', ...this.theme.gap(this.componentOptions().gap ?? 'xxs') }), ...(ngDevMode ? [{ debugName: "gridClass" }] : /* istanbul ignore next */ []));
2955
+ rowClass = computed(() => css({
2956
+ display: 'grid',
2957
+ gridTemplateColumns: 'repeat(7, 1fr)',
2958
+ justifyItems: 'center',
2959
+ ...this.theme.gap(this.componentOptions().gap ?? 'xxs'),
2960
+ }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
2961
+ weekdayClass = computed(() => css({
2962
+ display: 'flex',
2963
+ alignItems: 'center',
2964
+ justifyContent: 'center',
2965
+ ...this.daySize(),
2966
+ height: 'auto',
2967
+ ...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
2968
+ ...this.theme.color('on-background-variant'),
2969
+ '& abbr': { textDecoration: 'none' },
2970
+ }), ...(ngDevMode ? [{ debugName: "weekdayClass" }] : /* istanbul ignore next */ []));
2971
+ /** The gridcell — range bands paint here so they read as one bar. */
2972
+ cellClass = computed(() => css({ position: 'relative', display: 'flex' }), ...(ngDevMode ? [{ debugName: "cellClass" }] : /* istanbul ignore next */ []));
2973
+ inRangeClass = computed(() => css({ ...this.theme.backgroundColor('primary-container'), borderRadius: 0 }), ...(ngDevMode ? [{ debugName: "inRangeClass" }] : /* istanbul ignore next */ []));
2974
+ previewClass = computed(() => {
2975
+ const colors = this.theme.colorPalette();
2976
+ return css({
2977
+ backgroundColor: `color-mix(in srgb, ${colors['primary-container']} 55%, transparent)`,
2978
+ outline: `1px dashed ${colors['primary']}`,
2979
+ outlineOffset: -1,
2980
+ });
2981
+ }, ...(ngDevMode ? [{ debugName: "previewClass" }] : /* istanbul ignore next */ []));
2982
+ bandStartClass = computed(() => css({ ...this.theme.getRadiusLeft(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandStartClass" }] : /* istanbul ignore next */ []));
2983
+ bandEndClass = computed(() => css({ ...this.theme.getRadiusRight(this.componentOptions().dayBorderRadius ?? 'max') }), ...(ngDevMode ? [{ debugName: "bandEndClass" }] : /* istanbul ignore next */ []));
2984
+ dayClass = computed(() => {
2985
+ const options = this.componentOptions();
2986
+ const colors = this.theme.colorPalette();
2987
+ return css({
2988
+ position: 'relative',
2989
+ display: 'flex',
2990
+ alignItems: 'center',
2991
+ justifyContent: 'center',
2992
+ border: 0,
2993
+ padding: 0,
2994
+ background: 'transparent',
2995
+ color: 'inherit',
2996
+ cursor: 'pointer',
2997
+ ...this.theme.typeface(options.typeface ?? 'label'),
2998
+ ...this.daySize(),
2999
+ ...this.theme.radius(options.dayBorderRadius ?? 'max'),
3000
+ '&:hover:not(:disabled)': { ...this.theme.colorPair('primary-container') },
3001
+ ...this.theme.focusRing(),
3002
+ '&:disabled': {
3003
+ color: colors['on-disabled'],
3004
+ cursor: 'default',
3005
+ pointerEvents: 'none',
3006
+ },
3007
+ });
3008
+ }, ...(ngDevMode ? [{ debugName: "dayClass" }] : /* istanbul ignore next */ []));
3009
+ todayClass = computed(() => {
3010
+ const colors = this.theme.colorPalette();
3011
+ // Outline (or dot), never fill — so today and selected can coincide and
3012
+ // both stay legible (WCAG 1.4.1: no state carried by colour alone).
3013
+ return (this.componentOptions().todayStyle ?? 'outline') === 'outline'
3014
+ ? css({ boxShadow: `inset 0 0 0 1.5px ${colors['primary']}` })
3015
+ : css({
3016
+ '&::after': {
3017
+ content: '""',
3018
+ position: 'absolute',
3019
+ bottom: 3,
3020
+ left: '50%',
3021
+ transform: 'translateX(-50%)',
3022
+ width: 4,
3023
+ height: 4,
3024
+ borderRadius: 999,
3025
+ backgroundColor: colors['primary'],
3026
+ },
3027
+ });
3028
+ }, ...(ngDevMode ? [{ debugName: "todayClass" }] : /* istanbul ignore next */ []));
3029
+ selectedClass = computed(() => {
3030
+ const colors = this.theme.colorPalette();
3031
+ return css({
3032
+ ...this.theme.colorPair('primary'),
3033
+ // A marker dot survives selection by switching to the on-colour.
3034
+ '& [data-dot]': { backgroundColor: colors['on-primary'] },
3035
+ });
3036
+ }, ...(ngDevMode ? [{ debugName: "selectedClass" }] : /* istanbul ignore next */ []));
3037
+ outsideDayClass = computed(() => {
3038
+ const colors = this.theme.colorPalette();
3039
+ return css({
3040
+ display: 'flex',
3041
+ alignItems: 'center',
3042
+ justifyContent: 'center',
3043
+ ...this.theme.typeface(this.componentOptions().typeface ?? 'label'),
3044
+ ...this.daySize(),
3045
+ color: colors['on-disabled'],
3046
+ });
3047
+ }, ...(ngDevMode ? [{ debugName: "outsideDayClass" }] : /* istanbul ignore next */ []));
3048
+ dotsClass = computed(() => css({
3049
+ position: 'absolute',
3050
+ bottom: 2,
3051
+ left: 0,
3052
+ right: 0,
3053
+ display: 'flex',
3054
+ justifyContent: 'center',
3055
+ gap: 2,
3056
+ pointerEvents: 'none',
3057
+ }), ...(ngDevMode ? [{ debugName: "dotsClass" }] : /* istanbul ignore next */ []));
3058
+ /** One dot class per marker variant present, resolved from the palette. */
3059
+ dotClasses = computed(() => {
3060
+ const colors = this.theme.colorPalette();
3061
+ const classes = new Map();
3062
+ for (const marker of this.markers()) {
3063
+ const variant = marker.variant ?? 'primary';
3064
+ if (!classes.has(variant)) {
3065
+ classes.set(variant, css({ width: 4, height: 4, borderRadius: 999, backgroundColor: colors[variant] }));
3066
+ }
3067
+ }
3068
+ return classes;
3069
+ }, ...(ngDevMode ? [{ debugName: "dotClasses" }] : /* istanbul ignore next */ []));
3070
+ dotClassFor(variant) {
3071
+ return this.dotClasses().get(variant ?? 'primary') ?? '';
3072
+ }
3073
+ showOutside = computed(() => this.componentOptions().showOutsideDays ?? false, ...(ngDevMode ? [{ debugName: "showOutside" }] : /* istanbul ignore next */ []));
3074
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3075
+ 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 });
3076
+ }
3077
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalendarComponent, decorators: [{
3078
+ type: Component,
3079
+ 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" }]
3080
+ }], 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"] }] } });
3081
+
3082
+ class UniCardContentComponent extends BaseComponent {
3083
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3084
+ 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 });
3085
+ }
3086
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, decorators: [{
3087
+ type: Component,
3088
+ 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" }]
3089
+ }] });
3090
+
3091
+ /**
3092
+ * Typeface defaults by host element, so semantic HTML and the type scale
3093
+ * reinforce each other: `<h1 uni-text>` is already headline-large.
3094
+ */
3095
+ const TagTypefaces = {
3096
+ h1: 'headline-large',
3097
+ h2: 'headline-medium',
3098
+ h3: 'headline-small',
3099
+ h4: 'title-large',
3100
+ h5: 'title-medium',
3101
+ h6: 'title-small',
3102
+ p: 'body-1-long',
3103
+ small: 'caption',
3104
+ figcaption: 'caption',
3105
+ blockquote: 'quote',
3106
+ label: 'label',
3107
+ };
3108
+ /**
3109
+ * The typography primitive, applied as an attribute to any element so
3110
+ * semantics stay yours. The attribute value is the typeface:
3111
+ * `<h1 uni-text="display-small">`, `<span uni-text="caption">`, dynamic via
3112
+ * `[uni-text]="role()"`. With no value, the typeface is inferred from the
3113
+ * host element (h1 → headline-large, p → body-1-long, …), falling back to
3114
+ * `title-small`; the `typeface` input remains as an explicit override.
3115
+ */
3116
+ class UniTextComponent {
3117
+ theme = inject(ThemeService);
3118
+ tag = inject(ElementRef).nativeElement.tagName.toLowerCase();
3119
+ /** Typeface via the selector attribute: `uni-text="headline-large"`. */
3120
+ uniText = input('', { ...(ngDevMode ? { debugName: "uniText" } : /* istanbul ignore next */ {}), alias: 'uni-text' });
3121
+ /** Explicit typeface; the attribute value wins when both are set. */
3122
+ typeface = input(undefined, ...(ngDevMode ? [{ debugName: "typeface" }] : /* istanbul ignore next */ []));
3123
+ color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
3124
+ display = input(...(ngDevMode ? [undefined, { debugName: "display" }] : /* istanbul ignore next */ []));
3125
+ align = input(...(ngDevMode ? [undefined, { debugName: "align" }] : /* istanbul ignore next */ []));
3126
+ nowrap = input(...(ngDevMode ? [undefined, { debugName: "nowrap" }] : /* istanbul ignore next */ []));
3127
+ maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
3128
+ ellipsis = input(false, ...(ngDevMode ? [{ debugName: "ellipsis" }] : /* istanbul ignore next */ []));
3129
+ resolvedTypeface = computed(() => this.uniText() || this.typeface() || TagTypefaces[this.tag] || 'title-small', ...(ngDevMode ? [{ debugName: "resolvedTypeface" }] : /* istanbul ignore next */ []));
3130
+ className = computed(() => {
3131
+ return css([
3132
+ {
3133
+ ...this.theme.typeface(this.resolvedTypeface()),
3134
+ ...this.theme.color(this.color()),
3135
+ display: this.display(),
3136
+ },
3137
+ this.align() && {
3138
+ textAlign: this.align(),
3139
+ },
3140
+ this.nowrap() && {
3141
+ whiteSpace: 'nowrap',
3142
+ },
3143
+ this.maxWidth() && {
3144
+ maxWidth: this.maxWidth(),
3145
+ overflow: 'hidden',
3146
+ whiteSpace: 'nowrap',
3147
+ textOverflow: 'ellipsis',
3148
+ display: 'inline-block',
2209
3149
  },
2210
3150
  this.ellipsis() && {
2211
3151
  whiteSpace: 'nowrap',
@@ -2376,19 +3316,13 @@ class UniCheckboxComponent extends BaseComponent {
2376
3316
  '&:disabled + .checkbox': {
2377
3317
  cursor: 'not-allowed',
2378
3318
  },
3319
+ // The shared, themable focus indicator, keyed off the hidden input's
3320
+ // focus. The ring sits out from the box, so its radius carries an extra
3321
+ // 4px to round proportionally (as the original hand-drawn ring did) —
3322
+ // without it the corners gap away from the box.
2379
3323
  '&: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
- },
3324
+ ...this.theme.focusRingStyle(this.getThemeColor(this.variant()), this.componentOptions().focusRingGap),
3325
+ borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 2}px`,
2392
3326
  },
2393
3327
  }), ...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
2394
3328
  getThemeColor(token) {
@@ -2408,196 +3342,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2408
3342
  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
3343
  }], 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
3344
 
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
3345
  class UniDataSearchComponent extends BaseComponent {
2602
3346
  datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
2603
3347
  placeholder = input('Search', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
@@ -3056,7 +3800,7 @@ class UniDataTableComponent extends BaseComponent {
3056
3800
  return name ? this.theme.theme().borders[name] : undefined;
3057
3801
  }
3058
3802
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDataTableComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3059
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDataTableComponent, isStandalone: true, selector: "uni-data-table", inputs: { datasource: { classPropertyName: "datasource", publicName: "datasource", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, detailRowTemplate: { classPropertyName: "detailRowTemplate", publicName: "detailRowTemplate", isSignal: true, isRequired: false, transformFunction: null }, useMultiSelect: { classPropertyName: "useMultiSelect", publicName: "useMultiSelect", isSignal: true, isRequired: false, transformFunction: null }, useRowClick: { classPropertyName: "useRowClick", publicName: "useRowClick", isSignal: true, isRequired: false, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowSelect: "rowSelect", rowClick: "rowClick" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], usesInheritance: true, ngImport: i0, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (record of ds.records(); track record; let i = $index) {\n <tr\n [class]=\"trClass()\"\n [attr.tabindex]=\"useRowClick() ? 0 : null\"\n (click)=\"handleRowClick(record, $index)\"\n (keydown.enter)=\"useRowClick() && handleRowClick(record, $index)\"\n (keydown.space)=\"\n useRowClick() && handleRowClick(record, $index);\n useRowClick() && $event.preventDefault()\n \"\n >\n @for (column of columns(); track column) {\n <td\n [class]=\"tdClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [class.template]=\"!!column.template\"\n [style.text-align]=\"column.textAlign\"\n >\n @if (column.template) {\n <ng-container\n [ngTemplateOutlet]=\"column.template\"\n [ngTemplateOutletContext]=\"record\"\n ></ng-container>\n } @else {\n <span uni-text [typeface]=\"componentOptions().tdTextRole\">\n @if (!column.cell) {\n {{ record[column.columnDef] }}\n } @else {\n {{ column.cell(record) }}\n }\n </span>\n }\n </td>\n }\n </tr>\n @if (detailRowTemplate()) {\n <!-- Collapsed detail rows are inert: invisible to screen\n readers and unreachable by keyboard until expanded -->\n <tr\n [class]=\"detailRowClass()\"\n [class.expanded]=\"expandedIndex() === $index\"\n [attr.inert]=\"expandedIndex() === $index ? null : ''\"\n >\n <td [attr.colspan]=\"columns().length\">\n <div class=\"detail-content-wrapper\">\n <div class=\"detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowTemplate()\"\n [ngTemplateOutletContext]=\"{\n $implicit: record,\n index: i,\n }\"\n ></ng-container>\n </div>\n </div>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n <!-- Loading Overlay -->\n @if (isLoading()) {\n <div\n uni-center-layout\n role=\"status\"\n aria-label=\"Loading\"\n position=\"absolute\"\n [inset]=\"0\"\n [backgroundColor]=\"componentOptions().loadingOverlayColor\"\n [class]=\"loadingOverlayClass\"\n zIndex=\"overlay\"\n >\n <!-- Loading Spinner -->\n <div\n uni-box-layout\n [height]=\"componentOptions().loadingSpinnerSize\"\n [width]=\"componentOptions().loadingSpinnerSize\"\n >\n <uni-icon name=\"spinner\" [color]=\"componentOptions().loadingSpinnerColor\" />\n </div>\n </div>\n }\n </div>\n\n <div\n box-layout\n [padding]=\"componentOptions().footerPadding\"\n [color]=\"componentOptions().footerColor\"\n [class]=\"footerClass()\"\n >\n <ng-content select=\"data-table-footer\"></ng-content>\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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"] }, { kind: "component", type: UniScrollAreaComponent, selector: "[uni-scroll-area], [scroll-area]", inputs: ["color", "borderRadius", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "height", "width", "appearance", "autoHeightDisabled", "verticalScrollPadding"], outputs: ["scrollable"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSortHeaderComponent, selector: "uni-sort-header", inputs: ["column", "datasource"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniCenterComponent, selector: "[uni-center-layout], [center-layout]", inputs: ["display", "justifyContent", "alignItems"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3803
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDataTableComponent, isStandalone: true, selector: "uni-data-table", inputs: { datasource: { classPropertyName: "datasource", publicName: "datasource", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, detailRowTemplate: { classPropertyName: "detailRowTemplate", publicName: "detailRowTemplate", isSignal: true, isRequired: false, transformFunction: null }, useMultiSelect: { classPropertyName: "useMultiSelect", publicName: "useMultiSelect", isSignal: true, isRequired: false, transformFunction: null }, useRowClick: { classPropertyName: "useRowClick", publicName: "useRowClick", isSignal: true, isRequired: false, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowSelect: "rowSelect", rowClick: "rowClick" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], usesInheritance: true, ngImport: i0, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column.columnDef) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n <!-- track $index: records are arbitrary consumer objects, typically\n re-fetched as fresh references \u2014 identity tracking recreated\n every row (NG0956). Row expansion is index-addressed already. -->\n @for (record of ds.records(); track $index; let i = $index) {\n <tr\n [class]=\"trClass()\"\n [attr.tabindex]=\"useRowClick() ? 0 : null\"\n (click)=\"handleRowClick(record, $index)\"\n (keydown.enter)=\"useRowClick() && handleRowClick(record, $index)\"\n (keydown.space)=\"\n useRowClick() && handleRowClick(record, $index);\n useRowClick() && $event.preventDefault()\n \"\n >\n @for (column of columns(); track column.columnDef) {\n <td\n [class]=\"tdClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [class.template]=\"!!column.template\"\n [style.text-align]=\"column.textAlign\"\n >\n @if (column.template) {\n <ng-container\n [ngTemplateOutlet]=\"column.template\"\n [ngTemplateOutletContext]=\"record\"\n ></ng-container>\n } @else {\n <span uni-text [typeface]=\"componentOptions().tdTextRole\">\n @if (!column.cell) {\n {{ record[column.columnDef] }}\n } @else {\n {{ column.cell(record) }}\n }\n </span>\n }\n </td>\n }\n </tr>\n @if (detailRowTemplate()) {\n <!-- Collapsed detail rows are inert: invisible to screen\n readers and unreachable by keyboard until expanded -->\n <tr\n [class]=\"detailRowClass()\"\n [class.expanded]=\"expandedIndex() === $index\"\n [attr.inert]=\"expandedIndex() === $index ? null : ''\"\n >\n <td [attr.colspan]=\"columns().length\">\n <div class=\"detail-content-wrapper\">\n <div class=\"detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowTemplate()\"\n [ngTemplateOutletContext]=\"{\n $implicit: record,\n index: i,\n }\"\n ></ng-container>\n </div>\n </div>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n <!-- Loading Overlay -->\n @if (isLoading()) {\n <div\n uni-center-layout\n role=\"status\"\n aria-label=\"Loading\"\n position=\"absolute\"\n [inset]=\"0\"\n [backgroundColor]=\"componentOptions().loadingOverlayColor\"\n [class]=\"loadingOverlayClass\"\n zIndex=\"overlay\"\n >\n <!-- Loading Spinner -->\n <div\n uni-box-layout\n [height]=\"componentOptions().loadingSpinnerSize\"\n [width]=\"componentOptions().loadingSpinnerSize\"\n >\n <uni-icon name=\"spinner\" [color]=\"componentOptions().loadingSpinnerColor\" />\n </div>\n </div>\n }\n </div>\n\n <div\n box-layout\n [padding]=\"componentOptions().footerPadding\"\n [color]=\"componentOptions().footerColor\"\n [class]=\"footerClass()\"\n >\n <ng-content select=\"data-table-footer\"></ng-content>\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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"] }, { kind: "component", type: UniScrollAreaComponent, selector: "[uni-scroll-area], [scroll-area]", inputs: ["color", "borderRadius", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "height", "width", "appearance", "autoHeightDisabled", "verticalScrollPadding"], outputs: ["scrollable"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSortHeaderComponent, selector: "uni-sort-header", inputs: ["column", "datasource"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniCenterComponent, selector: "[uni-center-layout], [center-layout]", inputs: ["display", "justifyContent", "alignItems"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3060
3804
  }
3061
3805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDataTableComponent, decorators: [{
3062
3806
  type: Component,
@@ -3068,7 +3812,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3068
3812
  UniSortHeaderComponent,
3069
3813
  UniIconComponent,
3070
3814
  UniCenterComponent,
3071
- ], providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (record of ds.records(); track record; let i = $index) {\n <tr\n [class]=\"trClass()\"\n [attr.tabindex]=\"useRowClick() ? 0 : null\"\n (click)=\"handleRowClick(record, $index)\"\n (keydown.enter)=\"useRowClick() && handleRowClick(record, $index)\"\n (keydown.space)=\"\n useRowClick() && handleRowClick(record, $index);\n useRowClick() && $event.preventDefault()\n \"\n >\n @for (column of columns(); track column) {\n <td\n [class]=\"tdClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [class.template]=\"!!column.template\"\n [style.text-align]=\"column.textAlign\"\n >\n @if (column.template) {\n <ng-container\n [ngTemplateOutlet]=\"column.template\"\n [ngTemplateOutletContext]=\"record\"\n ></ng-container>\n } @else {\n <span uni-text [typeface]=\"componentOptions().tdTextRole\">\n @if (!column.cell) {\n {{ record[column.columnDef] }}\n } @else {\n {{ column.cell(record) }}\n }\n </span>\n }\n </td>\n }\n </tr>\n @if (detailRowTemplate()) {\n <!-- Collapsed detail rows are inert: invisible to screen\n readers and unreachable by keyboard until expanded -->\n <tr\n [class]=\"detailRowClass()\"\n [class.expanded]=\"expandedIndex() === $index\"\n [attr.inert]=\"expandedIndex() === $index ? null : ''\"\n >\n <td [attr.colspan]=\"columns().length\">\n <div class=\"detail-content-wrapper\">\n <div class=\"detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowTemplate()\"\n [ngTemplateOutletContext]=\"{\n $implicit: record,\n index: i,\n }\"\n ></ng-container>\n </div>\n </div>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n <!-- Loading Overlay -->\n @if (isLoading()) {\n <div\n uni-center-layout\n role=\"status\"\n aria-label=\"Loading\"\n position=\"absolute\"\n [inset]=\"0\"\n [backgroundColor]=\"componentOptions().loadingOverlayColor\"\n [class]=\"loadingOverlayClass\"\n zIndex=\"overlay\"\n >\n <!-- Loading Spinner -->\n <div\n uni-box-layout\n [height]=\"componentOptions().loadingSpinnerSize\"\n [width]=\"componentOptions().loadingSpinnerSize\"\n >\n <uni-icon name=\"spinner\" [color]=\"componentOptions().loadingSpinnerColor\" />\n </div>\n </div>\n }\n </div>\n\n <div\n box-layout\n [padding]=\"componentOptions().footerPadding\"\n [color]=\"componentOptions().footerColor\"\n [class]=\"footerClass()\"\n >\n <ng-content select=\"data-table-footer\"></ng-content>\n </div>\n </div>\n}\n" }]
3815
+ ], providers: [{ provide: COMPONENT_NAME, useValue: 'dataTable' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let ds = datasource();\n@if (ds) {\n <div\n box-layout\n [color]=\"componentOptions().color\"\n [border]=\"componentOptions().border\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [elevation]=\"componentOptions().elevation\"\n overflow=\"hidden\"\n >\n <div\n box-layout\n [padding]=\"componentOptions().headerPadding\"\n [color]=\"componentOptions().headerColor\"\n [class]=\"headerClass()\"\n >\n <ng-content select=\"data-table-header\"></ng-content>\n </div>\n\n <div box-layout [height]=\"height()\" position=\"relative\" [attr.aria-busy]=\"isLoading()\">\n <div scroll-area [verticalScrollPadding]=\"0\" (scrollable)=\"handleScrollable($event)\">\n <table [class]=\"tableClass()\">\n <thead>\n <tr>\n @for (column of columns(); track column.columnDef) {\n <th\n scope=\"col\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [class]=\"thClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [style.text-align]=\"column.textAlign\"\n >\n <uni-sort-header [datasource]=\"datasource()\" [column]=\"column.columnDef\">\n <span uni-text [typeface]=\"componentOptions().thTextRole\">{{ column.header }}</span>\n </uni-sort-header>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n <!-- track $index: records are arbitrary consumer objects, typically\n re-fetched as fresh references \u2014 identity tracking recreated\n every row (NG0956). Row expansion is index-addressed already. -->\n @for (record of ds.records(); track $index; let i = $index) {\n <tr\n [class]=\"trClass()\"\n [attr.tabindex]=\"useRowClick() ? 0 : null\"\n (click)=\"handleRowClick(record, $index)\"\n (keydown.enter)=\"useRowClick() && handleRowClick(record, $index)\"\n (keydown.space)=\"\n useRowClick() && handleRowClick(record, $index);\n useRowClick() && $event.preventDefault()\n \"\n >\n @for (column of columns(); track column.columnDef) {\n <td\n [class]=\"tdClass()\"\n [class.scrollable]=\"scrollable()\"\n [class.sticky]=\"column.isSticky\"\n [class.template]=\"!!column.template\"\n [style.text-align]=\"column.textAlign\"\n >\n @if (column.template) {\n <ng-container\n [ngTemplateOutlet]=\"column.template\"\n [ngTemplateOutletContext]=\"record\"\n ></ng-container>\n } @else {\n <span uni-text [typeface]=\"componentOptions().tdTextRole\">\n @if (!column.cell) {\n {{ record[column.columnDef] }}\n } @else {\n {{ column.cell(record) }}\n }\n </span>\n }\n </td>\n }\n </tr>\n @if (detailRowTemplate()) {\n <!-- Collapsed detail rows are inert: invisible to screen\n readers and unreachable by keyboard until expanded -->\n <tr\n [class]=\"detailRowClass()\"\n [class.expanded]=\"expandedIndex() === $index\"\n [attr.inert]=\"expandedIndex() === $index ? null : ''\"\n >\n <td [attr.colspan]=\"columns().length\">\n <div class=\"detail-content-wrapper\">\n <div class=\"detail-inner\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowTemplate()\"\n [ngTemplateOutletContext]=\"{\n $implicit: record,\n index: i,\n }\"\n ></ng-container>\n </div>\n </div>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n\n <!-- Loading Overlay -->\n @if (isLoading()) {\n <div\n uni-center-layout\n role=\"status\"\n aria-label=\"Loading\"\n position=\"absolute\"\n [inset]=\"0\"\n [backgroundColor]=\"componentOptions().loadingOverlayColor\"\n [class]=\"loadingOverlayClass\"\n zIndex=\"overlay\"\n >\n <!-- Loading Spinner -->\n <div\n uni-box-layout\n [height]=\"componentOptions().loadingSpinnerSize\"\n [width]=\"componentOptions().loadingSpinnerSize\"\n >\n <uni-icon name=\"spinner\" [color]=\"componentOptions().loadingSpinnerColor\" />\n </div>\n </div>\n }\n </div>\n\n <div\n box-layout\n [padding]=\"componentOptions().footerPadding\"\n [color]=\"componentOptions().footerColor\"\n [class]=\"footerClass()\"\n >\n <ng-content select=\"data-table-footer\"></ng-content>\n </div>\n </div>\n}\n" }]
3072
3816
  }], propDecorators: { datasource: [{ type: i0.Input, args: [{ isSignal: true, alias: "datasource", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], detailRowTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "detailRowTemplate", required: false }] }], useMultiSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "useMultiSelect", required: false }] }], useRowClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "useRowClick", required: false }] }], highlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlight", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }] } });
3073
3817
 
3074
3818
  /**
@@ -3077,6 +3821,1024 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3077
3821
  * This file exports all public-facing elements of the data-table component.
3078
3822
  */
3079
3823
 
3824
+ class UniDropdownComponent extends BaseComponent {
3825
+ renderer = inject(Renderer2);
3826
+ delay = 100;
3827
+ // Reactively track visibility status using Signals
3828
+ showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
3829
+ trigger = input.required(...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
3830
+ placement = input('bottom-start', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
3831
+ offset = input({ mainAxis: 4, alignmentAxis: 12 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
3832
+ /** CSS anchor-name linking the trigger to the popover panel. */
3833
+ anchorName = newAnchorName();
3834
+ /**
3835
+ * Value for aria-haspopup on the trigger, describing what the popover
3836
+ * contains (e.g. 'menu' for Menu, 'dialog' for rich content). When unset,
3837
+ * only aria-expanded/aria-controls are managed.
3838
+ */
3839
+ ariaHasPopup = input(null, ...(ngDevMode ? [{ debugName: "ariaHasPopup" }] : /* istanbul ignore next */ []));
3840
+ /** Document-unique id of the popover element, for aria-controls wiring. */
3841
+ popoverId = uniqueId('uni-dropdown');
3842
+ paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
3843
+ paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
3844
+ // Per-instance panel-chrome overrides; undefined falls back to the theme's
3845
+ // `dropdown` options, so hosts like uni-menu can restyle their panel
3846
+ // without forking the shared dropdown entry.
3847
+ border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
3848
+ borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
3849
+ shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
3850
+ color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
3851
+ dropdownShowing = output();
3852
+ dropdownHiding = output();
3853
+ dropdownRef;
3854
+ get _trigger() {
3855
+ return this.trigger();
3856
+ }
3857
+ get _dropdown() {
3858
+ return this.dropdownRef.nativeElement;
3859
+ }
3860
+ transformOriginMap = {
3861
+ top: 'bottom center',
3862
+ right: 'center left',
3863
+ bottom: 'top center',
3864
+ left: 'center right',
3865
+ 'top-start': 'bottom left',
3866
+ 'top-end': 'bottom right',
3867
+ 'right-start': 'top left',
3868
+ 'right-end': 'bottom left',
3869
+ 'bottom-start': 'top left',
3870
+ 'bottom-end': 'top right',
3871
+ 'left-start': 'top right',
3872
+ 'left-end': 'bottom right',
3873
+ };
3874
+ dropdownClass = computed(() => {
3875
+ const currentPlacement = this.placement();
3876
+ return css([
3877
+ {
3878
+ // Reset browser agent default popover styles
3879
+ border: 'none',
3880
+ background: 'transparent',
3881
+ padding: 0,
3882
+ overflow: 'visible',
3883
+ width: 'max-content',
3884
+ // Native anchor positioning: the browser keeps the panel attached to
3885
+ // the trigger (no scroll/resize listeners needed)
3886
+ ...anchorStyles(this.anchorName, currentPlacement, this.offset()),
3887
+ // 2. Animate discrete properties across top layer layout contexts
3888
+ transitionProperty: 'transform, opacity, display, overlay',
3889
+ transitionDuration: `${this.delay}ms`,
3890
+ transitionTimingFunction: 'linear',
3891
+ transitionBehavior: 'allow-discrete',
3892
+ // Hidden State (Closed)
3893
+ opacity: 0,
3894
+ transform: 'scale(0.8)',
3895
+ transformOrigin: this.transformOriginMap[currentPlacement],
3896
+ // 3. Active state styling controlled via the native browser pseudo-class
3897
+ ['&:popover-open']: {
3898
+ opacity: 1,
3899
+ transform: 'scale(1)',
3900
+ },
3901
+ // 4. Starting-style rules what properties animate *from* when transitioning in
3902
+ ['@starting-style']: {
3903
+ ['&:popover-open']: {
3904
+ opacity: 0,
3905
+ transform: 'scale(0.8)',
3906
+ },
3907
+ },
3908
+ },
3909
+ ]);
3910
+ }, ...(ngDevMode ? [{ debugName: "dropdownClass" }] : /* istanbul ignore next */ []));
3911
+ /** The element that receives focus and carries the ARIA popup state. */
3912
+ get _focusTarget() {
3913
+ return resolveFocusTarget(this._trigger);
3914
+ }
3915
+ ngOnInit() {
3916
+ // Single native click binding to manage open/close commands
3917
+ this.renderer.listen(this._trigger, 'click', (e) => {
3918
+ e.stopPropagation();
3919
+ this.toggleDropdown();
3920
+ });
3921
+ // Anchor the popover panel to the trigger element
3922
+ this.renderer.setStyle(this._trigger, 'anchor-name', this.anchorName);
3923
+ // Wire the ARIA popup contract onto the focusable trigger element
3924
+ const focusTarget = this._focusTarget;
3925
+ this.renderer.setAttribute(focusTarget, 'aria-expanded', 'false');
3926
+ this.renderer.setAttribute(focusTarget, 'aria-controls', this.popoverId);
3927
+ if (this.ariaHasPopup()) {
3928
+ this.renderer.setAttribute(focusTarget, 'aria-haspopup', this.ariaHasPopup());
3929
+ }
3930
+ // Sync state if user invokes light-dismiss via outside click or Escape key
3931
+ this.renderer.listen(this._dropdown, 'toggle', (event) => {
3932
+ const isOpened = event.newState === 'open';
3933
+ this.showing.set(isOpened);
3934
+ this.renderer.setAttribute(this._focusTarget, 'aria-expanded', `${isOpened}`);
3935
+ if (isOpened) {
3936
+ this.dropdownShowing.emit(true);
3937
+ }
3938
+ else {
3939
+ this.dropdownHiding.emit(true);
3940
+ this.restoreFocus();
3941
+ }
3942
+ });
3943
+ }
3944
+ /**
3945
+ * Returns focus to the trigger when the popover closes while focus was
3946
+ * inside it (or was dropped on <body> by the top layer closing), so
3947
+ * keyboard users are never stranded (WCAG 2.4.3).
3948
+ */
3949
+ restoreFocus() {
3950
+ const active = document.activeElement;
3951
+ if (active === document.body || (active && this._dropdown.contains(active))) {
3952
+ this._focusTarget.focus();
3953
+ }
3954
+ }
3955
+ toggleDropdown() {
3956
+ if (this.showing()) {
3957
+ this._dropdown.hidePopover();
3958
+ }
3959
+ else {
3960
+ this._dropdown.showPopover();
3961
+ }
3962
+ }
3963
+ hideDropdown() {
3964
+ this._dropdown.hidePopover();
3965
+ }
3966
+ ngOnDestroy() {
3967
+ try {
3968
+ this._dropdown.hidePopover();
3969
+ }
3970
+ catch {
3971
+ // popover was already closed or detached
3972
+ }
3973
+ }
3974
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3975
+ 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: `
3976
+ <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
3977
+ <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3978
+ <div
3979
+ box-layout
3980
+ [border]="border() ?? componentOptions().border"
3981
+ [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
3982
+ [paddingVertical]="paddingVertical()"
3983
+ [paddingHorizontal]="paddingHorizontal()"
3984
+ [color]="color() ?? componentOptions().color"
3985
+ [shadow]="shadow() ?? componentOptions().shadow"
3986
+ >
3987
+ <ng-content></ng-content>
3988
+ </div>
3989
+ </div>
3990
+ `, 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 });
3991
+ }
3992
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, decorators: [{
3993
+ type: Component,
3994
+ args: [{
3995
+ changeDetection: ChangeDetectionStrategy.OnPush,
3996
+ selector: 'uni-dropdown',
3997
+ imports: [UniBoxComponent],
3998
+ template: `
3999
+ <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
4000
+ <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
4001
+ <div
4002
+ box-layout
4003
+ [border]="border() ?? componentOptions().border"
4004
+ [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
4005
+ [paddingVertical]="paddingVertical()"
4006
+ [paddingHorizontal]="paddingHorizontal()"
4007
+ [color]="color() ?? componentOptions().color"
4008
+ [shadow]="shadow() ?? componentOptions().shadow"
4009
+ >
4010
+ <ng-content></ng-content>
4011
+ </div>
4012
+ </div>
4013
+ `,
4014
+ providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
4015
+ }]
4016
+ }], 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: [{
4017
+ type: ViewChild,
4018
+ args: ['dropdown', { static: true }]
4019
+ }] } });
4020
+
4021
+ class UniInputBoxComponent extends BaseComponent {
4022
+ className = css({ display: 'contents' });
4023
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4024
+ error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
4025
+ minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
4026
+ /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
4027
+ height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4028
+ color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4029
+ border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
4030
+ shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4031
+ inputBoxClass = computed(() => css([
4032
+ this.disabled() && {
4033
+ ...this.theme.color(this.componentOptions().disabledTextColor),
4034
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4035
+ cursor: 'not-allowed !important',
4036
+ },
4037
+ {
4038
+ '& input, select, textarea': {
4039
+ ...removeInputPlatformStyling,
4040
+ height: '100%',
4041
+ ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4042
+ ...this.theme.color(this.componentOptions().textColor),
4043
+ // `typeFace` is the deprecated casing; themes that still set it win
4044
+ // only when the canonical key is absent.
4045
+ ...this.theme.typeface(this.componentOptions().typeface ?? this.componentOptions().typeFace),
4046
+ },
4047
+ // Multi-line fields size themselves (rows/resize), not from the box.
4048
+ '& textarea': {
4049
+ height: 'auto',
4050
+ ...this.theme.paddingTop('xs'),
4051
+ ...this.theme.paddingBottom('xs'),
4052
+ },
4053
+ '&:has(input:disabled, select:disabled, textarea:disabled)': {
4054
+ ...this.theme.color(this.componentOptions().disabledTextColor),
4055
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4056
+ },
4057
+ '& input:disabled, select:disabled, textarea:disabled': {
4058
+ cursor: 'not-allowed !important',
4059
+ },
4060
+ '&:has(input:focus, select:focus, textarea:focus)': {
4061
+ outline: this.componentOptions().focusOutline,
4062
+ outlineOffset: this.componentOptions().focusOutlineOffset,
4063
+ // Optional focus chrome (border/ring/background). It yields to the
4064
+ // error state, so a flagged field stays visibly flagged while the
4065
+ // user is in it correcting the value.
4066
+ ...(this.error()
4067
+ ? {}
4068
+ : {
4069
+ ...this.theme.border(this.componentOptions().focusBorder),
4070
+ ...this.theme.boxShadow(this.componentOptions().focusShadow),
4071
+ ...this.theme.backgroundColor(this.componentOptions().focusColor),
4072
+ }),
4073
+ },
4074
+ },
4075
+ ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4076
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4077
+ 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 });
4078
+ }
4079
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4080
+ type: Component,
4081
+ 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" }]
4082
+ }], 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 }] }] } });
4083
+
4084
+ /**
4085
+ * Date field with free-typed parsing and a popup calendar. Type `aug 20`,
4086
+ * `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
4087
+ * same canonical `'YYYY-MM-DD'` string either way. Parsing is `Intl`-driven
4088
+ * (locale digit order and month names, never hardcoded); unreadable text
4089
+ * stays in the field, flagged, with a `rejected` event. The popup is a
4090
+ * native popover hosting the same `uni-calendar` an app could render inline.
4091
+ */
4092
+ class UniDateInputComponent extends BaseComponent {
4093
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4094
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
4095
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4096
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4097
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4098
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4099
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4100
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4101
+ // --- Configuration -------------------------------------------------------
4102
+ /** Accessible name for the field, e.g. "Appointment date". */
4103
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4104
+ /** Defaults to the locale's digit pattern, e.g. `MM/DD/YYYY`. */
4105
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
4106
+ /** How the committed value renders, e.g. `{ dateStyle: 'long' }`. */
4107
+ displayFormat = input({ dateStyle: 'medium' }, ...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
4108
+ /** BCP 47 tag; defaults to the document language, then the browser's. */
4109
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
4110
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
4111
+ /** Custom parser, replacing the built-in ISO/locale/month-name parsing. */
4112
+ parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
4113
+ /** Renders without its own input-box chrome, for composers like uni-date-time-input. */
4114
+ embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
4115
+ // --- Forwarded to the popup calendar --------------------------------------
4116
+ minDate = input(...(ngDevMode ? [undefined, { debugName: "minDate" }] : /* istanbul ignore next */ []));
4117
+ maxDate = input(...(ngDevMode ? [undefined, { debugName: "maxDate" }] : /* istanbul ignore next */ []));
4118
+ disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
4119
+ markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
4120
+ weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
4121
+ // --- Events ----------------------------------------------------------------
4122
+ /** Popup shown. */
4123
+ opened = output();
4124
+ /** Popup hidden. */
4125
+ closed = output();
4126
+ /** A typed commit was refused; the raw text stays in the field. */
4127
+ rejected = output();
4128
+ host = inject(ElementRef);
4129
+ inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
4130
+ // #toggle sits on the icon-button component, so read the element explicitly.
4131
+ toggleRef = viewChild('toggle', { ...(ngDevMode ? { debugName: "toggleRef" } : /* istanbul ignore next */ {}), read: ElementRef });
4132
+ popupRef = viewChild('popupDialog', ...(ngDevMode ? [{ debugName: "popupRef" }] : /* istanbul ignore next */ []));
4133
+ dropdown = viewChild(UniDropdownComponent, ...(ngDevMode ? [{ debugName: "dropdown" }] : /* istanbul ignore next */ []));
4134
+ calendar = viewChild(UniCalendarComponent, ...(ngDevMode ? [{ debugName: "calendar" }] : /* istanbul ignore next */ []));
4135
+ srOnly = css(visuallyHidden);
4136
+ /** A refused commit — styles the field and sets aria-invalid until edited. */
4137
+ draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
4138
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
4139
+ toggleElement = computed(() => this.toggleRef()?.nativeElement, ...(ngDevMode ? [{ debugName: "toggleElement" }] : /* istanbul ignore next */ []));
4140
+ popupOpen = computed(() => this.dropdown()?.showing() ?? false, ...(ngDevMode ? [{ debugName: "popupOpen" }] : /* istanbul ignore next */ []));
4141
+ resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
4142
+ displayText = computed(() => {
4143
+ const value = this.value();
4144
+ return value ? formatDate(value, this.resolvedLocale(), this.displayFormat()) : '';
4145
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
4146
+ resolvedPlaceholder = computed(() => this.placeholder() ?? localeDatePlaceholder(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
4147
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4148
+ toggleLabel = computed(() => {
4149
+ const value = this.value();
4150
+ return value
4151
+ ? `Change date, ${formatDate(value, this.resolvedLocale(), { dateStyle: 'full' })}`
4152
+ : 'Choose date';
4153
+ }, ...(ngDevMode ? [{ debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
4154
+ // --- Committing -------------------------------------------------------------
4155
+ fullDate(date) {
4156
+ return formatDate(date, this.resolvedLocale(), { dateStyle: 'full' });
4157
+ }
4158
+ isDayBlocked(date) {
4159
+ const dates = this.disabledDates();
4160
+ if (!dates)
4161
+ return false;
4162
+ return Array.isArray(dates) ? dates.includes(date) : dates(date);
4163
+ }
4164
+ setValue(date, silent = false) {
4165
+ this.value.set(date);
4166
+ this.draftInvalid.set(false);
4167
+ this.setFieldText(this.displayText());
4168
+ if (!silent)
4169
+ this.announce(date ? `${this.fullDate(date)}.` : 'Date cleared.');
4170
+ }
4171
+ refuse(raw, reason) {
4172
+ this.draftInvalid.set(true);
4173
+ const message = {
4174
+ unparseable: `Couldn't read “${raw}” as a date.`,
4175
+ 'out-of-range': `${raw} is outside the allowed dates.`,
4176
+ disabled: `${raw} isn't available.`,
4177
+ }[reason];
4178
+ this.announce(message);
4179
+ this.rejected.emit({ raw, reason });
4180
+ }
4181
+ commit(raw) {
4182
+ const trimmed = raw.trim();
4183
+ if (!trimmed) {
4184
+ this.setValue(undefined);
4185
+ return true;
4186
+ }
4187
+ const custom = this.parse();
4188
+ const parsed = custom
4189
+ ? custom(trimmed, this.resolvedLocale())
4190
+ : parseDateText(trimmed, this.resolvedLocale());
4191
+ if (!parsed) {
4192
+ this.refuse(trimmed, 'unparseable');
4193
+ return false;
4194
+ }
4195
+ const min = this.minDate();
4196
+ const max = this.maxDate();
4197
+ if ((min && parsed < min) || (max && parsed > max)) {
4198
+ this.refuse(trimmed, 'out-of-range');
4199
+ return false;
4200
+ }
4201
+ if (this.isDayBlocked(parsed)) {
4202
+ this.refuse(trimmed, 'disabled');
4203
+ return false;
4204
+ }
4205
+ this.setValue(parsed);
4206
+ return true;
4207
+ }
4208
+ /** Step a committed value ±1 day, skipping blocked days, stopping at fences. */
4209
+ step(direction) {
4210
+ const committed = this.value();
4211
+ if (!committed)
4212
+ return;
4213
+ let date = addDays(committed, direction);
4214
+ let guard = 0;
4215
+ while (this.isDayBlocked(date) && guard++ < 400)
4216
+ date = addDays(date, direction);
4217
+ const min = this.minDate();
4218
+ const max = this.maxDate();
4219
+ if ((min && date < min) || (max && date > max))
4220
+ return; // fence
4221
+ this.setValue(date);
4222
+ }
4223
+ // --- Keyboard ----------------------------------------------------------------
4224
+ onInputKeydown(event) {
4225
+ const element = this.inputRef().nativeElement;
4226
+ switch (event.key) {
4227
+ case 'Enter':
4228
+ event.preventDefault();
4229
+ this.commit(element.value);
4230
+ break;
4231
+ case 'Escape':
4232
+ if (this.popupOpen()) {
4233
+ this.closePopup();
4234
+ }
4235
+ else {
4236
+ this.setFieldText(this.displayText());
4237
+ this.draftInvalid.set(false);
4238
+ }
4239
+ break;
4240
+ case 'ArrowDown':
4241
+ event.preventDefault();
4242
+ // Alt or an empty field opens the popup; on a committed value the
4243
+ // caret has nowhere to go, so stepping is what a spinner would do.
4244
+ if (event.altKey || element.value.trim() === '')
4245
+ this.openPopup();
4246
+ else if (this.value() && element.value === this.displayText())
4247
+ this.step(-1);
4248
+ break;
4249
+ case 'ArrowUp':
4250
+ event.preventDefault();
4251
+ if (this.value() && element.value === this.displayText())
4252
+ this.step(1);
4253
+ break;
4254
+ }
4255
+ }
4256
+ onInput() {
4257
+ this.draftInvalid.set(false);
4258
+ }
4259
+ onFocusOut(event) {
4260
+ const next = event.relatedTarget;
4261
+ if (next && this.host.nativeElement.contains(next))
4262
+ return;
4263
+ this.touched.set(true);
4264
+ if (this.popupOpen())
4265
+ return;
4266
+ const element = this.inputRef()?.nativeElement;
4267
+ if (element && this.commitOnBlur() && element.value !== this.displayText())
4268
+ this.commit(element.value);
4269
+ }
4270
+ // --- Popup ---------------------------------------------------------------------
4271
+ openPopup() {
4272
+ if (this.disabled() || this.popupOpen())
4273
+ return;
4274
+ this.dropdown()?.toggleDropdown();
4275
+ }
4276
+ closePopup() {
4277
+ if (this.popupOpen())
4278
+ this.dropdown()?.hideDropdown();
4279
+ }
4280
+ onPopupShowing() {
4281
+ // The grid opens on the committed value's month (or today's). Falsy
4282
+ // guard: a bound '' counts as no value, like everywhere else.
4283
+ this.calendar()?.month.set(monthOf(this.value() || todayIso()));
4284
+ this.calendar()?.focusActiveDay();
4285
+ this.opened.emit();
4286
+ }
4287
+ onPopupHiding() {
4288
+ this.closed.emit();
4289
+ // Focus returns to the field (not the toggle) when it was in the popup —
4290
+ // this runs before the dropdown's own restoreFocus, which then no-ops.
4291
+ const active = document.activeElement;
4292
+ const popup = this.popupRef()?.nativeElement;
4293
+ if (!active || active === document.body || (popup && popup.contains(active)))
4294
+ this.inputRef()?.nativeElement.focus();
4295
+ }
4296
+ onCalendarPick(date) {
4297
+ this.setValue(date);
4298
+ this.closePopup();
4299
+ }
4300
+ /** The popup is a focus-holding dialog: Tab cycles inside it (APG pattern). */
4301
+ onPopupKeydown(event) {
4302
+ if (event.key === 'Escape') {
4303
+ event.preventDefault();
4304
+ this.closePopup();
4305
+ return;
4306
+ }
4307
+ if (event.key !== 'Tab')
4308
+ return;
4309
+ const popup = this.popupRef()?.nativeElement;
4310
+ if (!popup)
4311
+ return;
4312
+ const focusables = Array.from(popup.querySelectorAll('button:not(:disabled)')).filter((button) => button.tabIndex >= 0);
4313
+ if (!focusables.length)
4314
+ return;
4315
+ const index = focusables.indexOf(document.activeElement);
4316
+ const next = focusables[(index + (event.shiftKey ? -1 : 1) + focusables.length) % focusables.length];
4317
+ event.preventDefault();
4318
+ next.focus();
4319
+ }
4320
+ // --- Internals -------------------------------------------------------------------
4321
+ setFieldText(text) {
4322
+ const element = this.inputRef()?.nativeElement;
4323
+ if (element)
4324
+ element.value = text;
4325
+ }
4326
+ announce(message) {
4327
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
4328
+ }
4329
+ // --- Styling -----------------------------------------------------------------------
4330
+ className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4331
+ rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
4332
+ inputClass = computed(() => {
4333
+ const colors = this.theme.colorPalette();
4334
+ return css([
4335
+ {
4336
+ flex: 1,
4337
+ minWidth: 0,
4338
+ border: 0,
4339
+ outline: 'none',
4340
+ background: 'transparent',
4341
+ color: 'inherit',
4342
+ font: 'inherit',
4343
+ },
4344
+ this.draftInvalid() && {
4345
+ color: colors['warn'],
4346
+ // Shape and colour, not colour alone (WCAG 1.4.1).
4347
+ textDecoration: `underline dashed ${colors['warn']} 1.5px`,
4348
+ textUnderlineOffset: 3,
4349
+ },
4350
+ ]);
4351
+ }, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
4352
+ toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
4353
+ /** Embedded mode: the composer owns the box; keep only the flex row. */
4354
+ embeddedClass = computed(() => {
4355
+ const colors = this.theme.colorPalette();
4356
+ return css([
4357
+ { display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 },
4358
+ this.draftInvalid() && { color: colors['warn'] },
4359
+ ]);
4360
+ }, ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4361
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4362
+ 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 });
4363
+ }
4364
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, decorators: [{
4365
+ type: Component,
4366
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-date-input, DateInput', imports: [
4367
+ NgTemplateOutlet,
4368
+ UniCalendarComponent,
4369
+ UniDropdownComponent,
4370
+ UniIconButtonComponent,
4371
+ UniInputBoxComponent,
4372
+ ], 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" }]
4373
+ }], 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 }] }] } });
4374
+
4375
+ const toMinutes = (time) => {
4376
+ const [h, m] = time.split(':').map(Number);
4377
+ return h * 60 + m;
4378
+ };
4379
+ const fromMinutes = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
4380
+ /**
4381
+ * Time field: a combobox over time options — the same listbox contract as
4382
+ * uni-search-input and uni-tag-input. Type `3p`, `930` or `15:00`, or pick
4383
+ * `3:00 PM` from the list; the form always gets 24-hour `'HH:mm'` (`hour12`
4384
+ * affects display only). The list is assistive, not exhaustive: any
4385
+ * parseable time commits, unless `slots` pins the choices (a slot picker) —
4386
+ * then a typed time must match one. Unreadable or unavailable text stays in
4387
+ * the field, flagged, with a `rejected` event.
4388
+ */
4389
+ class UniTimeInputComponent extends BaseComponent {
4390
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4391
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
4392
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4393
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4394
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4395
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4396
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4397
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4398
+ // --- Configuration -------------------------------------------------------
4399
+ /** Accessible name for the field, e.g. "Start time". */
4400
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4401
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
4402
+ /** Generated list granularity, in minutes. */
4403
+ minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
4404
+ /** Earliest allowed time (inclusive), `'09:00'`. */
4405
+ minTime = input(...(ngDevMode ? [undefined, { debugName: "minTime" }] : /* istanbul ignore next */ []));
4406
+ /** Latest allowed time (inclusive), `'17:00'`. */
4407
+ maxTime = input(...(ngDevMode ? [undefined, { debugName: "maxTime" }] : /* istanbul ignore next */ []));
4408
+ /** Exact allowed times (scheduling). When set, typed entry must match one. */
4409
+ slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
4410
+ /** 12-hour display; defaults from the locale. The value stays 24-hour. */
4411
+ hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
4412
+ /** BCP 47 tag for the display format; defaults to the document language. */
4413
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
4414
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
4415
+ /** Renders without its own input-box chrome, for composers like uni-date-time-input. */
4416
+ embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
4417
+ // --- Events ----------------------------------------------------------------
4418
+ /** A typed commit was refused; the raw text stays in the field. */
4419
+ rejected = output();
4420
+ host = inject(ElementRef);
4421
+ inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
4422
+ listRef = viewChild('listbox', ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
4423
+ srOnly = css(visuallyHidden);
4424
+ /** A refused commit — styles the field and sets aria-invalid until edited. */
4425
+ draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
4426
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
4427
+ resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
4428
+ resolvedHour12 = computed(() => this.hour12() ?? localeDefaultHour12(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedHour12" }] : /* istanbul ignore next */ []));
4429
+ /** The listed times: pinned `slots` verbatim, else the generated step grid. */
4430
+ options = computed(() => this.slots() ?? timeSlots(this.minuteStep(), this.minTime(), this.maxTime()), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4431
+ optionLabels = computed(() => this.options().map((time) => this.formatValue(time)), ...(ngDevMode ? [{ debugName: "optionLabels" }] : /* istanbul ignore next */ []));
4432
+ /** Shared combobox bookkeeping — identical contract to uni-search-input. */
4433
+ list = createListboxNavigation({
4434
+ count: () => this.options().length,
4435
+ idPrefix: 'uni-time-listbox',
4436
+ });
4437
+ displayText = computed(() => {
4438
+ const value = this.value();
4439
+ return value ? this.formatValue(value) : '';
4440
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
4441
+ resolvedPlaceholder = computed(() => this.placeholder() ?? this.formatValue('09:00'), ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
4442
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4443
+ formatValue(time) {
4444
+ return formatTime(time, this.resolvedLocale(), this.resolvedHour12());
4445
+ }
4446
+ // --- Committing -------------------------------------------------------------
4447
+ setValue(time, silent = false) {
4448
+ this.value.set(time);
4449
+ this.draftInvalid.set(false);
4450
+ this.setFieldText(this.displayText());
4451
+ if (!silent)
4452
+ this.announce(time ? `${this.formatValue(time)}.` : 'Time cleared.');
4453
+ }
4454
+ refuse(raw, reason, shown = raw) {
4455
+ this.draftInvalid.set(true);
4456
+ const message = {
4457
+ unparseable: `Couldn't read “${shown}” as a time.`,
4458
+ 'out-of-range': `${shown} is outside the allowed times.`,
4459
+ unavailable: `${shown} isn't available.`,
4460
+ }[reason];
4461
+ this.announce(message);
4462
+ this.rejected.emit({ raw, reason });
4463
+ }
4464
+ commit(raw) {
4465
+ const trimmed = raw.trim();
4466
+ if (!trimmed) {
4467
+ this.setValue(undefined);
4468
+ return true;
4469
+ }
4470
+ let parsed = parseTimeText(trimmed, this.resolvedHour12());
4471
+ if (!parsed) {
4472
+ this.refuse(trimmed, 'unparseable');
4473
+ return false;
4474
+ }
4475
+ const min = this.minTime();
4476
+ const max = this.maxTime();
4477
+ const inBounds = (time) => !(min && time < min) && !(max && time > max);
4478
+ const slots = this.slots();
4479
+ if (!slots && !inBounds(parsed)) {
4480
+ // The PM bias yields if it pushed the time out of bounds.
4481
+ const unbiased = parseTimeText(trimmed, false);
4482
+ if (unbiased && inBounds(unbiased))
4483
+ parsed = unbiased;
4484
+ else {
4485
+ this.refuse(trimmed, 'out-of-range');
4486
+ return false;
4487
+ }
4488
+ }
4489
+ if (slots && !slots.includes(parsed)) {
4490
+ // Announce the formatted time — "5:00 PM isn't available."
4491
+ this.refuse(trimmed, 'unavailable', this.formatValue(parsed));
4492
+ return false;
4493
+ }
4494
+ this.setValue(parsed);
4495
+ return true;
4496
+ }
4497
+ /** Step a committed value ±minuteStep (±1 slot when pinned), clamped. */
4498
+ stepValue(direction) {
4499
+ const committed = this.value();
4500
+ if (!committed)
4501
+ return;
4502
+ const slots = this.slots();
4503
+ if (slots?.length) {
4504
+ const sorted = [...slots].sort();
4505
+ const index = sorted.indexOf(committed);
4506
+ const next = index >= 0
4507
+ ? sorted[index + direction]
4508
+ : direction > 0
4509
+ ? sorted.find((slot) => slot > committed)
4510
+ : [...sorted].reverse().find((slot) => slot < committed);
4511
+ if (next)
4512
+ this.setValue(next);
4513
+ return;
4514
+ }
4515
+ const step = this.minuteStep();
4516
+ const current = toMinutes(committed);
4517
+ const snapped = direction > 0
4518
+ ? Math.floor(current / step) * step + step
4519
+ : Math.ceil(current / step) * step - step;
4520
+ if (snapped < 0 || snapped >= 24 * 60)
4521
+ return;
4522
+ const candidate = fromMinutes(snapped);
4523
+ const min = this.minTime();
4524
+ const max = this.maxTime();
4525
+ if ((min && candidate < min) || (max && candidate > max))
4526
+ return; // fence
4527
+ this.setValue(candidate);
4528
+ }
4529
+ // --- Keyboard ----------------------------------------------------------------
4530
+ onInputKeydown(event) {
4531
+ const element = this.inputRef().nativeElement;
4532
+ switch (event.key) {
4533
+ case 'ArrowDown':
4534
+ case 'ArrowUp': {
4535
+ event.preventDefault();
4536
+ const direction = event.key === 'ArrowDown' ? 1 : -1;
4537
+ // A committed value with the list closed steps like a spinner —
4538
+ // ArrowUp means later, like the date field's ArrowUp means tomorrow —
4539
+ // while the list opens from an empty or edited field (or the toggle).
4540
+ if (!this.list.open() && this.value() && element.value === this.displayText()) {
4541
+ this.stepValue(direction === 1 ? -1 : 1);
4542
+ break;
4543
+ }
4544
+ if (!this.list.open()) {
4545
+ this.openList();
4546
+ if (this.list.activeIndex() < 0)
4547
+ this.list.setActive(direction === 1 ? 0 : this.options().length - 1);
4548
+ }
4549
+ else {
4550
+ const count = this.options().length;
4551
+ this.list.setActive((this.list.activeIndex() + direction + count) % count);
4552
+ }
4553
+ this.scrollToActive();
4554
+ break;
4555
+ }
4556
+ case 'Enter': {
4557
+ event.preventDefault();
4558
+ const active = this.list.activeIndex();
4559
+ if (this.list.open() && active >= 0)
4560
+ this.setValue(this.options()[active]);
4561
+ else
4562
+ this.commit(element.value);
4563
+ this.list.hide();
4564
+ break;
4565
+ }
4566
+ case 'Escape':
4567
+ if (this.list.open()) {
4568
+ this.list.hide();
4569
+ }
4570
+ else {
4571
+ this.setFieldText(this.displayText());
4572
+ this.draftInvalid.set(false);
4573
+ }
4574
+ break;
4575
+ case 'Tab':
4576
+ // Never trap: commit what is typed, then let focus move on.
4577
+ if (element.value.trim() && element.value !== this.displayText())
4578
+ this.commit(element.value);
4579
+ this.list.hide();
4580
+ break;
4581
+ }
4582
+ }
4583
+ onInput() {
4584
+ this.draftInvalid.set(false);
4585
+ // Typing never selects an option — Enter must commit the draft, not
4586
+ // whatever happens to sit nearest; the list only scrolls alongside.
4587
+ this.list.show();
4588
+ this.list.setActive(-1);
4589
+ const parsed = parseTimeText(this.inputRef().nativeElement.value, this.resolvedHour12());
4590
+ if (parsed) {
4591
+ const options = this.options();
4592
+ let nearest = options.findIndex((time) => time >= parsed);
4593
+ if (nearest < 0)
4594
+ nearest = options.length - 1;
4595
+ this.scrollToIndex(nearest);
4596
+ }
4597
+ }
4598
+ onToggle() {
4599
+ if (this.list.open()) {
4600
+ this.list.hide();
4601
+ }
4602
+ else {
4603
+ this.openList();
4604
+ this.inputRef()?.nativeElement.focus();
4605
+ }
4606
+ }
4607
+ selectOption(time) {
4608
+ this.setValue(time);
4609
+ this.list.hide();
4610
+ this.inputRef()?.nativeElement.focus();
4611
+ }
4612
+ onFocusOut(event) {
4613
+ const next = event.relatedTarget;
4614
+ if (next && this.host.nativeElement.contains(next))
4615
+ return;
4616
+ this.list.hide();
4617
+ this.touched.set(true);
4618
+ const element = this.inputRef()?.nativeElement;
4619
+ if (element && this.commitOnBlur() && element.value !== this.displayText())
4620
+ this.commit(element.value);
4621
+ }
4622
+ // --- Internals -------------------------------------------------------------------
4623
+ openList() {
4624
+ this.list.show();
4625
+ const committed = this.value();
4626
+ if (committed)
4627
+ this.list.setActive(this.options().indexOf(committed));
4628
+ this.scrollToActive();
4629
+ }
4630
+ scrollToActive() {
4631
+ this.scrollToIndex(this.list.activeIndex());
4632
+ }
4633
+ scrollToIndex(index) {
4634
+ if (index < 0)
4635
+ return;
4636
+ queueMicrotask(() => this.listRef()?.nativeElement.children[index]?.scrollIntoView?.({ block: 'nearest' }));
4637
+ }
4638
+ setFieldText(text) {
4639
+ const element = this.inputRef()?.nativeElement;
4640
+ if (element)
4641
+ element.value = text;
4642
+ }
4643
+ announce(message) {
4644
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
4645
+ }
4646
+ // --- Styling -----------------------------------------------------------------------
4647
+ className = computed(() => css({ display: 'block', position: 'relative' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4648
+ rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
4649
+ inputClass = computed(() => {
4650
+ const colors = this.theme.colorPalette();
4651
+ return css([
4652
+ {
4653
+ flex: 1,
4654
+ minWidth: 0,
4655
+ border: 0,
4656
+ outline: 'none',
4657
+ background: 'transparent',
4658
+ color: 'inherit',
4659
+ font: 'inherit',
4660
+ },
4661
+ this.draftInvalid() && {
4662
+ color: colors['warn'],
4663
+ // Shape and colour, not colour alone (WCAG 1.4.1).
4664
+ textDecoration: `underline dashed ${colors['warn']} 1.5px`,
4665
+ textUnderlineOffset: 3,
4666
+ },
4667
+ ]);
4668
+ }, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
4669
+ toggleWrapClass = computed(() => css({ display: 'flex', alignItems: 'center', ...this.theme.paddingRight('xxs') }), ...(ngDevMode ? [{ debugName: "toggleWrapClass" }] : /* istanbul ignore next */ []));
4670
+ /** Embedded mode: the composer owns the box; keep only the flex row. */
4671
+ embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4672
+ listClass = computed(() => {
4673
+ const options = this.componentOptions();
4674
+ return css({
4675
+ position: 'absolute',
4676
+ top: '100%',
4677
+ left: 0,
4678
+ right: 0,
4679
+ zIndex: 20,
4680
+ margin: '4px 0 0',
4681
+ padding: 4,
4682
+ listStyle: 'none',
4683
+ maxHeight: (options.maxVisibleOptions ?? 7) * 36,
4684
+ overflowY: 'auto',
4685
+ ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
4686
+ ...this.theme.boxShadow(options.listShadow ?? 'menu'),
4687
+ ...this.theme.radius(options.listBorderRadius ?? 'xs'),
4688
+ '& [role="option"]': {
4689
+ padding: '8px 12px',
4690
+ cursor: 'pointer',
4691
+ ...this.theme.typeface('label'),
4692
+ ...this.theme.color('on-primary-surface'),
4693
+ ...this.theme.radius('xxs'),
4694
+ '&.active, &:hover': {
4695
+ ...this.theme.backgroundColor('primary-container'),
4696
+ ...this.theme.color('on-primary-container'),
4697
+ },
4698
+ },
4699
+ });
4700
+ }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4701
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4702
+ 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 });
4703
+ }
4704
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, decorators: [{
4705
+ type: Component,
4706
+ 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" }]
4707
+ }], 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 }] }] } });
4708
+
4709
+ /**
4710
+ * One field for a date and a time: a thin composer seating a uni-date-input
4711
+ * and a uni-time-input in one input-box chrome under one label, yielding one
4712
+ * combined `'YYYY-MM-DDTHH:mm'` value. The value emits only when both parts
4713
+ * are set — a time without a day is not an answer — and clearing the date
4714
+ * clears it. With `slotsFor`, the time part stays disabled until a day is
4715
+ * chosen and offers exactly that day's slots: the scheduling flow in one
4716
+ * attribute. Two honest tab stops (it is two questions); apps needing a
4717
+ * different arrangement compose the primitives directly.
4718
+ */
4719
+ class UniDateTimeInputComponent extends BaseComponent {
4720
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4721
+ value = model(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
4722
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4723
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4724
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4725
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4726
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4727
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4728
+ // --- Configuration -------------------------------------------------------
4729
+ /** Names the group; the parts are announced as "Date" and "Time" under it. */
4730
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4731
+ /** Earliest allowed moment; the date and time fences are split from it. */
4732
+ minDateTime = input(...(ngDevMode ? [undefined, { debugName: "minDateTime" }] : /* istanbul ignore next */ []));
4733
+ /** Latest allowed moment; the date and time fences are split from it. */
4734
+ maxDateTime = input(...(ngDevMode ? [undefined, { debugName: "maxDateTime" }] : /* istanbul ignore next */ []));
4735
+ // --- Forwarded wholesale ----------------------------------------------------
4736
+ disabledDates = input(...(ngDevMode ? [undefined, { debugName: "disabledDates" }] : /* istanbul ignore next */ []));
4737
+ markers = input([], ...(ngDevMode ? [{ debugName: "markers" }] : /* istanbul ignore next */ []));
4738
+ /** Fixed time choices; superseded per-day by `slotsFor` when that is set. */
4739
+ slots = input(...(ngDevMode ? [undefined, { debugName: "slots" }] : /* istanbul ignore next */ []));
4740
+ minuteStep = input(30, ...(ngDevMode ? [{ debugName: "minuteStep" }] : /* istanbul ignore next */ []));
4741
+ hour12 = input(...(ngDevMode ? [undefined, { debugName: "hour12" }] : /* istanbul ignore next */ []));
4742
+ weekStart = input(...(ngDevMode ? [undefined, { debugName: "weekStart" }] : /* istanbul ignore next */ []));
4743
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
4744
+ /** Scheduling: the day's available times. Gates the time part on a date. */
4745
+ slotsFor = input(...(ngDevMode ? [undefined, { debugName: "slotsFor" }] : /* istanbul ignore next */ []));
4746
+ host = inject(ElementRef);
4747
+ /**
4748
+ * The two part-values. An external `value` write re-derives both; an
4749
+ * internal partial state (a date without a time round-trips through
4750
+ * `undefined`) must not be wiped by its own echo.
4751
+ */
4752
+ parts = linkedSignal({ ...(ngDevMode ? { debugName: "parts" } : /* istanbul ignore next */ {}), source: this.value,
4753
+ computation: (value, previous) => {
4754
+ if (value)
4755
+ return splitDateTime(value);
4756
+ const kept = previous?.value;
4757
+ if (kept && joinDateTime(kept.date, kept.time) === undefined)
4758
+ return kept;
4759
+ return {};
4760
+ } });
4761
+ dateValue = computed(() => this.parts().date, ...(ngDevMode ? [{ debugName: "dateValue" }] : /* istanbul ignore next */ []));
4762
+ timeValue = computed(() => this.parts().time, ...(ngDevMode ? [{ debugName: "timeValue" }] : /* istanbul ignore next */ []));
4763
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4764
+ minParts = computed(() => splitDateTime(this.minDateTime()), ...(ngDevMode ? [{ debugName: "minParts" }] : /* istanbul ignore next */ []));
4765
+ maxParts = computed(() => splitDateTime(this.maxDateTime()), ...(ngDevMode ? [{ debugName: "maxParts" }] : /* istanbul ignore next */ []));
4766
+ dateMin = computed(() => this.minParts().date, ...(ngDevMode ? [{ debugName: "dateMin" }] : /* istanbul ignore next */ []));
4767
+ dateMax = computed(() => this.maxParts().date, ...(ngDevMode ? [{ debugName: "dateMax" }] : /* istanbul ignore next */ []));
4768
+ // The time fence applies only on the boundary date itself — 'after 9:00'
4769
+ // on the min date, any time on later days.
4770
+ timeMin = computed(() => {
4771
+ const { date, time } = this.minParts();
4772
+ return time && date && this.dateValue() === date ? time : undefined;
4773
+ }, ...(ngDevMode ? [{ debugName: "timeMin" }] : /* istanbul ignore next */ []));
4774
+ timeMax = computed(() => {
4775
+ const { date, time } = this.maxParts();
4776
+ return time && date && this.dateValue() === date ? time : undefined;
4777
+ }, ...(ngDevMode ? [{ debugName: "timeMax" }] : /* istanbul ignore next */ []));
4778
+ /** The chosen day's slots when `slotsFor` is set, else the fixed list. */
4779
+ effectiveSlots = computed(() => {
4780
+ const slotsFor = this.slotsFor();
4781
+ if (!slotsFor)
4782
+ return this.slots();
4783
+ const date = this.dateValue();
4784
+ return date ? slotsFor(date) : [];
4785
+ }, ...(ngDevMode ? [{ debugName: "effectiveSlots" }] : /* istanbul ignore next */ []));
4786
+ timeDisabled = computed(() => this.disabled() || (!!this.slotsFor() && !this.dateValue()), ...(ngDevMode ? [{ debugName: "timeDisabled" }] : /* istanbul ignore next */ []));
4787
+ // --- Part plumbing -----------------------------------------------------------
4788
+ onDatePartChange(date) {
4789
+ let time = this.parts().time;
4790
+ if (!date) {
4791
+ time = undefined; // clearing the date clears the combined value
4792
+ }
4793
+ else {
4794
+ const slotsFor = this.slotsFor();
4795
+ // Changing the day clears a slot that no longer exists.
4796
+ if (slotsFor && time && !slotsFor(date).includes(time))
4797
+ time = undefined;
4798
+ }
4799
+ this.setParts({ date, time });
4800
+ }
4801
+ onTimePartChange(time) {
4802
+ this.setParts({ date: this.parts().date, time });
4803
+ }
4804
+ setParts(parts) {
4805
+ this.parts.set(parts);
4806
+ this.value.set(joinDateTime(parts.date, parts.time));
4807
+ }
4808
+ onHostFocusOut(event) {
4809
+ const next = event.relatedTarget;
4810
+ if (!next || !this.host.nativeElement.contains(next))
4811
+ this.touched.set(true);
4812
+ }
4813
+ // --- Styling --------------------------------------------------------------------
4814
+ className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4815
+ groupClass = computed(() => css({
4816
+ display: 'flex',
4817
+ alignItems: 'stretch',
4818
+ flex: 1,
4819
+ width: '100%',
4820
+ minWidth: 0,
4821
+ ...this.theme.gap(this.componentOptions().partGap ?? 'sm'),
4822
+ }), ...(ngDevMode ? [{ debugName: "groupClass" }] : /* istanbul ignore next */ []));
4823
+ datePartClass = computed(() => css({ flex: 1.4, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "datePartClass" }] : /* istanbul ignore next */ []));
4824
+ timePartClass = computed(() => css({ flex: 1, minWidth: 0, display: 'flex', '& > *': { flex: 1, minWidth: 0 } }), ...(ngDevMode ? [{ debugName: "timePartClass" }] : /* istanbul ignore next */ []));
4825
+ dividerClass = computed(() => {
4826
+ const colors = this.theme.colorPalette();
4827
+ return css({
4828
+ width: 1,
4829
+ alignSelf: 'stretch',
4830
+ flex: 'none',
4831
+ backgroundColor: colors[this.componentOptions().dividerColor ?? 'outline'],
4832
+ });
4833
+ }, ...(ngDevMode ? [{ debugName: "dividerClass" }] : /* istanbul ignore next */ []));
4834
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4835
+ 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 });
4836
+ }
4837
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, decorators: [{
4838
+ type: Component,
4839
+ 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" }]
4840
+ }], 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 }] }] } });
4841
+
3080
4842
  class UniDialogComponent extends BaseComponent {
3081
4843
  elem = inject(ElementRef);
3082
4844
  /** Two-way bindable open state: [(show)]. */
@@ -3482,203 +5244,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3482
5244
  }]
3483
5245
  }], 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
5246
 
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
5247
  class UniExpandComponent extends BaseComponent {
3683
5248
  collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
3684
5249
  /**
@@ -4169,57 +5734,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4169
5734
  * This file exports all public-facing elements of the file-drop-zone component.
4170
5735
  */
4171
5736
 
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
5737
  /**
4224
5738
  * Text input that emits `change` only after the user pauses typing. Wears the
4225
5739
  * shared input chrome (themed color, border, typeface, focus ring) via
@@ -4586,7 +6100,11 @@ class UniMenuComponent {
4586
6100
  Enter/Space activation is dispatched from onMenuKeydown. -->
4587
6101
  <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
4588
6102
  <div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
4589
- @for (item of menuItems(); track item) {
6103
+ <!-- track $index: items carry no stable key (template items have no
6104
+ label, labels may repeat, dividers have neither) and consumers
6105
+ naturally rebuild the array each CD pass — identity tracking
6106
+ recreated every node and tripped NG0956. -->
6107
+ @for (item of menuItems(); track $index) {
4590
6108
  @if (isDivider(item)) {
4591
6109
  <div role="separator" [class]="dividerClassName()"></div>
4592
6110
  } @else {
@@ -4642,7 +6160,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4642
6160
  Enter/Space activation is dispatched from onMenuKeydown. -->
4643
6161
  <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
4644
6162
  <div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
4645
- @for (item of menuItems(); track item) {
6163
+ <!-- track $index: items carry no stable key (template items have no
6164
+ label, labels may repeat, dividers have neither) and consumers
6165
+ naturally rebuild the array each CD pass — identity tracking
6166
+ recreated every node and tripped NG0956. -->
6167
+ @for (item of menuItems(); track $index) {
4646
6168
  @if (isDivider(item)) {
4647
6169
  <div role="separator" [class]="dividerClassName()"></div>
4648
6170
  } @else {
@@ -4710,11 +6232,11 @@ class UniMultiSelectComponent {
4710
6232
  width: '100%',
4711
6233
  });
4712
6234
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4713
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectComponent, isStandalone: true, selector: "uni-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, selections: { classPropertyName: "selections", publicName: "selections", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, checkboxGap: { classPropertyName: "checkboxGap", publicName: "checkboxGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updates: "updates" }, host: { properties: { "class": "className" } }, ngImport: i0, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n @for (option of options(); track option) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { 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 });
6235
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectComponent, isStandalone: true, selector: "uni-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, selections: { classPropertyName: "selections", publicName: "selections", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, checkboxGap: { classPropertyName: "checkboxGap", publicName: "checkboxGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updates: "updates" }, host: { properties: { "class": "className" } }, ngImport: i0, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n <!-- track $index: option values may be objects and consumers rebuild the\n array, so identity tracking recreated every node (NG0956). -->\n @for (option of options(); track $index) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { 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 });
4714
6236
  }
4715
6237
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, decorators: [{
4716
6238
  type: Component,
4717
- args: [{ selector: 'uni-multi-select', imports: [UniCheckboxComponent, UniBoxComponent], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n @for (option of options(); track option) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n" }]
6239
+ args: [{ selector: 'uni-multi-select', imports: [UniCheckboxComponent, UniBoxComponent], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n <!-- track $index: option values may be objects and consumers rebuild the\n array, so identity tracking recreated every node (NG0956). -->\n @for (option of options(); track $index) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n" }]
4718
6240
  }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], selections: [{ type: i0.Input, args: [{ isSignal: true, alias: "selections", required: false }] }], updates: [{ type: i0.Output, args: ["updates"] }], flexDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexDirection", required: false }] }], checkboxGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkboxGap", required: false }] }] } });
4719
6241
 
4720
6242
  class UniMultiSelectDropdownComponent extends BaseComponent {
@@ -4928,7 +6450,9 @@ class UniNotificationBadgeComponent extends BaseComponent {
4928
6450
  const variant = this.badgeVariant();
4929
6451
  const color = this.color();
4930
6452
  const position = this.position();
4931
- const offset = this.componentOptions().offset + 'px';
6453
+ // A theme may omit `offset`; without the fallback this concatenates to
6454
+ // the invalid length 'undefinedpx' and the badge loses its position.
6455
+ const offset = (this.componentOptions().offset ?? 0) + 'px';
4932
6456
  const positionStyles = {
4933
6457
  'top-right': { top: offset, right: offset },
4934
6458
  'top-left': { top: offset, left: offset },
@@ -5533,6 +7057,13 @@ class UniRadioComponent extends BaseComponent {
5533
7057
  });
5534
7058
  radioOptionClass = computed(() => {
5535
7059
  const { outerCircleSize, innerCircleSize, innerCircleOffset } = this.metrics();
7060
+ // The dot's grow/retract is a token: 0.3s default, 0 = instant. The
7061
+ // transitions are scoped — never `all` — so the focus ring's outline and
7062
+ // shadow apply instantly instead of interpolating from a stale outline
7063
+ // color, which flashed a dark ring before the themed ring color landed.
7064
+ const speed = this.componentOptions().transitionSpeed ?? 0.3;
7065
+ const ringTransition = `border-color ${speed}s ease, background-color ${speed}s ease`;
7066
+ const dotTransition = `transform ${speed}s ease`;
5536
7067
  return css({
5537
7068
  userSelect: 'none',
5538
7069
  cursor: this.disabled() ? 'not-allowed' : 'pointer',
@@ -5548,7 +7079,7 @@ class UniRadioComponent extends BaseComponent {
5548
7079
  ? this.getThemeColor('on-disabled')
5549
7080
  : this.getThemeColor(this.componentOptions().ringColor ?? 'outline')}`,
5550
7081
  position: 'relative',
5551
- transition: 'all 0.3s ease',
7082
+ transition: ringTransition,
5552
7083
  backgroundColor: this.getThemeColor(this.componentOptions().fillColor ?? 'surface'),
5553
7084
  flexShrink: 0,
5554
7085
  },
@@ -5561,7 +7092,7 @@ class UniRadioComponent extends BaseComponent {
5561
7092
  top: innerCircleOffset,
5562
7093
  left: innerCircleOffset,
5563
7094
  transform: 'scale(0)',
5564
- transition: 'all 0.3s ease',
7095
+ transition: dotTransition,
5565
7096
  },
5566
7097
  '&:hover .radio-button': this.disabled()
5567
7098
  ? {}
@@ -5589,9 +7120,9 @@ class UniRadioComponent extends BaseComponent {
5589
7120
  '&:checked + .radio-button .radio-inner': {
5590
7121
  transform: 'scale(1)',
5591
7122
  },
7123
+ // The shared, themable focus indicator, keyed off the hidden input.
5592
7124
  '&:focus + .radio-button': {
5593
- outline: `2px solid ${this.getThemeColor(this.variant())}`,
5594
- outlineOffset: '2px',
7125
+ ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
5595
7126
  },
5596
7127
  }), ...(ngDevMode ? [{ debugName: "radioInputClass" }] : /* istanbul ignore next */ []));
5597
7128
  handleRadioChange(optionValue) {
@@ -5765,6 +7296,14 @@ class UniSelectComponent {
5765
7296
  // --- CONFIGURATION ---
5766
7297
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
5767
7298
  placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
7299
+ /**
7300
+ * Equality used to match `value` against option values, called as
7301
+ * `compareWith(optionValue, value)`. Defaults to reference equality, which
7302
+ * never matches an object value rebuilt from elsewhere (e.g. a saved record
7303
+ * against options from a fresh fetch) — pass a key comparison like
7304
+ * `(a, b) => a?.id === b?.id` for object values.
7305
+ */
7306
+ compareWith = input((a, b) => a === b, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
5768
7307
  /** Accessible name for the select; a placeholder is not a label. */
5769
7308
  ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
5770
7309
  UNSELECTED = -1;
@@ -5777,7 +7316,7 @@ class UniSelectComponent {
5777
7316
  if (currentVal === null || currentVal === undefined)
5778
7317
  return this.UNSELECTED;
5779
7318
  // Find the index of the option that contains our current value
5780
- const index = this.options().findIndex((opt) => opt.value === currentVal);
7319
+ const index = this.options().findIndex((opt) => this.compareWith()(opt.value, currentVal));
5781
7320
  return index.toString();
5782
7321
  }, ...(ngDevMode ? [{ debugName: "currentSelectedIndex" }] : /* istanbul ignore next */ []));
5783
7322
  showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
@@ -5809,12 +7348,12 @@ class UniSelectComponent {
5809
7348
  pointerEvents: 'none' /* Crucial for clicking through */,
5810
7349
  });
5811
7350
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5812
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", 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 }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7351
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", 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 }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5813
7352
  }
5814
7353
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, decorators: [{
5815
7354
  type: Component,
5816
7355
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-select', imports: [UniInputBoxComponent, UniSymbolComponent], template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n" }]
5817
- }], 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
7356
+ }], 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
5818
7357
 
5819
7358
  /**
5820
7359
  * Loading placeholder painted with surface tokens. `text` renders one or more
@@ -5978,7 +7517,8 @@ class UniSliderComponent extends BaseComponent {
5978
7517
  '&::-moz-range-track': { height: trackHeight, borderRadius: radius, background: track },
5979
7518
  '&::-moz-range-progress': { height: trackHeight, borderRadius: radius, background: fill },
5980
7519
  '&::-moz-range-thumb': thumb,
5981
- '&:focus-visible': { outline: `2px solid ${fill}`, outlineOffset: 2 },
7520
+ // The shared, themable focus indicator, in the track's fill color.
7521
+ '&:focus-visible': { ...this.theme.focusRingStyle(fill) },
5982
7522
  '&:disabled': {
5983
7523
  cursor: 'not-allowed',
5984
7524
  opacity: 0.5,
@@ -7963,7 +9503,7 @@ class UniThemeSwitchComponent {
7963
9503
  (valueChange)="select($event)"
7964
9504
  [ariaLabel]="ariaLabel()"
7965
9505
  />
7966
- `, isInline: true, dependencies: [{ kind: "component", type: UniSelectComponent, selector: "uni-select", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "options", "placeholder", "ariaLabel"], outputs: ["valueChange", "touchedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9506
+ `, isInline: true, dependencies: [{ kind: "component", type: UniSelectComponent, selector: "uni-select", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "options", "placeholder", "compareWith", "ariaLabel"], outputs: ["valueChange", "touchedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7967
9507
  }
7968
9508
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniThemeSwitchComponent, decorators: [{
7969
9509
  type: Component,
@@ -8035,7 +9575,9 @@ class UniToggleComponent extends BaseComponent {
8035
9575
  : this.getThemeColor(this.componentOptions().trackColor ?? 'surface-variant'),
8036
9576
  borderRadius: toggleSize / 2,
8037
9577
  position: 'relative',
8038
- transition: 'all 0.3s ease',
9578
+ // Scoped, never `all`: the focus ring must apply instantly rather
9579
+ // than interpolating its outline color from a stale value.
9580
+ transition: 'background-color 0.3s ease, border-color 0.3s ease',
8039
9581
  },
8040
9582
  '& .toggle-slider': {
8041
9583
  width: sliderSize,
@@ -8045,7 +9587,7 @@ class UniToggleComponent extends BaseComponent {
8045
9587
  position: 'absolute',
8046
9588
  top: sliderOffset,
8047
9589
  left: sliderOffset,
8048
- transition: 'all 0.3s ease',
9590
+ transition: 'transform 0.3s ease, background-color 0.3s ease',
8049
9591
  ...this.theme.boxShadow('raised'),
8050
9592
  },
8051
9593
  // Hover darkens whatever the token resolves to — the button convention.
@@ -8074,9 +9616,9 @@ class UniToggleComponent extends BaseComponent {
8074
9616
  '&:disabled + .toggle-switch': {
8075
9617
  cursor: 'not-allowed',
8076
9618
  },
9619
+ // The shared, themable focus indicator, keyed off the hidden input.
8077
9620
  '&:focus + .toggle-switch': {
8078
- outline: `2px solid ${this.getThemeColor(this.variant())}`,
8079
- outlineOffset: '2px',
9621
+ ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
8080
9622
  },
8081
9623
  });
8082
9624
  }, ...(ngDevMode ? [{ debugName: "toggleInput" }] : /* istanbul ignore next */ []));
@@ -8298,5 +9840,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
8298
9840
  * Generated bundle index. Do not edit.
8299
9841
  */
8300
9842
 
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 };
9843
+ 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
9844
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map