@uni-design-system/uni-angular 8.1.0 → 8.3.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
- 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';
2
+ import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, contentChildren, output, Renderer2, ElementRef, Directive, model, viewChild, effect, untracked, 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, removeInputPlatformStyling, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, 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, removeInputPlatformStyling, fadeIn, 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;
@@ -361,6 +361,7 @@ class ListboxNavigation {
361
361
  _open = signal(false, ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
362
362
  _activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "_activeIndex" }] : /* istanbul ignore next */ []));
363
363
  wrap;
364
+ isDisabled;
364
365
  /** Whether the popup is showing. Also false when there is nothing to show. */
365
366
  open = computed(() => this._open() && this.config.count() > 0, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
366
367
  /** Index of the highlighted option, or -1 when none is active. */
@@ -378,6 +379,7 @@ class ListboxNavigation {
378
379
  this.config = config;
379
380
  this.listboxId = uniqueId(config.idPrefix ?? 'uni-listbox');
380
381
  this.wrap = config.wrap ?? true;
382
+ this.isDisabled = config.disabled ?? (() => false);
381
383
  }
382
384
  /** Stable per-option id, for `role="option"` elements. */
383
385
  optionId(index) {
@@ -418,20 +420,35 @@ class ListboxNavigation {
418
420
  return this.step(current, 1, count);
419
421
  case 'ArrowUp':
420
422
  // Opening with ArrowUp lands on the last option, matching menus.
421
- return current < 0 ? count - 1 : this.step(current, -1, count);
423
+ return current < 0 ? this.seek(count - 1, -1, count) : this.step(current, -1, count);
422
424
  case 'Home':
423
- return 0;
425
+ return this.seek(0, 1, count);
424
426
  case 'End':
425
- return count - 1;
427
+ return this.seek(count - 1, -1, count);
426
428
  default:
427
429
  return null;
428
430
  }
429
431
  }
432
+ /** Nearest enabled index at or after `start`, walking by `delta` with wrap;
433
+ null when every option is disabled. */
434
+ seek(start, delta, count) {
435
+ for (let i = start, n = 0; n < count; n++, i += delta) {
436
+ const index = (i + count) % count;
437
+ if (!this.isDisabled(index))
438
+ return index;
439
+ }
440
+ return null;
441
+ }
430
442
  step(current, delta, count) {
431
- const next = current + delta;
432
443
  if (this.wrap)
433
- return (next + count) % count;
434
- return Math.min(Math.max(next, 0), count - 1);
444
+ return this.seek((current + delta + count) % count, delta, count);
445
+ // Clamped: walk toward the boundary only; no enabled candidate → hold position.
446
+ const from = Math.min(Math.max(current + delta, 0), count - 1);
447
+ for (let i = from; i >= 0 && i < count; i += delta) {
448
+ if (!this.isDisabled(i))
449
+ return i;
450
+ }
451
+ return current >= 0 && !this.isDisabled(current) ? current : null;
435
452
  }
436
453
  /** Close when focus leaves the control entirely (not on internal moves). */
437
454
  closeOnFocusOut(event) {
@@ -499,29 +516,59 @@ function anchorStyles(anchor, placement, offset = {}) {
499
516
  }
500
517
  return styles;
501
518
  }
519
+ /**
520
+ * The half of the arrow square kept by `clip-path`, in the square's local
521
+ * (pre-rotation) coordinates: the outer triangle, cut exactly on the
522
+ * panel-edge diagonal. Without the clip, the two bordered edges run the full
523
+ * square and their strokes visibly cut into the panel surface. The diagonal
524
+ * cut ends the strokes precisely at the panel edge, and the panel's own
525
+ * border line — which sits just *outside* that edge — falls inside the kept
526
+ * half, so the arrow background covers it and the base reads as an opening.
527
+ * Keyed by the side of the anchor the panel sits on.
528
+ *
529
+ * A positive `overlap` extends the kept half past the diagonal INTO the
530
+ * panel — only for borderless arrows that want anti-aliasing insurance along
531
+ * the base; on a bordered arrow it turns the stroke ends into visible stubs
532
+ * inside the panel body.
533
+ */
534
+ const ARROW_CLIP = {
535
+ // Arrow at the panel's top edge, borders top + left → keep the top-left half.
536
+ bottom: (d) => `polygon(0 0, ${past(d)} 0, 0 ${past(d)})`,
537
+ // Arrow at the panel's bottom edge, borders right + bottom.
538
+ top: (d) => `polygon(${before(d)} 100%, 100% ${before(d)}, 100% 100%)`,
539
+ // Arrow at the panel's right edge, borders right + top.
540
+ left: (d) => `polygon(${before(d)} 0, 100% 0, 100% ${past(d)})`,
541
+ // Arrow at the panel's left edge, borders left + bottom.
542
+ right: (d) => `polygon(0 ${before(d)}, ${past(d)} 100%, 0 100%)`,
543
+ };
544
+ const past = (d) => (d ? `calc(100% + ${d}px)` : '100%');
545
+ const before = (d) => (d ? `calc(0% - ${d}px)` : '0');
502
546
  /**
503
547
  * Absolute positioning for a rotated-square arrow sitting on the panel edge
504
548
  * that faces the anchor. Static per placement: if the browser flips the panel
505
549
  * via position-try, the arrow stays on the configured side (cosmetic-only
506
550
  * degradation, matching the flip-support caveat above).
551
+ *
552
+ * `overlap` (default 0 — cut exactly on the panel edge) extends the kept half
553
+ * into the panel; see {@link ARROW_CLIP} for when that is ever wanted.
507
554
  */
508
- function anchorArrowStyles(placement, size = 8) {
555
+ function anchorArrowStyles(placement, size = 8, overlap = 0) {
509
556
  const [side, align] = placement.split('-');
510
- const overlap = -(size / 2);
511
557
  const styles = {
512
558
  position: 'absolute',
513
559
  width: size,
514
560
  height: size,
515
561
  transform: 'rotate(45deg)',
562
+ clipPath: ARROW_CLIP[side](overlap),
516
563
  };
517
- // Edge facing the anchor
564
+ // Edge facing the anchor: half the square pokes out past the panel edge
518
565
  const facing = {
519
566
  top: 'bottom',
520
567
  bottom: 'top',
521
568
  left: 'right',
522
569
  right: 'left',
523
570
  };
524
- styles[facing[side]] = overlap;
571
+ styles[facing[side]] = -(size / 2);
525
572
  // Cross-axis position along that edge
526
573
  const horizontal = side === 'top' || side === 'bottom';
527
574
  const crossProp = horizontal ? 'left' : 'top';
@@ -537,6 +584,147 @@ function anchorArrowStyles(placement, size = 8) {
537
584
  }
538
585
  return styles;
539
586
  }
587
+ /**
588
+ * Scrim pieces that cut a spotlight hole around the element carrying
589
+ * `anchor-name: <anchor>`. Every inset is `calc(anchor(<side>) ± px)`, so the
590
+ * browser tracks the target through scroll/resize/layout with zero listeners.
591
+ *
592
+ * The window paints everything: its `border` is the focus ring and its huge
593
+ * spread shadow is the scrim (an outer shadow of a rounded rect covers the
594
+ * whole viewport except the rect, rounded corners included). It is
595
+ * click-through; the four strips do the click-blocking, and the optional
596
+ * cover blocks the hole itself. Callers append these to a fixed, transparent,
597
+ * `pointer-events: none` full-viewport container (each piece is
598
+ * `position: fixed`).
599
+ */
600
+ function spotlightStyles(anchor, options = {}) {
601
+ const pad = options.pad ?? 6;
602
+ const ringWidth = options.ringWidth ?? 2;
603
+ const scrimColor = options.scrimColor ?? 'rgba(0, 0, 0, 0.45)';
604
+ const holeInset = (extra) => Object.fromEntries(['top', 'left', 'right', 'bottom'].map((side) => [
605
+ side,
606
+ `calc(anchor(${side}) - ${pad + extra}px)`,
607
+ ]));
608
+ const piece = { position: 'fixed', positionAnchor: anchor };
609
+ const blocker = { ...piece, pointerEvents: 'auto', background: 'transparent' };
610
+ return {
611
+ window: {
612
+ ...piece,
613
+ ...holeInset(ringWidth), // the ring sits outside the padded hole
614
+ pointerEvents: 'none',
615
+ borderWidth: ringWidth,
616
+ borderStyle: 'solid',
617
+ boxShadow: `0 0 0 200vmax ${scrimColor}`,
618
+ },
619
+ strips: {
620
+ top: { ...blocker, top: 0, left: 0, right: 0, bottom: `calc(anchor(top) + ${pad}px)` },
621
+ bottom: { ...blocker, bottom: 0, left: 0, right: 0, top: `calc(anchor(bottom) + ${pad}px)` },
622
+ left: {
623
+ ...blocker,
624
+ left: 0,
625
+ right: `calc(anchor(left) + ${pad}px)`,
626
+ top: `calc(anchor(top) - ${pad}px)`,
627
+ bottom: `calc(anchor(bottom) - ${pad}px)`,
628
+ },
629
+ right: {
630
+ ...blocker,
631
+ right: 0,
632
+ left: `calc(anchor(right) + ${pad}px)`,
633
+ top: `calc(anchor(top) - ${pad}px)`,
634
+ bottom: `calc(anchor(bottom) - ${pad}px)`,
635
+ },
636
+ },
637
+ cover: { ...blocker, ...holeInset(0) },
638
+ };
639
+ }
640
+
641
+ /**
642
+ * Shared plumbing for top-layer overlays built on the native `popover`
643
+ * attribute (dropdown, popover, callout). Pure functions and data — no DOM
644
+ * ownership — so each component keeps its own template while the anchor
645
+ * bookkeeping, discrete-transition block, and focus-restore rule stay single-
646
+ * sourced.
647
+ */
648
+ /** Placement → `transform-origin`, so scale animations grow from the anchor. */
649
+ const TRANSFORM_ORIGINS = {
650
+ top: 'bottom center',
651
+ right: 'center left',
652
+ bottom: 'top center',
653
+ left: 'center right',
654
+ 'top-start': 'bottom left',
655
+ 'top-end': 'bottom right',
656
+ 'right-start': 'top left',
657
+ 'right-end': 'bottom left',
658
+ 'bottom-start': 'top left',
659
+ 'bottom-end': 'top right',
660
+ 'left-start': 'top right',
661
+ 'left-end': 'bottom right',
662
+ };
663
+ /** Write a CSS `anchor-name` onto an element (detached anchors and targets). */
664
+ function setAnchorName(el, name) {
665
+ el.style.setProperty('anchor-name', name);
666
+ }
667
+ function clearAnchorName(el) {
668
+ el.style.removeProperty('anchor-name');
669
+ }
670
+ /**
671
+ * Resolve an element-or-id reference. Id strings are looked up at call time —
672
+ * never cache the result across opens, the element may have been re-rendered.
673
+ * `''` and null-ish mean "unset" (falsy guard: an empty string is the only
674
+ * typeable empty for a string-typed input).
675
+ */
676
+ function resolveElement(ref) {
677
+ if (!ref)
678
+ return null;
679
+ return typeof ref === 'string' ? document.getElementById(ref) : ref;
680
+ }
681
+ /** True when a native `toggle` event reports the overlay opening. */
682
+ function isToggleOpen(event) {
683
+ return event.newState === 'open';
684
+ }
685
+ /**
686
+ * The discrete-transition block that animates an element into and out of the
687
+ * top layer: `transition-behavior: allow-discrete` over the `hidden` keys plus
688
+ * `display`/`overlay`, the shown state under `:popover-open`, and
689
+ * `@starting-style` so entry transitions run from the hidden state.
690
+ */
691
+ function discreteOverlayTransition(durationMs, hidden, shown) {
692
+ return {
693
+ transitionProperty: [...Object.keys(hidden), 'display', 'overlay']
694
+ .map((key) => key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`))
695
+ .join(', '),
696
+ transitionDuration: `${durationMs}ms`,
697
+ transitionBehavior: 'allow-discrete',
698
+ ...hidden,
699
+ '&:popover-open': shown,
700
+ '@starting-style': {
701
+ '&:popover-open': hidden,
702
+ },
703
+ };
704
+ }
705
+ /**
706
+ * Returns focus to `target` when an overlay closes while focus was inside its
707
+ * panel (or was dropped on `<body>` by the top layer closing), so keyboard
708
+ * users are never stranded (WCAG 2.4.3). Focus resting anywhere else is left
709
+ * alone.
710
+ */
711
+ function restoreOverlayFocus(panel, target) {
712
+ const active = document.activeElement;
713
+ if (active === document.body || (active && panel.contains(active))) {
714
+ target?.focus();
715
+ }
716
+ }
717
+ /**
718
+ * The focusable elements inside `root`, in DOM order. Filters disabled
719
+ * controls, and hidden ones where the environment can tell: jsdom reports
720
+ * `offsetParent` as null for every element, so the visibility filter applies
721
+ * only when the document actually lays out (some element has an offsetParent).
722
+ */
723
+ function focusableElements(root) {
724
+ const all = Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => !el.disabled);
725
+ const laidOut = all.some((el) => el.offsetParent !== null);
726
+ return laidOut ? all.filter((el) => el.offsetParent !== null) : all;
727
+ }
540
728
 
541
729
  class UniBaseDatasource {
542
730
  selections = signal([], ...(ngDevMode ? [{ debugName: "selections" }] : /* istanbul ignore next */ []));
@@ -2685,8 +2873,13 @@ class UniCalendarComponent extends BaseComponent {
2685
2873
  announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
2686
2874
  resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
2687
2875
  resolvedWeekStart = computed(() => this.weekStart() ?? localeWeekStart(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "resolvedWeekStart" }] : /* istanbul ignore next */ []));
2688
- /** The month on screen: the `month` model, else the value's month, else today's. */
2689
- viewMonth = computed(() => this.month() ?? monthOf(this.anchorDate() ?? todayIso()), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
2876
+ /**
2877
+ * The month on screen: the `month` model, else the value's month, else
2878
+ * today's. Falsy guards on purpose: `''` — the only typeable empty for a
2879
+ * string-typed model — counts as unset, or it would reach the month math
2880
+ * and blow up the grid and heading.
2881
+ */
2882
+ viewMonth = computed(() => this.month() || monthOf(this.anchorDate() || todayIso()), ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
2690
2883
  anchorDate = computed(() => {
2691
2884
  const value = this.value();
2692
2885
  return this.mode() === 'range'
@@ -3074,6 +3267,389 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3074
3267
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-calendar, Calendar', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'calendar' }], host: { '[class]': 'className()', '(focusout)': 'onHostFocusOut($event)' }, template: "<div [class]=\"navClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navPrevSymbol ?? 'chevron_left'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(-1)\"\n >\n Previous month\n </button>\n <!-- aria-live so PageUp/Down narrates the new month without refocusing. -->\n <div [class]=\"headingClass()\" [id]=\"headingId\" aria-live=\"polite\">{{ heading() }}</div>\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().navNextSymbol ?? 'chevron_right'\"\n [disable]=\"disabled()\"\n (click)=\"onNav(1)\"\n >\n Next month\n </button>\n</div>\n\n<!-- One tab stop: the roving tabindex lives on the day buttons, and the grid\n itself only relays their bubbling keys (same delegation as tag-input). -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div\n role=\"grid\"\n [class]=\"gridClass()\"\n [attr.aria-label]=\"ariaLabel() ?? null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : headingId\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onGridKeydown($event)\"\n>\n <div role=\"row\" [class]=\"rowClass()\">\n @for (weekday of weekdays(); track weekday.full) {\n <div role=\"columnheader\" [class]=\"weekdayClass()\">\n <abbr [attr.title]=\"weekday.full\">{{ weekday.label }}</abbr>\n </div>\n }\n </div>\n\n @for (week of gridWeeks(); track week[0].date) {\n <div role=\"row\" [class]=\"rowClass()\">\n @for (cell of week; track cell.date) {\n @if (cell.outside) {\n <!-- Outside days: hidden placeholders by default (geometry kept),\n muted and non-interactive under the showOutsideDays option. -->\n <div role=\"gridcell\" aria-hidden=\"true\" [class]=\"cell.cellClass\">\n @if (showOutside()) {\n <span [class]=\"outsideDayClass()\">{{ cell.day }}</span>\n }\n </div>\n } @else {\n <div\n role=\"gridcell\"\n [class]=\"cell.cellClass\"\n [attr.aria-selected]=\"cell.selected || cell.inBand ? true : null\"\n >\n <button\n type=\"button\"\n [class]=\"cell.dayClass\"\n [attr.data-date]=\"cell.date\"\n [tabindex]=\"cell.tabIndex\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n [disabled]=\"cell.disabled || disabled()\"\n (click)=\"select(cell.date)\"\n (mouseenter)=\"onDayHover(cell.date)\"\n >\n {{ cell.day }}\n @if (cell.markers.length) {\n <span [class]=\"dotsClass()\">\n @for (marker of cell.markers; track $index) {\n <span data-dot [class]=\"dotClassFor(marker.variant)\"></span>\n }\n </span>\n }\n </button>\n </div>\n }\n }\n </div>\n }\n</div>\n\n<!-- Selections and range progress are otherwise silent for a screen reader. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n" }]
3075
3268
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], month: [{ type: i0.Input, args: [{ isSignal: true, alias: "month", required: false }] }, { type: i0.Output, args: ["monthChange"] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], disabledDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledDates", required: false }] }], markers: [{ type: i0.Input, args: [{ isSignal: true, alias: "markers", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], weekStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "weekStart", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
3076
3269
 
3270
+ /**
3271
+ * Anchored coach-mark panel that dims the page and cuts a spotlight hole
3272
+ * around its target. Both the scrim and the panel are `popover="manual"`
3273
+ * elements shown in order, so they stack deterministically in the top layer
3274
+ * above every app z-index, and every scrim piece is CSS-anchor-positioned to
3275
+ * the target — the hole tracks scroll/resize/layout with zero listeners.
3276
+ *
3277
+ * The panel is a non-modal `role="dialog"` (deliberately no `aria-modal`):
3278
+ * focus moves into it on open, Tab runs a "duet loop" over the panel's
3279
+ * focusables plus the spotlit target while it stays interactive, and on close
3280
+ * focus returns to the pre-open element — unless the user moved into the
3281
+ * target, where it stays.
3282
+ *
3283
+ * Storage-free by rule: `key` and the `dismissed` output are the "don't show
3284
+ * again" hooks; persistence is the app's business (e.g. cdk local-storage).
3285
+ */
3286
+ class UniCalloutComponent extends BaseComponent {
3287
+ destroyRef = inject(DestroyRef);
3288
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
3289
+ /** Identifies this callout in `dismissed` payloads. Empty string = unset. */
3290
+ key = input(...(ngDevMode ? [undefined, { debugName: "key" }] : /* istanbul ignore next */ []));
3291
+ /** Element (or id, resolved at open time) to spotlight. '' = unset. */
3292
+ target = input(...(ngDevMode ? [undefined, { debugName: "target" }] : /* istanbul ignore next */ []));
3293
+ placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
3294
+ /** Defaults to `spotlight` when a target resolves, else `dim`. */
3295
+ backdrop = input(...(ngDevMode ? [undefined, { debugName: "backdrop" }] : /* istanbul ignore next */ []));
3296
+ /** When false, a transparent cover blocks the spotlit target. */
3297
+ targetInteractive = input(true, ...(ngDevMode ? [{ debugName: "targetInteractive" }] : /* istanbul ignore next */ []));
3298
+ /** Gates Escape and the close button. */
3299
+ dismissible = input(true, ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
3300
+ /** When true, scrim clicks close (reason `backdrop`); else the panel pulses. */
3301
+ dismissOnBackdrop = input(false, ...(ngDevMode ? [{ debugName: "dismissOnBackdrop" }] : /* istanbul ignore next */ []));
3302
+ header = input(...(ngDevMode ? [undefined, { debugName: "header" }] : /* istanbul ignore next */ []));
3303
+ arrow = input(true, ...(ngDevMode ? [{ debugName: "arrow" }] : /* istanbul ignore next */ []));
3304
+ /** Wins over the header/body labelling; the tour sets "{title}, step n of N". */
3305
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
3306
+ /** Accessible name of the dismiss button (the tour passes its skip label). */
3307
+ closeLabel = input('Close', ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
3308
+ opened = output();
3309
+ closed = output();
3310
+ dismissed = output();
3311
+ /** Keys pressed while focus is inside the panel (the tour's arrow-key hook). */
3312
+ panelKeydown = output();
3313
+ panelId = uniqueId('uni-callout');
3314
+ headerId = uniqueId('uni-callout-header');
3315
+ bodyId = uniqueId('uni-callout-body');
3316
+ anchorName = newAnchorName();
3317
+ showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
3318
+ activeTarget = signal(null, ...(ngDevMode ? [{ debugName: "activeTarget" }] : /* istanbul ignore next */ []));
3319
+ scrimShown = signal(false, ...(ngDevMode ? [{ debugName: "scrimShown" }] : /* istanbul ignore next */ []));
3320
+ /**
3321
+ * Keeps the spotlight pieces rendered and the panel anchored through the
3322
+ * close fade — tearing the anchor down at close time made the panel jump
3323
+ * and the scrim vanish mid-transition.
3324
+ */
3325
+ scrimContent = signal(false, ...(ngDevMode ? [{ debugName: "scrimContent" }] : /* istanbul ignore next */ []));
3326
+ pendingTeardown = null;
3327
+ prevFocus = null;
3328
+ detachKeydown = null;
3329
+ stripSides = ['top', 'bottom', 'left', 'right'];
3330
+ scrimRef = viewChild.required('scrim');
3331
+ panelRef = viewChild.required('panel');
3332
+ actionsRef = viewChild.required('actions');
3333
+ constructor() {
3334
+ super();
3335
+ effect(() => {
3336
+ const want = this.open();
3337
+ if (want === untracked(this.showing))
3338
+ return;
3339
+ if (want)
3340
+ this.show();
3341
+ else
3342
+ this.close('programmatic');
3343
+ });
3344
+ // Re-seat an open callout when its target or backdrop changes (a tour
3345
+ // stepping) instead of closing and reopening — the panel stays up, the
3346
+ // original pre-open focus is preserved for the eventual close, and the
3347
+ // duet loop re-seats onto the new step's content.
3348
+ effect(() => {
3349
+ const ref = this.target();
3350
+ this.backdrop();
3351
+ if (!untracked(this.showing))
3352
+ return;
3353
+ untracked(() => this.retarget(resolveElement(ref)));
3354
+ });
3355
+ this.destroyRef.onDestroy(() => {
3356
+ this.detachKeydown?.();
3357
+ if (this.pendingTeardown)
3358
+ clearTimeout(this.pendingTeardown);
3359
+ const target = untracked(this.activeTarget);
3360
+ if (target)
3361
+ clearAnchorName(target);
3362
+ for (const ref of [this.panelRef, this.scrimRef]) {
3363
+ try {
3364
+ ref().nativeElement.hidePopover();
3365
+ }
3366
+ catch {
3367
+ // popover was already closed or detached
3368
+ }
3369
+ }
3370
+ });
3371
+ }
3372
+ effectiveBackdrop = computed(() => this.backdrop() ?? (this.activeTarget() ? 'spotlight' : 'dim'), ...(ngDevMode ? [{ debugName: "effectiveBackdrop" }] : /* istanbul ignore next */ []));
3373
+ show() {
3374
+ if (this.showing())
3375
+ return;
3376
+ if (this.pendingTeardown) {
3377
+ clearTimeout(this.pendingTeardown);
3378
+ this.pendingTeardown = null;
3379
+ }
3380
+ const target = resolveElement(this.target());
3381
+ const stale = this.activeTarget();
3382
+ if (stale && stale !== target)
3383
+ clearAnchorName(stale); // a close was mid-fade
3384
+ this.activeTarget.set(target);
3385
+ this.scrimContent.set(true);
3386
+ this.prevFocus = document.activeElement;
3387
+ // Anchor insets need the target on screen before the scrim paints; the
3388
+ // hole then tracks any further movement natively.
3389
+ target?.scrollIntoView?.({ block: 'center', behavior: 'instant' });
3390
+ if (target)
3391
+ setAnchorName(target, this.anchorName);
3392
+ // Scrim first, panel second: top-layer order is stacking order.
3393
+ if ((this.backdrop() ?? (target ? 'spotlight' : 'dim')) !== 'none') {
3394
+ this.scrimRef().nativeElement.showPopover();
3395
+ this.scrimShown.set(true);
3396
+ }
3397
+ this.panelRef().nativeElement.showPopover();
3398
+ const onKeydown = (event) => this.onKeydown(event);
3399
+ document.addEventListener('keydown', onKeydown, true);
3400
+ this.detachKeydown = () => {
3401
+ document.removeEventListener('keydown', onKeydown, true);
3402
+ this.detachKeydown = null;
3403
+ };
3404
+ this.showing.set(true);
3405
+ this.open.set(true);
3406
+ this.focusInitial();
3407
+ this.opened.emit();
3408
+ }
3409
+ close(reason) {
3410
+ if (!this.showing())
3411
+ return;
3412
+ this.detachKeydown?.();
3413
+ const target = this.activeTarget();
3414
+ const active = document.activeElement;
3415
+ const focusInTarget = !!target && (target === active || target.contains(active));
3416
+ for (const ref of [this.panelRef, this.scrimRef]) {
3417
+ try {
3418
+ ref().nativeElement.hidePopover();
3419
+ }
3420
+ catch {
3421
+ // scrim may not have been shown (backdrop="none")
3422
+ }
3423
+ }
3424
+ this.showing.set(false);
3425
+ this.scrimShown.set(false);
3426
+ this.open.set(false);
3427
+ // The close fade is still running: the panel must stay anchored and the
3428
+ // spotlight pieces rendered until it finishes, or everything jumps.
3429
+ this.pendingTeardown = setTimeout(() => {
3430
+ this.pendingTeardown = null;
3431
+ if (target)
3432
+ clearAnchorName(target);
3433
+ this.activeTarget.set(null);
3434
+ this.scrimContent.set(false);
3435
+ }, this.componentOptions().transitionMs);
3436
+ // The duet loop may have sent the user into the target on purpose — don't
3437
+ // yank them back out of it.
3438
+ if (!focusInTarget && this.prevFocus && document.contains(this.prevFocus)) {
3439
+ this.prevFocus.focus();
3440
+ }
3441
+ this.dismissed.emit({ key: this.key() || undefined, reason });
3442
+ this.closed.emit();
3443
+ }
3444
+ /** Move an open callout to a new target without a close/open round trip. */
3445
+ retarget(target) {
3446
+ const prev = this.activeTarget();
3447
+ if (prev && prev !== target)
3448
+ clearAnchorName(prev);
3449
+ target?.scrollIntoView?.({ block: 'center', behavior: 'instant' });
3450
+ if (target)
3451
+ setAnchorName(target, this.anchorName);
3452
+ this.activeTarget.set(target);
3453
+ const wantScrim = (this.backdrop() ?? (target ? 'spotlight' : 'dim')) !== 'none';
3454
+ if (wantScrim !== this.scrimShown()) {
3455
+ const scrim = this.scrimRef().nativeElement;
3456
+ const panel = this.panelRef().nativeElement;
3457
+ try {
3458
+ if (wantScrim) {
3459
+ // Top-layer order is stacking order: re-show the panel above the
3460
+ // late-arriving scrim.
3461
+ scrim.showPopover();
3462
+ panel.hidePopover();
3463
+ panel.showPopover();
3464
+ }
3465
+ else {
3466
+ scrim.hidePopover();
3467
+ }
3468
+ }
3469
+ catch {
3470
+ // a piece was already in the requested state
3471
+ }
3472
+ this.scrimShown.set(wantScrim);
3473
+ }
3474
+ this.focusInitial();
3475
+ }
3476
+ /** `[autofocus]` → first action → first panel focusable → the panel itself. */
3477
+ focusInitial() {
3478
+ const panel = this.panelRef().nativeElement;
3479
+ const first = panel.querySelector('[autofocus]') ??
3480
+ focusableElements(this.actionsRef().nativeElement)[0] ??
3481
+ focusableElements(panel)[0] ??
3482
+ panel;
3483
+ first.focus({ preventScroll: true });
3484
+ }
3485
+ /**
3486
+ * Capture phase so Escape works from inside the spotlit target and the tab
3487
+ * loop preempts the page. Non-Tab keys surface through `panelKeydown`.
3488
+ */
3489
+ onKeydown(event) {
3490
+ if (event.key === 'Escape') {
3491
+ if (this.dismissible()) {
3492
+ event.preventDefault();
3493
+ this.close('escape');
3494
+ }
3495
+ return;
3496
+ }
3497
+ if (event.key !== 'Tab') {
3498
+ if (this.panelRef().nativeElement.contains(document.activeElement)) {
3499
+ this.panelKeydown.emit(event);
3500
+ }
3501
+ return;
3502
+ }
3503
+ if (this.effectiveBackdrop() === 'none')
3504
+ return; // nothing is blocked — tab naturally
3505
+ // Duet loop: panel focusables plus the interactive target.
3506
+ const loop = focusableElements(this.panelRef().nativeElement);
3507
+ const target = this.activeTarget();
3508
+ if (target && this.targetInteractive())
3509
+ loop.push(target);
3510
+ if (!loop.length)
3511
+ return;
3512
+ const index = loop.indexOf(document.activeElement);
3513
+ event.preventDefault();
3514
+ const next = event.shiftKey
3515
+ ? loop[(index <= 0 ? loop.length : index) - 1]
3516
+ : loop[(index + 1) % loop.length];
3517
+ next.focus();
3518
+ }
3519
+ /** A stray click shouldn't kill onboarding: nudge the panel instead. */
3520
+ onBackdropClick() {
3521
+ if (this.dismissOnBackdrop()) {
3522
+ this.close('backdrop');
3523
+ return;
3524
+ }
3525
+ try {
3526
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches)
3527
+ return;
3528
+ this.panelRef().nativeElement.animate?.([
3529
+ { translate: '0 0' },
3530
+ { translate: '0 -3px', offset: 0.3 },
3531
+ { translate: '0 2px', offset: 0.6 },
3532
+ { translate: '0 0' },
3533
+ ], { duration: 400, easing: 'ease' });
3534
+ }
3535
+ catch {
3536
+ // animation is decoration; environments without WAAPI just skip it
3537
+ }
3538
+ }
3539
+ // --- styling --------------------------------------------------------------
3540
+ spotlight = computed(() => {
3541
+ const options = this.componentOptions();
3542
+ return spotlightStyles(this.anchorName, {
3543
+ pad: options.spotlightPadding,
3544
+ ringWidth: options.ringWidth,
3545
+ scrimColor: options.scrimColor,
3546
+ });
3547
+ }, ...(ngDevMode ? [{ debugName: "spotlight" }] : /* istanbul ignore next */ []));
3548
+ scrimClassName = computed(() => {
3549
+ const options = this.componentOptions();
3550
+ return css({
3551
+ position: 'fixed',
3552
+ inset: 0,
3553
+ width: '100%',
3554
+ height: '100%',
3555
+ margin: 0,
3556
+ border: 0,
3557
+ padding: 0,
3558
+ background: 'transparent',
3559
+ overflow: 'visible',
3560
+ pointerEvents: 'none',
3561
+ '& > *': { position: 'fixed' },
3562
+ ...discreteOverlayTransition(options.transitionMs, { opacity: 0 }, { opacity: 1 }),
3563
+ });
3564
+ }, ...(ngDevMode ? [{ debugName: "scrimClassName" }] : /* istanbul ignore next */ []));
3565
+ windowClassName = computed(() => {
3566
+ const ring = this.theme.colorPalette()[this.variant()] ?? this.theme.colorPalette()['primary'];
3567
+ return css({
3568
+ ...this.spotlight().window,
3569
+ ...this.theme.radius(this.componentOptions().spotlightRadius),
3570
+ borderColor: ring,
3571
+ });
3572
+ }, ...(ngDevMode ? [{ debugName: "windowClassName" }] : /* istanbul ignore next */ []));
3573
+ stripClassName(side) {
3574
+ return css(this.spotlight().strips[side]);
3575
+ }
3576
+ coverClassName = computed(() => css(this.spotlight().cover), ...(ngDevMode ? [{ debugName: "coverClassName" }] : /* istanbul ignore next */ []));
3577
+ fullCoverClassName = computed(() => css({
3578
+ inset: 0,
3579
+ background: this.componentOptions().scrimColor,
3580
+ pointerEvents: 'auto',
3581
+ }), ...(ngDevMode ? [{ debugName: "fullCoverClassName" }] : /* istanbul ignore next */ []));
3582
+ panelClassName = computed(() => {
3583
+ const options = this.componentOptions();
3584
+ const anchored = this.activeTarget() !== null;
3585
+ return css({
3586
+ ...this.theme.colorPair(options.color),
3587
+ ...this.theme.radius(options.borderRadius),
3588
+ ...this.theme.boxShadow(options.shadow),
3589
+ ...this.theme.typeface(options.typeface),
3590
+ width: options.width,
3591
+ maxWidth: 'calc(100vw - 32px)',
3592
+ padding: 0,
3593
+ border: 0,
3594
+ overflow: 'visible',
3595
+ // A target-less panel keeps the popover UA styles (inset:0 + margin:auto)
3596
+ // and centers in the viewport; anchorStyles resets them for anchoring.
3597
+ ...(anchored
3598
+ ? anchorStyles(this.anchorName, this.placement(), {
3599
+ mainAxis: options.offset + options.spotlightPadding,
3600
+ })
3601
+ : {}),
3602
+ ...discreteOverlayTransition(options.transitionMs, { opacity: 0, translate: '0 6px' }, { opacity: 1, translate: '0 0' }),
3603
+ });
3604
+ }, ...(ngDevMode ? [{ debugName: "panelClassName" }] : /* istanbul ignore next */ []));
3605
+ headRowClassName = computed(() => {
3606
+ const options = this.componentOptions();
3607
+ return css({
3608
+ display: 'flex',
3609
+ alignItems: 'center',
3610
+ gap: 8,
3611
+ padding: options.padding,
3612
+ paddingBottom: 0,
3613
+ '&:not(:has(*))': { display: 'none' },
3614
+ });
3615
+ }, ...(ngDevMode ? [{ debugName: "headRowClassName" }] : /* istanbul ignore next */ []));
3616
+ titleClassName = computed(() => css({ ...this.theme.typeface(this.componentOptions().headerTypeface), marginRight: 'auto' }), ...(ngDevMode ? [{ debugName: "titleClassName" }] : /* istanbul ignore next */ []));
3617
+ bodyClassName = computed(() => {
3618
+ const options = this.componentOptions();
3619
+ return css({ padding: options.padding, paddingTop: 6, paddingBottom: 12, '&:empty': { display: 'none' } });
3620
+ }, ...(ngDevMode ? [{ debugName: "bodyClassName" }] : /* istanbul ignore next */ []));
3621
+ actionsRowClassName = computed(() => {
3622
+ const options = this.componentOptions();
3623
+ return css({
3624
+ display: 'flex',
3625
+ alignItems: 'center',
3626
+ gap: 8,
3627
+ padding: options.padding,
3628
+ paddingTop: 0,
3629
+ '&:not(:has(*))': { display: 'none' },
3630
+ });
3631
+ }, ...(ngDevMode ? [{ debugName: "actionsRowClassName" }] : /* istanbul ignore next */ []));
3632
+ arrowClassName = computed(() => {
3633
+ const options = this.componentOptions();
3634
+ return css({
3635
+ ...this.theme.colorPair(options.color),
3636
+ ...anchorArrowStyles(this.placement(), options.arrowSize),
3637
+ });
3638
+ }, ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
3639
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalloutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3640
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniCalloutComponent, isStandalone: true, selector: "uni-callout", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, targetInteractive: { classPropertyName: "targetInteractive", publicName: "targetInteractive", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, dismissOnBackdrop: { classPropertyName: "dismissOnBackdrop", publicName: "dismissOnBackdrop", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed", dismissed: "dismissed", panelKeydown: "panelKeydown" }, providers: [{ provide: COMPONENT_NAME, useValue: 'callout' }], viewQueries: [{ propertyName: "scrimRef", first: true, predicate: ["scrim"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "actionsRef", first: true, predicate: ["actions"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Scrim first: top-layer order is stacking order, so the panel always sits\n above it. Strips and cover are pointer-blockers only; the keyboard path\n is the duet loop on the document keydown handler. -->\n<div #scrim popover=\"manual\" [class]=\"scrimClassName()\">\n @if (scrimContent()) {\n @if (effectiveBackdrop() === 'spotlight' && activeTarget()) {\n <div [class]=\"windowClassName()\"></div>\n @for (side of stripSides; track side) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"stripClassName(side)\" (click)=\"onBackdropClick()\"></div>\n }\n @if (!targetInteractive()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"coverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n } @else if (effectiveBackdrop() === 'dim') {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"fullCoverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n }\n</div>\n<div\n #panel\n popover=\"manual\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : header() ? headerId : bodyId\"\n [class]=\"panelClassName()\"\n>\n <ng-content select=\"[callout-media]\"></ng-content>\n <div [class]=\"headRowClassName()\">\n <ng-content select=\"[callout-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (dismissible()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"close('close-button')\"\n >\n {{ closeLabel() }}\n </button>\n }\n </div>\n <div [id]=\"bodyId\" [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div #actions [class]=\"actionsRowClassName()\"><ng-content select=\"[callout-actions]\"></ng-content></div>\n @if (arrow() && activeTarget()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\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 });
3641
+ }
3642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCalloutComponent, decorators: [{
3643
+ type: Component,
3644
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-callout', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'callout' }], template: "<!-- Scrim first: top-layer order is stacking order, so the panel always sits\n above it. Strips and cover are pointer-blockers only; the keyboard path\n is the duet loop on the document keydown handler. -->\n<div #scrim popover=\"manual\" [class]=\"scrimClassName()\">\n @if (scrimContent()) {\n @if (effectiveBackdrop() === 'spotlight' && activeTarget()) {\n <div [class]=\"windowClassName()\"></div>\n @for (side of stripSides; track side) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"stripClassName(side)\" (click)=\"onBackdropClick()\"></div>\n }\n @if (!targetInteractive()) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"coverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n } @else if (effectiveBackdrop() === 'dim') {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div [class]=\"fullCoverClassName()\" (click)=\"onBackdropClick()\"></div>\n }\n }\n</div>\n<div\n #panel\n popover=\"manual\"\n role=\"dialog\"\n tabindex=\"-1\"\n [id]=\"panelId\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabel() ? null : header() ? headerId : bodyId\"\n [class]=\"panelClassName()\"\n>\n <ng-content select=\"[callout-media]\"></ng-content>\n <div [class]=\"headRowClassName()\">\n <ng-content select=\"[callout-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (dismissible()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"close('close-button')\"\n >\n {{ closeLabel() }}\n </button>\n }\n </div>\n <div [id]=\"bodyId\" [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div #actions [class]=\"actionsRowClassName()\"><ng-content select=\"[callout-actions]\"></ng-content></div>\n @if (arrow() && activeTarget()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\n" }]
3645
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], targetInteractive: [{ type: i0.Input, args: [{ isSignal: true, alias: "targetInteractive", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], dismissOnBackdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissOnBackdrop", required: false }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], arrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrow", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], panelKeydown: [{ type: i0.Output, args: ["panelKeydown"] }], scrimRef: [{ type: i0.ViewChild, args: ['scrim', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], actionsRef: [{ type: i0.ViewChild, args: ['actions', { isSignal: true }] }] } });
3646
+
3647
+ /**
3648
+ * UniCalloutComponent Barrel File
3649
+ *
3650
+ * This file exports all public-facing elements of the callout component.
3651
+ */
3652
+
3077
3653
  class UniCardContentComponent extends BaseComponent {
3078
3654
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3079
3655
  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 });
@@ -3334,8 +3910,527 @@ class UniCheckboxComponent extends BaseComponent {
3334
3910
  }
3335
3911
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCheckboxComponent, decorators: [{
3336
3912
  type: Component,
3337
- args: [{ selector: 'uni-checkbox', imports: [UniTextComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n" }]
3338
- }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }] } });
3913
+ 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" }]
3914
+ }], 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"] }] } });
3915
+
3916
+ /**
3917
+ * Style block for the popup behind every `ListboxNavigation` consumer: an
3918
+ * absolutely-positioned `ul[role="listbox"]` under a `position: relative`
3919
+ * field wrapper, with the shared option chrome and active/hover highlight.
3920
+ *
3921
+ * Extracted because four components (`uni-search-input`, `uni-tag-input`,
3922
+ * `uni-time-input`, `uni-combobox`) carried hand-rolled copies, and the parts
3923
+ * that silently drift between copies all live here: the surface/elevation
3924
+ * trio, and the `activeColor` pair that themes re-point when their container
3925
+ * tokens don't contrast (see the Wellsourced overrides).
3926
+ *
3927
+ * Compose extras with the array form — `css([listboxPopupStyles(…), {…}])` —
3928
+ * so a component's own `& [role="option"]` block cascades after this one
3929
+ * instead of replacing it (an object spread would overwrite the key).
3930
+ */
3931
+ const listboxPopupStyles = (theme, options, { maxHeight = 280 } = {}) => ({
3932
+ position: 'absolute',
3933
+ top: '100%',
3934
+ left: 0,
3935
+ right: 0,
3936
+ zIndex: 20,
3937
+ margin: '4px 0 0',
3938
+ padding: 4,
3939
+ listStyle: 'none',
3940
+ maxHeight,
3941
+ overflowY: 'auto',
3942
+ ...theme.colorPair((options.listColor ?? 'primary-surface')),
3943
+ ...theme.boxShadow(options.listShadow ?? 'menu'),
3944
+ ...theme.radius(options.listBorderRadius ?? 'xs'),
3945
+ '& [role="option"]': {
3946
+ padding: '8px 12px',
3947
+ cursor: 'pointer',
3948
+ ...theme.typeface('label'),
3949
+ ...theme.radius('xxs'),
3950
+ '&.active, &:not([aria-disabled="true"]):hover': {
3951
+ ...theme.colorPair((options.activeColor ?? 'primary-container')),
3952
+ },
3953
+ },
3954
+ });
3955
+
3956
+ class UniInputBoxComponent extends BaseComponent {
3957
+ className = css({ display: 'contents' });
3958
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3959
+ error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
3960
+ minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
3961
+ /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
3962
+ height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
3963
+ color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3964
+ border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
3965
+ shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
3966
+ inputBoxClass = computed(() => css([
3967
+ this.disabled() && {
3968
+ ...this.theme.color(this.componentOptions().disabledTextColor),
3969
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
3970
+ cursor: 'not-allowed !important',
3971
+ },
3972
+ {
3973
+ '& input, select, textarea': {
3974
+ ...removeInputPlatformStyling,
3975
+ height: '100%',
3976
+ ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
3977
+ ...this.theme.color(this.componentOptions().textColor),
3978
+ // `typeFace` is the deprecated casing; themes that still set it win
3979
+ // only when the canonical key is absent.
3980
+ ...this.theme.typeface(this.componentOptions().typeface ?? this.componentOptions().typeFace),
3981
+ },
3982
+ // Multi-line fields size themselves (rows/resize), not from the box.
3983
+ '& textarea': {
3984
+ height: 'auto',
3985
+ ...this.theme.paddingTop('xs'),
3986
+ ...this.theme.paddingBottom('xs'),
3987
+ },
3988
+ '&:has(input:disabled, select:disabled, textarea:disabled)': {
3989
+ ...this.theme.color(this.componentOptions().disabledTextColor),
3990
+ ...this.theme.backgroundColor(this.componentOptions().disabledColor),
3991
+ },
3992
+ '& input:disabled, select:disabled, textarea:disabled': {
3993
+ cursor: 'not-allowed !important',
3994
+ },
3995
+ '&:has(input:focus, select:focus, textarea:focus)': {
3996
+ outline: this.componentOptions().focusOutline,
3997
+ outlineOffset: this.componentOptions().focusOutlineOffset,
3998
+ // Optional focus chrome (border/ring/background). It yields to the
3999
+ // error state, so a flagged field stays visibly flagged while the
4000
+ // user is in it correcting the value.
4001
+ ...(this.error()
4002
+ ? {}
4003
+ : {
4004
+ ...this.theme.border(this.componentOptions().focusBorder),
4005
+ ...this.theme.boxShadow(this.componentOptions().focusShadow),
4006
+ ...this.theme.backgroundColor(this.componentOptions().focusColor),
4007
+ }),
4008
+ },
4009
+ },
4010
+ ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4011
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4012
+ 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 });
4013
+ }
4014
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4015
+ type: Component,
4016
+ 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" }]
4017
+ }], 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 }] }] } });
4018
+
4019
+ /**
4020
+ * Form-bound, closed-set, single-select autocomplete: `FormValueControl<T | null>`
4021
+ * over object `Options<T>`, type-to-filter, commit-on-select. Typing produces a
4022
+ * draft — a filter, never a value; the value changes only when an option
4023
+ * commits, `clear()` runs, or the model is written from outside. Select
4024
+ * semantics, not search: a real label, a chevron, no magnifier, nothing
4025
+ * submits. For short lists with no need to type, prefer `uni-select` — the
4026
+ * value contract is identical, so swapping is a template-only change.
4027
+ */
4028
+ class UniComboboxComponent extends BaseComponent {
4029
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
4030
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
4031
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4032
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
4033
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
4034
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
4035
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
4036
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
4037
+ // --- Configuration -------------------------------------------------------
4038
+ /** Accessible name for the field, e.g. "State"; echoed on the listbox. */
4039
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
4040
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
4041
+ options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4042
+ /**
4043
+ * Equality used to match `value` against option values, called as
4044
+ * `compareWith(optionValue, value)`. Defaults to reference equality, which
4045
+ * never matches an object value rebuilt from elsewhere (e.g. a saved record
4046
+ * against options from a fresh fetch) — pass a key comparison like
4047
+ * `(a, b) => a?.id === b?.id` for object values.
4048
+ */
4049
+ compareWith = input((a, b) => a === b, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
4050
+ width = input('100%', ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
4051
+ /** Show a ✕ while a value is set; an emptied field on blur also clears. */
4052
+ clearable = input(true, ...(ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []));
4053
+ /** Blur commits an exact-match draft; a non-matching draft reverts. */
4054
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
4055
+ /** i18n-able empty row, also announced when a filter matches nothing. */
4056
+ emptyText = input('No matches', ...(ngDevMode ? [{ debugName: "emptyText" }] : /* istanbul ignore next */ []));
4057
+ // --- Filtering (local by default — the closed set is already in memory) ---
4058
+ /** `false` renders `options` verbatim; narrow them app-side from `query`. */
4059
+ filterLocally = input(true, ...(ngDevMode ? [{ debugName: "filterLocally" }] : /* istanbul ignore next */ []));
4060
+ /** Filter predicate; default is locale-lowercased label-contains. */
4061
+ filterWith = input(...(ngDevMode ? [undefined, { debugName: "filterWith" }] : /* istanbul ignore next */ []));
4062
+ debounceTime = input(250, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ []));
4063
+ // --- Events ---------------------------------------------------------------
4064
+ /** Debounced draft text, for async option lists. */
4065
+ query = output();
4066
+ /** An option committed. */
4067
+ selected = output();
4068
+ cleared = output();
4069
+ /** A commit was refused (no match); the field reverted. */
4070
+ rejected = output();
4071
+ host = inject(ElementRef);
4072
+ inputRef = viewChild.required('field');
4073
+ listRef = viewChild('listbox', ...(ngDevMode ? [{ debugName: "listRef" }] : /* istanbul ignore next */ []));
4074
+ /** Cancelled on destroy — a late tick would emit on a destroyed OutputRef. */
4075
+ queryTimer;
4076
+ constructor() {
4077
+ super();
4078
+ inject(DestroyRef).onDestroy(() => clearTimeout(this.queryTimer));
4079
+ }
4080
+ srOnly = css(visuallyHidden);
4081
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
4082
+ /** null → the field shows the committed label; a string is an uncommitted draft. */
4083
+ draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
4084
+ /**
4085
+ * Popup visibility. Component-owned rather than `list.open()` — the shared
4086
+ * signal gates on `count() > 0`, which would hide the "No matches" row and
4087
+ * misreport `aria-expanded` over an empty filter.
4088
+ */
4089
+ popupOpen = signal(false, ...(ngDevMode ? [{ debugName: "popupOpen" }] : /* istanbul ignore next */ []));
4090
+ /** Index in options() of the option matching value(), else -1. */
4091
+ committedIndex = computed(() => {
4092
+ const value = this.value();
4093
+ if (value === null || value === undefined)
4094
+ return -1;
4095
+ return this.options().findIndex((option) => this.compareWith()(option.value, value));
4096
+ }, ...(ngDevMode ? [{ debugName: "committedIndex" }] : /* istanbul ignore next */ []));
4097
+ /** A value with no matching option renders '' but is preserved — options may
4098
+ still be loading; the field self-heals when they arrive. */
4099
+ committedLabel = computed(() => {
4100
+ const index = this.committedIndex();
4101
+ return index >= 0 ? this.options()[index].label : '';
4102
+ }, ...(ngDevMode ? [{ debugName: "committedLabel" }] : /* istanbul ignore next */ []));
4103
+ displayValue = computed(() => this.draft() ?? this.committedLabel(), ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
4104
+ /** Indices into options() surviving the filter. The draft filters; the
4105
+ committed label does not — reopening always shows the full set. */
4106
+ filteredIndices = computed(() => {
4107
+ const query = this.draft();
4108
+ const all = this.options().map((_, index) => index);
4109
+ if (!this.filterLocally() || query === null || query === '')
4110
+ return all;
4111
+ const filter = this.filterWith() ??
4112
+ ((option, q) => option.label.toLocaleLowerCase().includes(q.toLocaleLowerCase()));
4113
+ return all.filter((index) => filter(this.options()[index], query));
4114
+ }, ...(ngDevMode ? [{ debugName: "filteredIndices" }] : /* istanbul ignore next */ []));
4115
+ /** Shared combobox bookkeeping, in filtered-list positions — the fourth
4116
+ consumer of the contract behind search-input, tag-input and time-input. */
4117
+ list = createListboxNavigation({
4118
+ count: () => this.filteredIndices().length,
4119
+ idPrefix: 'uni-combobox-listbox',
4120
+ disabled: (position) => !!this.options()[this.filteredIndices()[position]]?.disabled,
4121
+ });
4122
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
4123
+ // --- Committing ------------------------------------------------------------
4124
+ commit(optIndex) {
4125
+ const option = this.options()[optIndex];
4126
+ if (!option || option.disabled)
4127
+ return false;
4128
+ this.value.set(option.value);
4129
+ this.draft.set(null);
4130
+ this.closeList();
4131
+ this.setFieldText(this.displayValue());
4132
+ this.announce(`${option.label} selected.`);
4133
+ this.selected.emit(option);
4134
+ return true;
4135
+ }
4136
+ clear() {
4137
+ this.value.set(null);
4138
+ this.draft.set(null);
4139
+ this.closeList();
4140
+ this.setFieldText('');
4141
+ this.announce('Selection cleared.');
4142
+ this.cleared.emit();
4143
+ this.inputRef().nativeElement.focus();
4144
+ }
4145
+ /**
4146
+ * Draft resolution, used by Enter, Tab and blur:
4147
+ * 1. an active option in the list → commit it;
4148
+ * 2. else a unique exact label match (locale-case-insensitive) → commit it;
4149
+ * 3. Enter only: the filter narrowed to exactly one enabled option → commit it
4150
+ * (on Enter the user can see the single candidate; on blur they're gone,
4151
+ * and committing a value they never saw confirmed is how forms grow
4152
+ * mystery data);
4153
+ * 4. else the caller keeps the list open (Enter) or reverts (Tab/blur).
4154
+ */
4155
+ resolveDraft(enterOnly) {
4156
+ const filtered = this.filteredIndices();
4157
+ const active = this.list.activeIndex();
4158
+ if (this.popupOpen() && active >= 0 && active < filtered.length) {
4159
+ return this.commit(filtered[active]);
4160
+ }
4161
+ const draft = this.draft();
4162
+ if (draft === null)
4163
+ return true;
4164
+ if (draft === '') {
4165
+ // An emptied field on Tab/blur is a deliberate "no value" — but only
4166
+ // clearable fields may null the model this way.
4167
+ if (this.clearable() && this.value() !== null && !enterOnly) {
4168
+ this.value.set(null);
4169
+ this.announce('Selection cleared.');
4170
+ this.cleared.emit();
4171
+ }
4172
+ this.draft.set(null);
4173
+ this.setFieldText(this.displayValue());
4174
+ return true;
4175
+ }
4176
+ const normalize = (text) => text.toLocaleLowerCase();
4177
+ const exact = this.options()
4178
+ .map((option, index) => ({ option, index }))
4179
+ .filter(({ option }) => !option.disabled && normalize(option.label) === normalize(draft));
4180
+ if (exact.length === 1)
4181
+ return this.commit(exact[0].index);
4182
+ if (enterOnly) {
4183
+ const enabled = filtered.filter((index) => !this.options()[index].disabled);
4184
+ if (enabled.length === 1)
4185
+ return this.commit(enabled[0]);
4186
+ }
4187
+ return false;
4188
+ }
4189
+ revertDraft() {
4190
+ const draft = this.draft();
4191
+ if (draft === null)
4192
+ return null;
4193
+ this.draft.set(null);
4194
+ this.setFieldText(this.displayValue());
4195
+ return draft;
4196
+ }
4197
+ reject() {
4198
+ const query = this.revertDraft();
4199
+ if (query) {
4200
+ this.announce(`No match for “${query}”.`);
4201
+ this.rejected.emit({ query });
4202
+ }
4203
+ }
4204
+ // --- Keyboard --------------------------------------------------------------
4205
+ onKeydown(event) {
4206
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
4207
+ event.preventDefault();
4208
+ if (event.altKey) {
4209
+ // Alt+Down opens without activating; Alt+Up closes keeping the draft.
4210
+ if (event.key === 'ArrowDown') {
4211
+ if (!this.popupOpen())
4212
+ this.openList(-1);
4213
+ }
4214
+ else {
4215
+ this.closeList();
4216
+ }
4217
+ return;
4218
+ }
4219
+ if (!this.popupOpen()) {
4220
+ this.popupOpen.set(true);
4221
+ this.list.show();
4222
+ if (event.key === 'ArrowDown') {
4223
+ const committed = this.committedFilteredPos();
4224
+ if (committed >= 0) {
4225
+ this.list.setActive(committed);
4226
+ this.scrollToActive();
4227
+ return;
4228
+ }
4229
+ }
4230
+ // Fall through: Down lands on the first enabled option, Up on the last.
4231
+ }
4232
+ if (this.list.navigate(event))
4233
+ this.scrollToActive();
4234
+ return;
4235
+ }
4236
+ if (event.key === 'Home' || event.key === 'End') {
4237
+ // Only while the list is open — closed, they belong to the caret.
4238
+ if (this.popupOpen() && this.list.navigate(event))
4239
+ this.scrollToActive();
4240
+ return;
4241
+ }
4242
+ switch (event.key) {
4243
+ case 'Enter':
4244
+ // Never submits a form while the list is open.
4245
+ if (this.popupOpen())
4246
+ event.preventDefault();
4247
+ if (!this.resolveDraft(true)) {
4248
+ const count = this.filteredIndices().length;
4249
+ this.announce(count === 0 ? `${this.emptyText()}.` : `${count} results. Use the arrow keys.`);
4250
+ }
4251
+ return;
4252
+ case 'Escape':
4253
+ // One layer at a time: close the list, then revert the draft.
4254
+ // Never clears the committed value — Escape on a form control must
4255
+ // not be destructive.
4256
+ if (this.popupOpen())
4257
+ this.closeList();
4258
+ else if (this.draft() !== null)
4259
+ this.revertDraft();
4260
+ return;
4261
+ case 'Tab':
4262
+ // Resolve rules 1–2 (never rule 3), then let focus move on.
4263
+ if (this.commitOnBlur() && !this.resolveDraft(false))
4264
+ this.reject();
4265
+ this.closeList();
4266
+ }
4267
+ }
4268
+ onInput() {
4269
+ const text = this.inputRef().nativeElement.value;
4270
+ this.draft.set(text);
4271
+ this.popupOpen.set(true);
4272
+ this.list.show();
4273
+ // Typing never selects — rules 2/3 make Enter still work without arrowing.
4274
+ this.list.setActive(-1);
4275
+ clearTimeout(this.queryTimer);
4276
+ this.queryTimer = setTimeout(() => {
4277
+ this.query.emit(text);
4278
+ // Filtering is otherwise silent to a screen reader.
4279
+ if (this.filterLocally() && text !== '') {
4280
+ const count = this.filteredIndices().length;
4281
+ this.announce(count === 0 ? `${this.emptyText()}.` : `${count} result${count === 1 ? '' : 's'}.`);
4282
+ }
4283
+ }, this.debounceTime());
4284
+ }
4285
+ // --- Pointer ---------------------------------------------------------------
4286
+ /** Click-to-browse: pointer users get the select affordance. Focus alone
4287
+ never opens — Tab-through forms must not spray popups. */
4288
+ onFieldClick() {
4289
+ if (this.disabled() || this.popupOpen())
4290
+ return;
4291
+ this.openList(this.committedFilteredPos());
4292
+ }
4293
+ onToggleMousedown(event) {
4294
+ event.preventDefault();
4295
+ if (this.disabled())
4296
+ return;
4297
+ this.inputRef().nativeElement.focus();
4298
+ if (this.popupOpen())
4299
+ this.closeList();
4300
+ else
4301
+ this.openList(this.committedFilteredPos());
4302
+ }
4303
+ onOptionClick(optIndex) {
4304
+ if (this.options()[optIndex]?.disabled)
4305
+ return;
4306
+ this.commit(optIndex);
4307
+ this.inputRef().nativeElement.focus();
4308
+ }
4309
+ onFocusOut(event) {
4310
+ const next = event.relatedTarget;
4311
+ if (next && this.host.nativeElement.contains(next))
4312
+ return;
4313
+ this.touched.set(true);
4314
+ if (this.commitOnBlur() && !this.resolveDraft(false))
4315
+ this.reject();
4316
+ else
4317
+ this.draft.set(null);
4318
+ this.closeList();
4319
+ }
4320
+ // --- Internals -------------------------------------------------------------
4321
+ /** Filtered position of the committed option when visible and enabled, else -1. */
4322
+ committedFilteredPos() {
4323
+ const committed = this.committedIndex();
4324
+ if (committed < 0 || this.options()[committed].disabled)
4325
+ return -1;
4326
+ return this.filteredIndices().indexOf(committed);
4327
+ }
4328
+ openList(activeFilteredPos) {
4329
+ this.popupOpen.set(true);
4330
+ this.list.show();
4331
+ this.list.setActive(activeFilteredPos);
4332
+ this.scrollToActive();
4333
+ }
4334
+ closeList() {
4335
+ this.popupOpen.set(false);
4336
+ this.list.hide();
4337
+ }
4338
+ scrollToActive() {
4339
+ const position = this.list.activeIndex();
4340
+ if (position < 0)
4341
+ return;
4342
+ queueMicrotask(() => this.listRef()?.nativeElement.children[position]?.scrollIntoView?.({ block: 'nearest' }));
4343
+ }
4344
+ setFieldText(text) {
4345
+ const element = this.inputRef()?.nativeElement;
4346
+ if (element)
4347
+ element.value = text;
4348
+ }
4349
+ announce(message) {
4350
+ // Re-announce identical text by breaking the string equality.
4351
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
4352
+ }
4353
+ // --- Styling ----------------------------------------------------------------
4354
+ className = computed(() => css({ display: 'block', position: 'relative', width: this.width() }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4355
+ rowClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, width: '100%', minWidth: 0 }), ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
4356
+ inputClass = computed(() => css({
4357
+ flex: 1,
4358
+ minWidth: 0,
4359
+ border: 0,
4360
+ outline: 'none',
4361
+ background: 'transparent',
4362
+ color: 'inherit',
4363
+ font: 'inherit',
4364
+ }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
4365
+ /** Pointer-only affordance: keyboard already has ArrowDown, and the input
4366
+ itself announces expanded state. */
4367
+ toggleClass = computed(() => css({
4368
+ flex: 'none',
4369
+ width: 28,
4370
+ height: 28,
4371
+ display: 'grid',
4372
+ placeItems: 'center',
4373
+ border: 0,
4374
+ padding: 0,
4375
+ background: 'transparent',
4376
+ cursor: this.disabled() ? 'not-allowed' : 'pointer',
4377
+ ...this.theme.color('on-background-variant'),
4378
+ '& uni-icon': {
4379
+ ...motionSafe({ transition: 'transform 0.15s ease' }),
4380
+ transform: this.popupOpen() ? 'rotate(180deg)' : 'none',
4381
+ },
4382
+ }), ...(ngDevMode ? [{ debugName: "toggleClass" }] : /* istanbul ignore next */ []));
4383
+ listClass = computed(() => {
4384
+ const options = this.componentOptions();
4385
+ return css([
4386
+ listboxPopupStyles(this.theme, options, {
4387
+ // A scroll height, never a cap: a closed-set control must not render a
4388
+ // reachable-by-keyboard-only subset (contrast searchInput.maxSuggestions).
4389
+ maxHeight: (options.maxVisibleOptions ?? 8) * 36 + 8,
4390
+ }),
4391
+ {
4392
+ '& [role="option"]': {
4393
+ display: 'flex',
4394
+ alignItems: 'center',
4395
+ gap: 10,
4396
+ minHeight: 36,
4397
+ boxSizing: 'border-box',
4398
+ padding: '5px 10px',
4399
+ '&.active, &:not([aria-disabled="true"]):hover': {
4400
+ '& .check': { color: 'inherit' },
4401
+ '& .desc': { color: 'inherit', opacity: 0.8 },
4402
+ },
4403
+ '&[aria-disabled="true"]': {
4404
+ ...this.theme.color('on-disabled'),
4405
+ cursor: 'not-allowed',
4406
+ '& .desc': { color: 'inherit' },
4407
+ },
4408
+ },
4409
+ // Fixed-width check column so labels align with or without the icon.
4410
+ '& .check': { flex: 'none', width: 18, ...this.theme.color('primary') },
4411
+ '& .text': { minWidth: 0, flex: 1, display: 'flex', alignItems: 'baseline', gap: 8 },
4412
+ '& .option-label': { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
4413
+ '& .desc': {
4414
+ marginLeft: 'auto',
4415
+ whiteSpace: 'nowrap',
4416
+ fontSize: '0.85em',
4417
+ ...this.theme.color(options.descriptionColor ?? 'on-primary-surface-variant'),
4418
+ },
4419
+ '& .empty': {
4420
+ padding: 10,
4421
+ ...this.theme.typeface('label'),
4422
+ ...this.theme.color(options.descriptionColor ?? 'on-primary-surface-variant'),
4423
+ },
4424
+ },
4425
+ ]);
4426
+ }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4427
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniComboboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4428
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniComboboxComponent, isStandalone: true, selector: "uni-combobox, Combobox", 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 }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, filterLocally: { classPropertyName: "filterLocally", publicName: "filterLocally", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", selected: "selected", cleared: "cleared", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'combobox' }], 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 resolves only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\">\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]=\"placeholder() ?? ''\"\n [value]=\"displayValue()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"popupOpen()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput()\"\n (click)=\"onFieldClick()\"\n />\n @if (clearable() && value() !== null && !disabled()) {\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [iconName]=\"componentOptions().clearIcon ?? 'close'\"\n (click)=\"clear()\"\n >\n Clear {{ label() }}\n </button>\n }\n <!-- Pointer-only: keyboard has ArrowDown, the input announces expanded. -->\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [class]=\"toggleClass()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"onToggleMousedown($event)\"\n >\n <uni-icon [name]=\"componentOptions().toggleIcon ?? 'chevronDown'\" size=\"20\" />\n </button>\n </div>\n </uni-input-box>\n\n @if (popupOpen()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @if (filteredIndices().length === 0) {\n <!-- Non-interactive: not an option, not navigable, no id. -->\n <li class=\"empty\">{{ emptyText() }}</li>\n } @else {\n @for (optIndex of filteredIndices(); track optIndex; let i = $index) {\n <!-- aria-selected marks the *committed* option; the active one is\n carried by aria-activedescendant \u2014 they are different facts. -->\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]=\"optIndex === committedIndex()\"\n [attr.aria-disabled]=\"options()[optIndex].disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(optIndex)\"\n >\n <span class=\"check\" aria-hidden=\"true\">\n @if (optIndex === committedIndex()) {\n <uni-icon [name]=\"componentOptions().selectedIcon ?? 'check'\" size=\"18\" />\n }\n </span>\n <span class=\"text\">\n <span class=\"option-label\">{{ options()[optIndex].label }}</span>\n @if (options()[optIndex].description) {\n <span class=\"desc\">{{ options()[optIndex].description }}</span>\n }\n </span>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Commits, clears, refusals and result counts are otherwise silent. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4429
+ }
4430
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniComboboxComponent, decorators: [{
4431
+ type: Component,
4432
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-combobox, Combobox', imports: [UniIconButtonComponent, UniIconComponent, UniInputBoxComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'combobox' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n resolves only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\">\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]=\"placeholder() ?? ''\"\n [value]=\"displayValue()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"popupOpen()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput()\"\n (click)=\"onFieldClick()\"\n />\n @if (clearable() && value() !== null && !disabled()) {\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [iconName]=\"componentOptions().clearIcon ?? 'close'\"\n (click)=\"clear()\"\n >\n Clear {{ label() }}\n </button>\n }\n <!-- Pointer-only: keyboard has ArrowDown, the input announces expanded. -->\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [class]=\"toggleClass()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"onToggleMousedown($event)\"\n >\n <uni-icon [name]=\"componentOptions().toggleIcon ?? 'chevronDown'\" size=\"20\" />\n </button>\n </div>\n </uni-input-box>\n\n @if (popupOpen()) {\n <ul #listbox [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @if (filteredIndices().length === 0) {\n <!-- Non-interactive: not an option, not navigable, no id. -->\n <li class=\"empty\">{{ emptyText() }}</li>\n } @else {\n @for (optIndex of filteredIndices(); track optIndex; let i = $index) {\n <!-- aria-selected marks the *committed* option; the active one is\n carried by aria-activedescendant \u2014 they are different facts. -->\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]=\"optIndex === committedIndex()\"\n [attr.aria-disabled]=\"options()[optIndex].disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(optIndex)\"\n >\n <span class=\"check\" aria-hidden=\"true\">\n @if (optIndex === committedIndex()) {\n <uni-icon [name]=\"componentOptions().selectedIcon ?? 'check'\" size=\"18\" />\n }\n </span>\n <span class=\"text\">\n <span class=\"option-label\">{{ options()[optIndex].label }}</span>\n @if (options()[optIndex].description) {\n <span class=\"desc\">{{ options()[optIndex].description }}</span>\n }\n </span>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Commits, clears, refusals and result counts are otherwise silent. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n" }]
4433
+ }], ctorParameters: () => [], 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], filterLocally: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterLocally", required: false }] }], filterWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterWith", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], query: [{ type: i0.Output, args: ["query"] }], selected: [{ type: i0.Output, args: ["selected"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ['listbox', { isSignal: true }] }] } });
3339
4434
 
3340
4435
  class UniDataSearchComponent extends BaseComponent {
3341
4436
  datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
@@ -3795,7 +4890,7 @@ class UniDataTableComponent extends BaseComponent {
3795
4890
  return name ? this.theme.theme().borders[name] : undefined;
3796
4891
  }
3797
4892
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDataTableComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3798
- 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 });
4893
+ 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 });
3799
4894
  }
3800
4895
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDataTableComponent, decorators: [{
3801
4896
  type: Component,
@@ -3807,7 +4902,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3807
4902
  UniSortHeaderComponent,
3808
4903
  UniIconComponent,
3809
4904
  UniCenterComponent,
3810
- ], 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" }]
4905
+ ], 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" }]
3811
4906
  }], 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"] }] } });
3812
4907
 
3813
4908
  /**
@@ -4013,67 +5108,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4013
5108
  args: ['dropdown', { static: true }]
4014
5109
  }] } });
4015
5110
 
4016
- class UniInputBoxComponent extends BaseComponent {
4017
- className = css({ display: 'contents' });
4018
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4019
- error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
4020
- minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
4021
- /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
4022
- height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4023
- color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4024
- border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
4025
- shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4026
- inputBoxClass = computed(() => css([
4027
- this.disabled() && {
4028
- ...this.theme.color(this.componentOptions().disabledTextColor),
4029
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4030
- cursor: 'not-allowed !important',
4031
- },
4032
- {
4033
- '& input, select, textarea': {
4034
- ...removeInputPlatformStyling,
4035
- height: '100%',
4036
- ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4037
- ...this.theme.color(this.componentOptions().textColor),
4038
- ...this.theme.typeface(this.componentOptions().typeFace),
4039
- },
4040
- // Multi-line fields size themselves (rows/resize), not from the box.
4041
- '& textarea': {
4042
- height: 'auto',
4043
- ...this.theme.paddingTop('xs'),
4044
- ...this.theme.paddingBottom('xs'),
4045
- },
4046
- '&:has(input:disabled, select:disabled, textarea:disabled)': {
4047
- ...this.theme.color(this.componentOptions().disabledTextColor),
4048
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4049
- },
4050
- '& input:disabled, select:disabled, textarea:disabled': {
4051
- cursor: 'not-allowed !important',
4052
- },
4053
- '&:has(input:focus, select:focus, textarea:focus)': {
4054
- outline: this.componentOptions().focusOutline,
4055
- outlineOffset: this.componentOptions().focusOutlineOffset,
4056
- // Optional focus chrome (border/ring/background). It yields to the
4057
- // error state, so a flagged field stays visibly flagged while the
4058
- // user is in it correcting the value.
4059
- ...(this.error()
4060
- ? {}
4061
- : {
4062
- ...this.theme.border(this.componentOptions().focusBorder),
4063
- ...this.theme.boxShadow(this.componentOptions().focusShadow),
4064
- ...this.theme.backgroundColor(this.componentOptions().focusColor),
4065
- }),
4066
- },
4067
- },
4068
- ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4069
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4070
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4071
- }
4072
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4073
- type: Component,
4074
- args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
4075
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4076
-
4077
5111
  /**
4078
5112
  * Date field with free-typed parsing and a popup calendar. Type `aug 20`,
4079
5113
  * `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
@@ -4271,8 +5305,9 @@ class UniDateInputComponent extends BaseComponent {
4271
5305
  this.dropdown()?.hideDropdown();
4272
5306
  }
4273
5307
  onPopupShowing() {
4274
- // The grid opens on the committed value's month (or today's).
4275
- this.calendar()?.month.set(monthOf(this.value() ?? todayIso()));
5308
+ // The grid opens on the committed value's month (or today's). Falsy
5309
+ // guard: a bound '' counts as no value, like everywhere else.
5310
+ this.calendar()?.month.set(monthOf(this.value() || todayIso()));
4276
5311
  this.calendar()?.focusActiveDay();
4277
5312
  this.opened.emit();
4278
5313
  }
@@ -4663,32 +5698,9 @@ class UniTimeInputComponent extends BaseComponent {
4663
5698
  embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4664
5699
  listClass = computed(() => {
4665
5700
  const options = this.componentOptions();
4666
- return css({
4667
- position: 'absolute',
4668
- top: '100%',
4669
- left: 0,
4670
- right: 0,
4671
- zIndex: 20,
4672
- margin: '4px 0 0',
4673
- padding: 4,
4674
- listStyle: 'none',
5701
+ return css(listboxPopupStyles(this.theme, options, {
4675
5702
  maxHeight: (options.maxVisibleOptions ?? 7) * 36,
4676
- overflowY: 'auto',
4677
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
4678
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
4679
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
4680
- '& [role="option"]': {
4681
- padding: '8px 12px',
4682
- cursor: 'pointer',
4683
- ...this.theme.typeface('label'),
4684
- ...this.theme.color('on-primary-surface'),
4685
- ...this.theme.radius('xxs'),
4686
- '&.active, &:hover': {
4687
- ...this.theme.backgroundColor('primary-container'),
4688
- ...this.theme.color('on-primary-container'),
4689
- },
4690
- },
4691
- });
5703
+ }));
4692
5704
  }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4693
5705
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4694
5706
  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 });
@@ -6092,7 +7104,11 @@ class UniMenuComponent {
6092
7104
  Enter/Space activation is dispatched from onMenuKeydown. -->
6093
7105
  <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
6094
7106
  <div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
6095
- @for (item of menuItems(); track item) {
7107
+ <!-- track $index: items carry no stable key (template items have no
7108
+ label, labels may repeat, dividers have neither) and consumers
7109
+ naturally rebuild the array each CD pass — identity tracking
7110
+ recreated every node and tripped NG0956. -->
7111
+ @for (item of menuItems(); track $index) {
6096
7112
  @if (isDivider(item)) {
6097
7113
  <div role="separator" [class]="dividerClassName()"></div>
6098
7114
  } @else {
@@ -6148,7 +7164,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
6148
7164
  Enter/Space activation is dispatched from onMenuKeydown. -->
6149
7165
  <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
6150
7166
  <div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
6151
- @for (item of menuItems(); track item) {
7167
+ <!-- track $index: items carry no stable key (template items have no
7168
+ label, labels may repeat, dividers have neither) and consumers
7169
+ naturally rebuild the array each CD pass — identity tracking
7170
+ recreated every node and tripped NG0956. -->
7171
+ @for (item of menuItems(); track $index) {
6152
7172
  @if (isDivider(item)) {
6153
7173
  <div role="separator" [class]="dividerClassName()"></div>
6154
7174
  } @else {
@@ -6216,11 +7236,11 @@ class UniMultiSelectComponent {
6216
7236
  width: '100%',
6217
7237
  });
6218
7238
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6219
- 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 });
7239
+ 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 });
6220
7240
  }
6221
7241
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, decorators: [{
6222
7242
  type: Component,
6223
- 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" }]
7243
+ 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" }]
6224
7244
  }], 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 }] }] } });
6225
7245
 
6226
7246
  class UniMultiSelectDropdownComponent extends BaseComponent {
@@ -6434,7 +7454,9 @@ class UniNotificationBadgeComponent extends BaseComponent {
6434
7454
  const variant = this.badgeVariant();
6435
7455
  const color = this.color();
6436
7456
  const position = this.position();
6437
- const offset = this.componentOptions().offset + 'px';
7457
+ // A theme may omit `offset`; without the fallback this concatenates to
7458
+ // the invalid length 'undefinedpx' and the badge loses its position.
7459
+ const offset = (this.componentOptions().offset ?? 0) + 'px';
6438
7460
  const positionStyles = {
6439
7461
  'top-right': { top: offset, right: offset },
6440
7462
  'top-left': { top: offset, left: offset },
@@ -6811,34 +7833,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
6811
7833
  * This file exports all public-facing elements of the paginator component.
6812
7834
  */
6813
7835
 
6814
- class UniPopoverComponent {
7836
+ /**
7837
+ * Anchored, top-layer panel on the native `popover` element — the browser owns
7838
+ * positioning (CSS anchor positioning), stacking, and, in rich mode, light
7839
+ * dismissal. Two modes share one implementation:
7840
+ *
7841
+ * - `rich` (default): a click-toggled disclosure. The projected `[trigger]`
7842
+ * carries `aria-expanded`/`aria-controls`; with no trigger content the app
7843
+ * drives `open` and no element claims controller ARIA. Focus stays on the
7844
+ * trigger (APG disclosure) unless the panel marks an `[autofocus]` field,
7845
+ * and returns to the trigger on close.
7846
+ * - `tooltip`: hover/focus-triggered, `role="tooltip"` + `aria-describedby`,
7847
+ * WCAG 1.4.13 dismissable/hoverable/persistent. Content must not contain
7848
+ * focusable elements.
7849
+ *
7850
+ * Anchoring and control are separate: `anchor` re-anchors the panel to any
7851
+ * element (or id) while the trigger keeps the disclosure semantics.
7852
+ */
7853
+ class UniPopoverComponent extends BaseComponent {
6815
7854
  renderer = inject(Renderer2);
6816
- theme = inject(ThemeService);
7855
+ destroyRef = inject(DestroyRef);
6817
7856
  placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
6818
7857
  /** Light-dismiss on outside click / Escape (native popover="auto"). */
6819
7858
  autoClose = input(true, ...(ngDevMode ? [{ debugName: "autoClose" }] : /* istanbul ignore next */ []));
7859
+ /** Two-way open state; light dismissal and hover timers sync back into it. */
7860
+ open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
7861
+ /** `rich` = click-toggled disclosure; `tooltip` = hover/focus description. */
7862
+ mode = input('rich', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
7863
+ /**
7864
+ * Anchor the panel to another element (or element id, resolved each time
7865
+ * the panel opens) while the projected trigger keeps the disclosure ARIA.
7866
+ * Empty string means unset.
7867
+ */
7868
+ anchor = input(...(ngDevMode ? [undefined, { debugName: "anchor" }] : /* istanbul ignore next */ []));
7869
+ /** Title rendered in the header row and used as the accessible name. */
7870
+ header = input(...(ngDevMode ? [undefined, { debugName: "header" }] : /* istanbul ignore next */ []));
7871
+ /** Renders a "Close" icon button in the header row. */
7872
+ closable = input(false, ...(ngDevMode ? [{ debugName: "closable" }] : /* istanbul ignore next */ []));
7873
+ arrow = input(true, ...(ngDevMode ? [{ debugName: "arrow" }] : /* istanbul ignore next */ []));
7874
+ /** Panel max-width; numbers are px. Defaults to the theme's `maxWidth`. */
7875
+ maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
7876
+ /** Tooltip-mode hover-open delay, ms; defaults to the theme option. */
7877
+ openDelay = input(...(ngDevMode ? [undefined, { debugName: "openDelay" }] : /* istanbul ignore next */ []));
7878
+ /** Tooltip-mode pointer-leave close delay, ms; defaults to the theme option. */
7879
+ closeDelay = input(...(ngDevMode ? [undefined, { debugName: "closeDelay" }] : /* istanbul ignore next */ []));
7880
+ opened = output();
7881
+ closed = output();
6820
7882
  panelId = uniqueId('uni-popover');
7883
+ headerId = uniqueId('uni-popover-header');
6821
7884
  anchorName = newAnchorName();
6822
7885
  showing = signal(false, ...(ngDevMode ? [{ debugName: "showing" }] : /* istanbul ignore next */ []));
7886
+ /** Whether the `[trigger]` slot projected any element content. */
7887
+ hasTrigger = signal(false, ...(ngDevMode ? [{ debugName: "hasTrigger" }] : /* istanbul ignore next */ []));
6823
7888
  triggerRef = viewChild.required('trigger');
6824
7889
  panelRef = viewChild.required('panel');
7890
+ openTimer = useTimer();
7891
+ closeTimer = useTimer();
7892
+ /** The element currently carrying our anchor-name. */
7893
+ anchoredEl = null;
7894
+ detachEscape = null;
6825
7895
  constructor() {
6826
- // Wire the ARIA disclosure contract onto the focusable trigger element
7896
+ super();
6827
7897
  afterNextRender(() => {
7898
+ this.hasTrigger.set(this.triggerRef().nativeElement.childElementCount > 0);
7899
+ this.applyAnchor();
7900
+ });
7901
+ // Controller ARIA follows mode/open state; each branch removes the other
7902
+ // mode's attributes so a runtime mode flip never leaves stale semantics.
7903
+ effect(() => {
7904
+ if (!this.hasTrigger())
7905
+ return;
6828
7906
  const target = resolveFocusTarget(this.triggerRef().nativeElement);
6829
- this.renderer.setAttribute(target, 'aria-expanded', 'false');
6830
- this.renderer.setAttribute(target, 'aria-controls', this.panelId);
7907
+ if (this.mode() === 'tooltip') {
7908
+ this.renderer.removeAttribute(target, 'aria-expanded');
7909
+ this.renderer.removeAttribute(target, 'aria-controls');
7910
+ this.renderer.setAttribute(target, 'aria-describedby', this.panelId);
7911
+ }
7912
+ else {
7913
+ this.renderer.removeAttribute(target, 'aria-describedby');
7914
+ this.renderer.setAttribute(target, 'aria-expanded', `${this.showing()}`);
7915
+ this.renderer.setAttribute(target, 'aria-controls', this.panelId);
7916
+ }
7917
+ });
7918
+ // Two-way `open`: drive the native popover when the model changes hands.
7919
+ effect(() => {
7920
+ const want = this.open();
7921
+ if (want === untracked(this.showing))
7922
+ return;
7923
+ if (want)
7924
+ this.showPopover();
7925
+ else
7926
+ this.hidePopover();
7927
+ });
7928
+ this.destroyRef.onDestroy(() => {
7929
+ this.detachEscape?.();
7930
+ if (this.anchoredEl)
7931
+ clearAnchorName(this.anchoredEl);
7932
+ try {
7933
+ this.panelRef().nativeElement.hidePopover();
7934
+ }
7935
+ catch {
7936
+ // popover was already closed or detached
7937
+ }
6831
7938
  });
6832
7939
  }
6833
- onToggle(event) {
6834
- const open = event.newState === 'open';
6835
- this.showing.set(open);
6836
- this.renderer.setAttribute(resolveFocusTarget(this.triggerRef().nativeElement), 'aria-expanded', `${open}`);
6837
- }
7940
+ /** Tooltip mode is always `manual` — its lifecycle belongs to the timers. */
7941
+ popoverKind = computed(() => this.mode() === 'tooltip' ? 'manual' : this.autoClose() ? 'auto' : 'manual', ...(ngDevMode ? [{ debugName: "popoverKind" }] : /* istanbul ignore next */ []));
6838
7942
  showPopover() {
7943
+ if (this.showing())
7944
+ return;
7945
+ this.applyAnchor();
6839
7946
  this.panelRef().nativeElement.showPopover();
6840
7947
  }
6841
7948
  hidePopover() {
7949
+ if (!this.showing())
7950
+ return;
6842
7951
  this.panelRef().nativeElement.hidePopover();
6843
7952
  }
6844
7953
  togglePopover(event) {
@@ -6848,60 +7957,198 @@ class UniPopoverComponent {
6848
7957
  else {
6849
7958
  this.showPopover();
6850
7959
  }
6851
- event.stopPropagation();
7960
+ event?.stopPropagation();
6852
7961
  }
6853
- triggerClassName = css({
6854
- display: 'inline-block',
6855
- anchorName: this.anchorName,
6856
- });
6857
- popoverClassName = computed(() => css([
6858
- {
6859
- ...this.theme.colorPair('primary-surface'),
6860
- ...this.theme.radius('xs'),
6861
- ...this.theme.boxShadow('raised'),
6862
- ...this.theme.typeface('label'),
6863
- ...this.theme.border('quaternary'),
6864
- padding: '6px 12px',
7962
+ /**
7963
+ * The panel anchors to the detached `anchor` when one resolves (id strings
7964
+ * are looked up now, at open time), else to the trigger span.
7965
+ */
7966
+ applyAnchor() {
7967
+ const el = resolveElement(this.anchor()) ?? this.triggerRef().nativeElement;
7968
+ if (this.anchoredEl === el)
7969
+ return;
7970
+ if (this.anchoredEl)
7971
+ clearAnchorName(this.anchoredEl);
7972
+ setAnchorName(el, this.anchorName);
7973
+ this.anchoredEl = el;
7974
+ }
7975
+ onToggle(event) {
7976
+ const isOpen = isToggleOpen(event);
7977
+ this.showing.set(isOpen);
7978
+ this.open.set(isOpen);
7979
+ const panel = this.panelRef().nativeElement;
7980
+ if (isOpen) {
7981
+ if (this.mode() === 'rich') {
7982
+ panel.querySelector('[autofocus]')?.focus();
7983
+ }
7984
+ else {
7985
+ this.attachTooltipEscape();
7986
+ if (typeof ngDevMode !== 'undefined' && ngDevMode && panel.querySelector(FOCUSABLE_SELECTOR)) {
7987
+ console.warn('[uni-popover] tooltip-mode content must not contain focusable elements — ' +
7988
+ 'a tooltip is a description, not a surface. Use mode="rich" instead.');
7989
+ }
7990
+ }
7991
+ this.opened.emit();
7992
+ }
7993
+ else {
7994
+ this.detachEscape?.();
7995
+ if (this.mode() === 'rich' && this.hasTrigger()) {
7996
+ restoreOverlayFocus(panel, resolveFocusTarget(this.triggerRef().nativeElement));
7997
+ }
7998
+ this.closed.emit();
7999
+ }
8000
+ }
8001
+ closeFromButton() {
8002
+ this.hidePopover();
8003
+ if (this.hasTrigger())
8004
+ resolveFocusTarget(this.triggerRef().nativeElement).focus();
8005
+ }
8006
+ // --- rich-mode gesture ----------------------------------------------------
8007
+ onTriggerClick(event) {
8008
+ if (this.mode() !== 'rich')
8009
+ return;
8010
+ this.togglePopover(event);
8011
+ }
8012
+ // --- tooltip-mode gestures (WCAG 1.4.13) ----------------------------------
8013
+ openDelayMs = computed(() => this.openDelay() ?? this.componentOptions().tooltipOpenDelay, ...(ngDevMode ? [{ debugName: "openDelayMs" }] : /* istanbul ignore next */ []));
8014
+ closeDelayMs = computed(() => this.closeDelay() ?? this.componentOptions().tooltipCloseDelay, ...(ngDevMode ? [{ debugName: "closeDelayMs" }] : /* istanbul ignore next */ []));
8015
+ onTriggerEnter() {
8016
+ if (this.mode() !== 'tooltip')
8017
+ return;
8018
+ this.closeTimer.stop();
8019
+ this.openTimer.start(this.openDelayMs(), () => this.showPopover());
8020
+ }
8021
+ onTriggerLeave() {
8022
+ if (this.mode() !== 'tooltip')
8023
+ return;
8024
+ this.openTimer.stop();
8025
+ if (this.showing())
8026
+ this.closeTimer.start(this.closeDelayMs(), () => this.hidePopover());
8027
+ }
8028
+ onTriggerFocusIn() {
8029
+ if (this.mode() !== 'tooltip')
8030
+ return;
8031
+ this.closeTimer.stop();
8032
+ this.showPopover();
8033
+ }
8034
+ onTriggerFocusOut() {
8035
+ if (this.mode() !== 'tooltip')
8036
+ return;
8037
+ this.openTimer.stop();
8038
+ this.closeTimer.stop();
8039
+ this.hidePopover();
8040
+ }
8041
+ /** Resting the pointer inside the panel must not dismiss it (hoverable). */
8042
+ onPanelEnter() {
8043
+ if (this.mode() !== 'tooltip')
8044
+ return;
8045
+ this.closeTimer.stop();
8046
+ }
8047
+ onPanelLeave() {
8048
+ if (this.mode() !== 'tooltip')
8049
+ return;
8050
+ if (this.showing())
8051
+ this.closeTimer.start(this.closeDelayMs(), () => this.hidePopover());
8052
+ }
8053
+ /**
8054
+ * Tooltip panels are `manual`, so Escape is ours: dismiss without moving
8055
+ * focus, and stop propagation so an enclosing dialog stays open. Capture
8056
+ * phase, document-level — the pointer may be nowhere near the trigger.
8057
+ */
8058
+ attachTooltipEscape() {
8059
+ const onKeydown = (event) => {
8060
+ if (event.key !== 'Escape')
8061
+ return;
8062
+ event.stopPropagation();
8063
+ this.openTimer.stop();
8064
+ this.closeTimer.stop();
8065
+ this.hidePopover();
8066
+ };
8067
+ document.addEventListener('keydown', onKeydown, true);
8068
+ this.detachEscape = () => {
8069
+ document.removeEventListener('keydown', onKeydown, true);
8070
+ this.detachEscape = null;
8071
+ };
8072
+ }
8073
+ // --- styling --------------------------------------------------------------
8074
+ triggerClassName = css({ display: 'inline-block' });
8075
+ resolvedMaxWidth = computed(() => {
8076
+ const width = this.maxWidth();
8077
+ if (width === undefined || width === '')
8078
+ return this.componentOptions().maxWidth;
8079
+ return typeof width === 'number' ? `${width}px` : width;
8080
+ }, ...(ngDevMode ? [{ debugName: "resolvedMaxWidth" }] : /* istanbul ignore next */ []));
8081
+ popoverClassName = computed(() => {
8082
+ const options = this.componentOptions();
8083
+ return css({
8084
+ ...this.theme.colorPair(options.color),
8085
+ ...this.theme.radius(options.borderRadius),
8086
+ ...this.theme.boxShadow(options.shadow),
8087
+ ...this.theme.typeface(options.typeface),
8088
+ ...this.theme.border(options.border),
8089
+ padding: 0,
6865
8090
  width: 'max-content',
8091
+ maxWidth: this.resolvedMaxWidth(),
6866
8092
  overflow: 'visible',
6867
- ...anchorStyles(this.anchorName, this.placement(), { mainAxis: 7 }),
6868
- // Fade via discrete transitions across the top layer
6869
- transitionProperty: 'opacity, display, overlay',
6870
- transitionDuration: '250ms',
6871
- transitionBehavior: 'allow-discrete',
6872
- opacity: 0,
6873
- '&:popover-open': {
6874
- opacity: 1,
6875
- },
6876
- '@starting-style': {
6877
- '&:popover-open': {
6878
- opacity: 0,
6879
- },
6880
- },
6881
- },
6882
- ]), ...(ngDevMode ? [{ debugName: "popoverClassName" }] : /* istanbul ignore next */ []));
8093
+ ...anchorStyles(this.anchorName, this.placement(), { mainAxis: options.offset }),
8094
+ ...discreteOverlayTransition(250, { opacity: 0 }, { opacity: 1 }),
8095
+ });
8096
+ }, ...(ngDevMode ? [{ debugName: "popoverClassName" }] : /* istanbul ignore next */ []));
8097
+ /** Empty regions collapse, so bare content renders v1's single-region look. */
8098
+ headerRowClassName = computed(() => {
8099
+ const options = this.componentOptions();
8100
+ return css({
8101
+ display: 'flex',
8102
+ alignItems: 'center',
8103
+ gap: 8,
8104
+ padding: options.padding,
8105
+ paddingBottom: 0,
8106
+ '&:not(:has(*))': { display: 'none' },
8107
+ });
8108
+ }, ...(ngDevMode ? [{ debugName: "headerRowClassName" }] : /* istanbul ignore next */ []));
8109
+ titleClassName = computed(() => css({ ...this.theme.typeface(this.componentOptions().headerTypeface), marginRight: 'auto' }), ...(ngDevMode ? [{ debugName: "titleClassName" }] : /* istanbul ignore next */ []));
8110
+ bodyClassName = computed(() => {
8111
+ const options = this.componentOptions();
8112
+ return css({
8113
+ padding: this.mode() === 'tooltip' ? options.tooltipPadding : options.padding,
8114
+ '&:empty': { display: 'none' },
8115
+ });
8116
+ }, ...(ngDevMode ? [{ debugName: "bodyClassName" }] : /* istanbul ignore next */ []));
8117
+ footerRowClassName = computed(() => {
8118
+ const options = this.componentOptions();
8119
+ return css({
8120
+ display: 'flex',
8121
+ alignItems: 'center',
8122
+ justifyContent: 'flex-end',
8123
+ gap: 8,
8124
+ padding: options.padding,
8125
+ paddingTop: 0,
8126
+ '&:not(:has(*))': { display: 'none' },
8127
+ });
8128
+ }, ...(ngDevMode ? [{ debugName: "footerRowClassName" }] : /* istanbul ignore next */ []));
6883
8129
  arrowClassName = computed(() => {
8130
+ const options = this.componentOptions();
6884
8131
  const side = this.placement().split('-')[0];
6885
- // Border on the two edges of the rotated square that face outward
8132
+ // Border on the two edges of the clipped half that face outward
6886
8133
  const borders = {
6887
- top: { ...this.theme.borderRight('quaternary'), ...this.theme.borderBottom('quaternary') },
6888
- bottom: { ...this.theme.borderLeft('quaternary'), ...this.theme.borderTop('quaternary') },
6889
- left: { ...this.theme.borderRight('quaternary'), ...this.theme.borderTop('quaternary') },
6890
- right: { ...this.theme.borderLeft('quaternary'), ...this.theme.borderBottom('quaternary') },
8134
+ top: { ...this.theme.borderRight(options.border), ...this.theme.borderBottom(options.border) },
8135
+ bottom: { ...this.theme.borderLeft(options.border), ...this.theme.borderTop(options.border) },
8136
+ left: { ...this.theme.borderRight(options.border), ...this.theme.borderTop(options.border) },
8137
+ right: { ...this.theme.borderLeft(options.border), ...this.theme.borderBottom(options.border) },
6891
8138
  };
6892
8139
  return css({
6893
- ...this.theme.colorPair('primary-surface'),
6894
- ...anchorArrowStyles(this.placement()),
8140
+ ...this.theme.colorPair(options.color),
8141
+ ...anchorArrowStyles(this.placement(), options.arrowSize),
6895
8142
  ...borders[side],
6896
8143
  });
6897
8144
  }, ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
6898
8145
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6899
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.12", type: UniPopoverComponent, isStandalone: true, selector: "uni-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span #trigger (click)=\"togglePopover($event)\" [class]=\"triggerClassName\">\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"autoClose() ? 'auto' : 'manual'\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n>\n <ng-content></ng-content>\n <span [class]=\"arrowClassName()\"></span>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
8146
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniPopoverComponent, isStandalone: true, selector: "uni-popover", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, anchor: { classPropertyName: "anchor", publicName: "anchor", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "header", isSignal: true, isRequired: false, transformFunction: null }, closable: { classPropertyName: "closable", publicName: "closable", isSignal: true, isRequired: false, transformFunction: null }, arrow: { classPropertyName: "arrow", publicName: "arrow", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, openDelay: { classPropertyName: "openDelay", publicName: "openDelay", isSignal: true, isRequired: false, transformFunction: null }, closeDelay: { classPropertyName: "closeDelay", publicName: "closeDelay", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", opened: "opened", closed: "closed" }, providers: [{ provide: COMPONENT_NAME, useValue: 'popover' }], viewQueries: [{ propertyName: "triggerRef", first: true, predicate: ["trigger"], descendants: true, isSignal: true }, { propertyName: "panelRef", first: true, predicate: ["panel"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span\n #trigger\n [class]=\"triggerClassName\"\n (click)=\"onTriggerClick($event)\"\n (mouseenter)=\"onTriggerEnter()\"\n (mouseleave)=\"onTriggerLeave()\"\n (focusin)=\"onTriggerFocusIn()\"\n (focusout)=\"onTriggerFocusOut()\"\n>\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"popoverKind()\"\n [attr.role]=\"mode() === 'tooltip' ? 'tooltip' : null\"\n [attr.aria-labelledby]=\"header() ? headerId : null\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n (mouseenter)=\"onPanelEnter()\"\n (mouseleave)=\"onPanelLeave()\"\n>\n <div [class]=\"headerRowClassName()\">\n <ng-content select=\"[popover-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (closable()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"closeFromButton()\"\n >\n Close\n </button>\n }\n </div>\n <div [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div [class]=\"footerRowClassName()\"><ng-content select=\"[popover-footer]\"></ng-content></div>\n @if (arrow()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\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 });
6900
8147
  }
6901
8148
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniPopoverComponent, decorators: [{
6902
8149
  type: Component,
6903
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-popover', imports: [], template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span #trigger (click)=\"togglePopover($event)\" [class]=\"triggerClassName\">\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"autoClose() ? 'auto' : 'manual'\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n>\n <ng-content></ng-content>\n <span [class]=\"arrowClassName()\"></span>\n</div>\n" }]
6904
- }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], autoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoClose", required: false }] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
8150
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-popover', imports: [UniIconButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'popover' }], template: "<!-- Click bubbles up from the projected [trigger] content, which is expected\n to be focusable (a button); keyboard activation arrives as a click. -->\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n<span\n #trigger\n [class]=\"triggerClassName\"\n (click)=\"onTriggerClick($event)\"\n (mouseenter)=\"onTriggerEnter()\"\n (mouseleave)=\"onTriggerLeave()\"\n (focusin)=\"onTriggerFocusIn()\"\n (focusout)=\"onTriggerFocusOut()\"\n>\n <ng-content select=\"[trigger]\"></ng-content>\n</span>\n<div\n #panel\n [id]=\"panelId\"\n [attr.popover]=\"popoverKind()\"\n [attr.role]=\"mode() === 'tooltip' ? 'tooltip' : null\"\n [attr.aria-labelledby]=\"header() ? headerId : null\"\n [class]=\"popoverClassName()\"\n (toggle)=\"onToggle($event)\"\n (mouseenter)=\"onPanelEnter()\"\n (mouseleave)=\"onPanelLeave()\"\n>\n <div [class]=\"headerRowClassName()\">\n <ng-content select=\"[popover-header]\"></ng-content>\n @if (header()) {\n <span [id]=\"headerId\" [class]=\"titleClassName()\">{{ header() }}</span>\n }\n @if (closable()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeSymbol\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"closeFromButton()\"\n >\n Close\n </button>\n }\n </div>\n <div [class]=\"bodyClassName()\"><ng-content></ng-content></div>\n <div [class]=\"footerRowClassName()\"><ng-content select=\"[popover-footer]\"></ng-content></div>\n @if (arrow()) {\n <span [class]=\"arrowClassName()\"></span>\n }\n</div>\n" }]
8151
+ }], ctorParameters: () => [], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], autoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoClose", required: false }] }], 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 }] }], anchor: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchor", required: false }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "header", required: false }] }], closable: [{ type: i0.Input, args: [{ isSignal: true, alias: "closable", required: false }] }], arrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrow", required: false }] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }], openDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "openDelay", required: false }] }], closeDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeDelay", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], triggerRef: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], panelRef: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
6905
8152
 
6906
8153
  /**
6907
8154
  * UniPopoverComponent Barrel File
@@ -7218,35 +8465,7 @@ class UniSearchInputComponent extends BaseComponent {
7218
8465
  ...this.theme.paddingLeft('sm'),
7219
8466
  },
7220
8467
  }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
7221
- listClass = computed(() => {
7222
- const options = this.componentOptions();
7223
- return css({
7224
- position: 'absolute',
7225
- top: '100%',
7226
- left: 0,
7227
- right: 0,
7228
- zIndex: 20,
7229
- margin: '4px 0 0',
7230
- padding: 4,
7231
- listStyle: 'none',
7232
- maxHeight: 280,
7233
- overflowY: 'auto',
7234
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
7235
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
7236
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
7237
- '& [role="option"]': {
7238
- padding: '8px 12px',
7239
- cursor: 'pointer',
7240
- ...this.theme.typeface('label'),
7241
- ...this.theme.color('on-primary-surface'),
7242
- ...this.theme.radius('xxs'),
7243
- '&.active, &:hover': {
7244
- ...this.theme.backgroundColor('primary-container'),
7245
- ...this.theme.color('on-primary-container'),
7246
- },
7247
- },
7248
- });
7249
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
8468
+ listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions())), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
7250
8469
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7251
8470
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSearchInputComponent, isStandalone: true, selector: "uni-search-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", search: "search", suggestionSelected: "suggestionSelected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], viewQueries: [{ propertyName: "field", first: true, predicate: UniDebounceInputComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Keydown/focusout ride the bubbling events from the inner input; the\n wrapper itself stays out of the tab order. -->\n<!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n<div (keydown)=\"onKeydown($event)\" (focusout)=\"onFocusOut($event)\">\n <uni-debounce-input\n #searchField\n [label]=\"label()\"\n [placeholder]=\"placeholder() ?? label()\"\n [debounceTime]=\"debounceTime()\"\n role=\"combobox\"\n [ariaExpanded]=\"list.open()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"list.activeDescendantId() ?? undefined\"\n (change)=\"handleChange($event)\"\n >\n <uni-symbol pre-input class=\"uni-search-lead\" name=\"{{ componentOptions().searchSymbol ?? 'search' }}\" aria-hidden=\"true\" />\n @if (hasQuery()) {\n <button\n post-input\n icon-button\n [iconName]=\"componentOptions().clearSymbol ?? 'close'\"\n variant=\"ghost\"\n size=\"sm\"\n (click)=\"clear()\"\n >\n Clear search\n </button>\n }\n </uni-debounce-input>\n\n @if (list.open()) {\n <ul [id]=\"listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion; 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]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"select(suggestion)\"\n >\n {{ suggestion }}\n </li>\n }\n </ul>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniDebounceInputComponent, selector: "uni-debounce-input", inputs: ["inputName", "inputId", "debounceTime", "label", "placeholder", "disabled", "role", "ariaExpanded", "ariaControls", "ariaActivedescendant"], outputs: ["change"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7252
8471
  }
@@ -7278,6 +8497,14 @@ class UniSelectComponent {
7278
8497
  // --- CONFIGURATION ---
7279
8498
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
7280
8499
  placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
8500
+ /**
8501
+ * Equality used to match `value` against option values, called as
8502
+ * `compareWith(optionValue, value)`. Defaults to reference equality, which
8503
+ * never matches an object value rebuilt from elsewhere (e.g. a saved record
8504
+ * against options from a fresh fetch) — pass a key comparison like
8505
+ * `(a, b) => a?.id === b?.id` for object values.
8506
+ */
8507
+ compareWith = input((a, b) => a === b, ...(ngDevMode ? [{ debugName: "compareWith" }] : /* istanbul ignore next */ []));
7281
8508
  /** Accessible name for the select; a placeholder is not a label. */
7282
8509
  ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
7283
8510
  UNSELECTED = -1;
@@ -7290,7 +8517,7 @@ class UniSelectComponent {
7290
8517
  if (currentVal === null || currentVal === undefined)
7291
8518
  return this.UNSELECTED;
7292
8519
  // Find the index of the option that contains our current value
7293
- const index = this.options().findIndex((opt) => opt.value === currentVal);
8520
+ const index = this.options().findIndex((opt) => this.compareWith()(opt.value, currentVal));
7294
8521
  return index.toString();
7295
8522
  }, ...(ngDevMode ? [{ debugName: "currentSelectedIndex" }] : /* istanbul ignore next */ []));
7296
8523
  showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
@@ -7322,12 +8549,12 @@ class UniSelectComponent {
7322
8549
  pointerEvents: 'none' /* Crucial for clicking through */,
7323
8550
  });
7324
8551
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7325
- 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 });
8552
+ 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 });
7326
8553
  }
7327
8554
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, decorators: [{
7328
8555
  type: Component,
7329
8556
  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" }]
7330
- }], 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 }] }] } });
8557
+ }], 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 }] }] } });
7331
8558
 
7332
8559
  /**
7333
8560
  * Loading placeholder painted with surface tokens. `text` renders one or more
@@ -8508,35 +9735,7 @@ class UniTagInputComponent extends BaseComponent {
8508
9735
  font: 'inherit',
8509
9736
  padding: 0,
8510
9737
  }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
8511
- listClass = computed(() => {
8512
- const options = this.componentOptions();
8513
- return css({
8514
- position: 'absolute',
8515
- top: '100%',
8516
- left: 0,
8517
- right: 0,
8518
- zIndex: 20,
8519
- margin: '4px 0 0',
8520
- padding: 4,
8521
- listStyle: 'none',
8522
- maxHeight: 280,
8523
- overflowY: 'auto',
8524
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
8525
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
8526
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
8527
- '& [role="option"]': {
8528
- padding: '8px 12px',
8529
- cursor: 'pointer',
8530
- ...this.theme.typeface('label'),
8531
- ...this.theme.color('on-primary-surface'),
8532
- ...this.theme.radius('xxs'),
8533
- '&.active, &:hover': {
8534
- ...this.theme.backgroundColor('primary-container'),
8535
- ...this.theme.color('on-primary-container'),
8536
- },
8537
- },
8538
- });
8539
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
9738
+ listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions())), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
8540
9739
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
8541
9740
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagInputComponent, isStandalone: true, selector: "uni-tag-input", 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 }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, tagVariant: { classPropertyName: "tagVariant", publicName: "tagVariant", isSignal: true, isRequired: false, transformFunction: null }, tagTone: { classPropertyName: "tagTone", publicName: "tagTone", isSignal: true, isRequired: false, transformFunction: null }, tagSize: { classPropertyName: "tagSize", publicName: "tagSize", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", added: "added", removed: "removed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chipRefs", predicate: ["chip"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div style=\"position: relative\" (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" height=\"auto\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\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]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul [id]=\"list.listboxId\" role=\"listbox\" [attr.aria-label]=\"label()\" [class]=\"listClass()\">\n @for (suggestion of visibleSuggestions(); track suggestion.value; 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]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcement() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniTagComponent, selector: "uni-tag", inputs: ["size", "tone", "label", "value", "maxWidth", "avatarSrc", "avatarName", "iconName", "symbolName", "dot", "removable", "interactive", "selected", "invalid", "disabled", "removeLabel", "controlTabIndex"], outputs: ["removed", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8542
9741
  }
@@ -9477,7 +10676,7 @@ class UniThemeSwitchComponent {
9477
10676
  (valueChange)="select($event)"
9478
10677
  [ariaLabel]="ariaLabel()"
9479
10678
  />
9480
- `, 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 });
10679
+ `, 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 });
9481
10680
  }
9482
10681
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniThemeSwitchComponent, decorators: [{
9483
10682
  type: Component,
@@ -9810,9 +11009,221 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
9810
11009
  }]
9811
11010
  }], ctorParameters: () => [], propDecorators: { hoverDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverDelay", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], inlineText: [{ type: i0.Input, args: [{ isSignal: true, alias: "inlineText", required: false }] }], appendToBody: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendToBody", required: false }] }], tipRef: [{ type: i0.ViewChild, args: ['tip', { isSignal: true }] }] } });
9812
11011
 
11012
+ /**
11013
+ * A thin sequencer over one `uni-callout`: steps spotlight their targets, a
11014
+ * footer walks Next/Back with dots-or-fraction progress, and `advanceOn`
11015
+ * gates a step behind an interaction with its target (auto-advancing for
11016
+ * clicks, unlocking Next otherwise — announced through one `role="status"`
11017
+ * region). Escape or the close button skips the tour and reports the step.
11018
+ *
11019
+ * `active` is a two-way model, so a tour is deep-linkable and inspectable;
11020
+ * steps whose declared target cannot be resolved are skipped (with a dev
11021
+ * warning) in the direction of travel rather than erroring.
11022
+ */
11023
+ class UniTourComponent extends BaseComponent {
11024
+ renderer = inject(Renderer2);
11025
+ destroyRef = inject(DestroyRef);
11026
+ steps = input.required(...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
11027
+ /** The presented step index, or null when the tour is not running. */
11028
+ active = model(null, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
11029
+ nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
11030
+ backLabel = input('Back', ...(ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []));
11031
+ skipLabel = input('Skip', ...(ngDevMode ? [{ debugName: "skipLabel" }] : /* istanbul ignore next */ []));
11032
+ doneLabel = input('Done', ...(ngDevMode ? [{ debugName: "doneLabel" }] : /* istanbul ignore next */ []));
11033
+ started = output();
11034
+ stepChanged = output();
11035
+ finished = output();
11036
+ skipped = output();
11037
+ calloutOpen = signal(false, ...(ngDevMode ? [{ debugName: "calloutOpen" }] : /* istanbul ignore next */ []));
11038
+ satisfied = signal(true, ...(ngDevMode ? [{ debugName: "satisfied" }] : /* istanbul ignore next */ []));
11039
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
11040
+ resolvedTarget = signal(undefined, ...(ngDevMode ? [{ debugName: "resolvedTarget" }] : /* istanbul ignore next */ []));
11041
+ presentedIndex = signal(null, ...(ngDevMode ? [{ debugName: "presentedIndex" }] : /* istanbul ignore next */ []));
11042
+ gateCleanup = null;
11043
+ index = computed(() => this.presentedIndex() ?? 0, ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
11044
+ currentStep = computed(() => {
11045
+ const index = this.presentedIndex();
11046
+ return index === null ? undefined : this.steps()[index];
11047
+ }, ...(ngDevMode ? [{ debugName: "currentStep" }] : /* istanbul ignore next */ []));
11048
+ isLast = computed(() => this.index() >= this.steps().length - 1, ...(ngDevMode ? [{ debugName: "isLast" }] : /* istanbul ignore next */ []));
11049
+ clickGate = computed(() => this.currentStep()?.advanceOn?.event === 'click', ...(ngDevMode ? [{ debugName: "clickGate" }] : /* istanbul ignore next */ []));
11050
+ /** Gated steps force an interactive target — the gate needs the gesture. */
11051
+ targetInteractive = computed(() => {
11052
+ const step = this.currentStep();
11053
+ return step?.advanceOn ? true : (step?.targetInteractive ?? true);
11054
+ }, ...(ngDevMode ? [{ debugName: "targetInteractive" }] : /* istanbul ignore next */ []));
11055
+ stepAriaLabel = computed(() => {
11056
+ const step = this.currentStep();
11057
+ return step ? `${step.title}, step ${this.index() + 1} of ${this.steps().length}` : '';
11058
+ }, ...(ngDevMode ? [{ debugName: "stepAriaLabel" }] : /* istanbul ignore next */ []));
11059
+ constructor() {
11060
+ super();
11061
+ // External writes to `active` (deep links) route into the sequencer.
11062
+ effect(() => {
11063
+ const index = this.active();
11064
+ if (index === untracked(this.presentedIndex))
11065
+ return;
11066
+ untracked(() => (index === null ? this.stopTour() : this.present(index, 1)));
11067
+ });
11068
+ this.destroyRef.onDestroy(() => this.gateCleanup?.());
11069
+ }
11070
+ start(at = 0) {
11071
+ this.started.emit();
11072
+ this.present(at, 1);
11073
+ }
11074
+ next() {
11075
+ this.advance();
11076
+ }
11077
+ back() {
11078
+ const index = this.presentedIndex();
11079
+ if (index !== null && index > 0)
11080
+ this.present(index - 1, -1);
11081
+ }
11082
+ skip() {
11083
+ const step = this.currentStep();
11084
+ const index = this.presentedIndex();
11085
+ this.stopTour();
11086
+ if (step && index !== null)
11087
+ this.skipped.emit({ key: step.key, index });
11088
+ }
11089
+ advance() {
11090
+ const index = this.presentedIndex();
11091
+ if (index === null)
11092
+ return;
11093
+ if (index >= this.steps().length - 1)
11094
+ this.finishTour();
11095
+ else
11096
+ this.present(index + 1, 1);
11097
+ }
11098
+ /**
11099
+ * Present step `index`, skipping (in the direction of travel) steps whose
11100
+ * declared target cannot be resolved right now.
11101
+ */
11102
+ present(index, dir) {
11103
+ this.gateCleanup?.();
11104
+ const steps = this.steps();
11105
+ if (index < 0 || index >= steps.length) {
11106
+ this.finishTour();
11107
+ return;
11108
+ }
11109
+ const step = steps[index];
11110
+ let target = null;
11111
+ if (step.target !== undefined && step.target !== '') {
11112
+ target = resolveElement(step.target);
11113
+ if (!target) {
11114
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
11115
+ console.warn(`[uni-tour] step "${step.key}" target missing — skipped`);
11116
+ }
11117
+ this.present(index + dir, dir);
11118
+ return;
11119
+ }
11120
+ }
11121
+ this.presentedIndex.set(index);
11122
+ this.active.set(index);
11123
+ this.resolvedTarget.set(target ?? undefined);
11124
+ this.setupGate(step, target);
11125
+ this.calloutOpen.set(true);
11126
+ this.stepChanged.emit({ key: step.key, index });
11127
+ }
11128
+ setupGate(step, target) {
11129
+ const gate = step.advanceOn;
11130
+ this.satisfied.set(!gate);
11131
+ if (!gate || !target)
11132
+ return;
11133
+ const auto = gate.auto ?? gate.event === 'click';
11134
+ const unlisten = this.renderer.listen(target, gate.event, () => {
11135
+ if (untracked(this.satisfied) && !auto)
11136
+ return;
11137
+ this.satisfied.set(true);
11138
+ if (auto) {
11139
+ this.gateCleanup?.();
11140
+ this.advance();
11141
+ }
11142
+ else {
11143
+ this.announcement.set('Next available');
11144
+ }
11145
+ });
11146
+ this.gateCleanup = () => {
11147
+ unlisten();
11148
+ this.gateCleanup = null;
11149
+ };
11150
+ }
11151
+ onDismissed(dismissal) {
11152
+ if (dismissal.reason === 'programmatic')
11153
+ return; // that's us, stepping or stopping
11154
+ const step = this.currentStep();
11155
+ const index = this.presentedIndex();
11156
+ this.stopTour();
11157
+ if (step && index !== null)
11158
+ this.skipped.emit({ key: step.key, index });
11159
+ }
11160
+ /** Arrow keys navigate only while focus is inside the panel. */
11161
+ onPanelKeydown(event) {
11162
+ if (event.key === 'ArrowRight' && this.satisfied() && !this.clickGate()) {
11163
+ event.preventDefault();
11164
+ this.advance();
11165
+ }
11166
+ else if (event.key === 'ArrowLeft' && this.index() > 0) {
11167
+ event.preventDefault();
11168
+ this.back();
11169
+ }
11170
+ }
11171
+ finishTour() {
11172
+ this.gateCleanup?.();
11173
+ this.calloutOpen.set(false);
11174
+ this.presentedIndex.set(null);
11175
+ this.active.set(null);
11176
+ this.finished.emit();
11177
+ }
11178
+ stopTour() {
11179
+ this.gateCleanup?.();
11180
+ this.calloutOpen.set(false);
11181
+ this.presentedIndex.set(null);
11182
+ this.active.set(null);
11183
+ }
11184
+ // --- styling --------------------------------------------------------------
11185
+ statusClassName = css(visuallyHidden);
11186
+ footerClassName = computed(() => css({
11187
+ display: 'flex',
11188
+ alignItems: 'center',
11189
+ flex: 1,
11190
+ gap: this.theme.spacing()[this.componentOptions().footerGap],
11191
+ }), ...(ngDevMode ? [{ debugName: "footerClassName" }] : /* istanbul ignore next */ []));
11192
+ dotsClassName = computed(() => {
11193
+ const colors = this.theme.colorPalette();
11194
+ const on = colors[this.variant()] ?? colors['primary'];
11195
+ return css({
11196
+ display: 'inline-flex',
11197
+ gap: 5,
11198
+ margin: '0 auto',
11199
+ '& i': {
11200
+ width: 6,
11201
+ height: 6,
11202
+ borderRadius: 999,
11203
+ background: colors['surface-variant'],
11204
+ border: `1px solid ${colors['outline'] ?? colors['surface-variant']}`,
11205
+ },
11206
+ '& i.on': { background: on, borderColor: on },
11207
+ });
11208
+ }, ...(ngDevMode ? [{ debugName: "dotsClassName" }] : /* istanbul ignore next */ []));
11209
+ fractionClassName = css({ margin: '0 auto' });
11210
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTourComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11211
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTourComponent, isStandalone: true, selector: "uni-tour", inputs: { steps: { classPropertyName: "steps", publicName: "steps", isSignal: true, isRequired: true, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, skipLabel: { classPropertyName: "skipLabel", publicName: "skipLabel", isSignal: true, isRequired: false, transformFunction: null }, doneLabel: { classPropertyName: "doneLabel", publicName: "doneLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { active: "activeChange", started: "started", stepChanged: "stepChanged", finished: "finished", skipped: "skipped" }, providers: [{ provide: COMPONENT_NAME, useValue: 'tour' }], usesInheritance: true, ngImport: i0, template: "<uni-callout\n [(open)]=\"calloutOpen\"\n [key]=\"currentStep()?.key ?? ''\"\n [target]=\"resolvedTarget()\"\n [placement]=\"currentStep()?.placement ?? 'bottom'\"\n [backdrop]=\"currentStep()?.backdrop\"\n [targetInteractive]=\"targetInteractive()\"\n [header]=\"currentStep()?.title ?? ''\"\n [ariaLabel]=\"stepAriaLabel()\"\n [closeLabel]=\"skipLabel()\"\n (dismissed)=\"onDismissed($event)\"\n (panelKeydown)=\"onPanelKeydown($event)\"\n>\n <p>{{ currentStep()?.body }}</p>\n <div callout-actions [class]=\"footerClassName()\">\n @if (index() > 0) {\n <button text-button variant=\"ghost\" (click)=\"back()\">{{ backLabel() }}</button>\n }\n @if (componentOptions().progressStyle === 'fraction') {\n <span aria-hidden=\"true\" [class]=\"fractionClassName\">{{ index() + 1 }} of {{ steps().length }}</span>\n } @else {\n <span aria-hidden=\"true\" [class]=\"dotsClassName()\">\n @for (step of steps(); track $index) {\n <i [class.on]=\"$index === index()\"></i>\n }\n </span>\n }\n <!-- A click-gate advances by using the target itself \u2014 a Next button\n would be a lie, so none renders. -->\n @if (!clickGate()) {\n <button text-button [disable]=\"!satisfied()\" (click)=\"advance()\">\n {{ isLast() ? doneLabel() : nextLabel() }}\n </button>\n }\n </div>\n</uni-callout>\n<!-- One live region per tour: gate unlocks are announced here. -->\n<span role=\"status\" [class]=\"statusClassName\">{{ announcement() }}</span>\n", dependencies: [{ kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "component", type: UniCalloutComponent, selector: "uni-callout", inputs: ["open", "key", "target", "placement", "backdrop", "targetInteractive", "dismissible", "dismissOnBackdrop", "header", "arrow", "ariaLabel", "closeLabel"], outputs: ["openChange", "opened", "closed", "dismissed", "panelKeydown"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11212
+ }
11213
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTourComponent, decorators: [{
11214
+ type: Component,
11215
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tour', imports: [UniButtonComponent, UniCalloutComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tour' }], template: "<uni-callout\n [(open)]=\"calloutOpen\"\n [key]=\"currentStep()?.key ?? ''\"\n [target]=\"resolvedTarget()\"\n [placement]=\"currentStep()?.placement ?? 'bottom'\"\n [backdrop]=\"currentStep()?.backdrop\"\n [targetInteractive]=\"targetInteractive()\"\n [header]=\"currentStep()?.title ?? ''\"\n [ariaLabel]=\"stepAriaLabel()\"\n [closeLabel]=\"skipLabel()\"\n (dismissed)=\"onDismissed($event)\"\n (panelKeydown)=\"onPanelKeydown($event)\"\n>\n <p>{{ currentStep()?.body }}</p>\n <div callout-actions [class]=\"footerClassName()\">\n @if (index() > 0) {\n <button text-button variant=\"ghost\" (click)=\"back()\">{{ backLabel() }}</button>\n }\n @if (componentOptions().progressStyle === 'fraction') {\n <span aria-hidden=\"true\" [class]=\"fractionClassName\">{{ index() + 1 }} of {{ steps().length }}</span>\n } @else {\n <span aria-hidden=\"true\" [class]=\"dotsClassName()\">\n @for (step of steps(); track $index) {\n <i [class.on]=\"$index === index()\"></i>\n }\n </span>\n }\n <!-- A click-gate advances by using the target itself \u2014 a Next button\n would be a lie, so none renders. -->\n @if (!clickGate()) {\n <button text-button [disable]=\"!satisfied()\" (click)=\"advance()\">\n {{ isLast() ? doneLabel() : nextLabel() }}\n </button>\n }\n </div>\n</uni-callout>\n<!-- One live region per tour: gate unlocks are announced here. -->\n<span role=\"status\" [class]=\"statusClassName\">{{ announcement() }}</span>\n" }]
11216
+ }], ctorParameters: () => [], propDecorators: { steps: [{ type: i0.Input, args: [{ isSignal: true, alias: "steps", required: true }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }, { type: i0.Output, args: ["activeChange"] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], skipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "skipLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], started: [{ type: i0.Output, args: ["started"] }], stepChanged: [{ type: i0.Output, args: ["stepChanged"] }], finished: [{ type: i0.Output, args: ["finished"] }], skipped: [{ type: i0.Output, args: ["skipped"] }] } });
11217
+
11218
+ /**
11219
+ * UniTourComponent Barrel File
11220
+ *
11221
+ * This file exports all public-facing elements of the tour component.
11222
+ */
11223
+
9813
11224
  /**
9814
11225
  * Generated bundle index. Do not edit.
9815
11226
  */
9816
11227
 
9817
- export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, createListboxNavigation, dayOfWeek, daysInMonth, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isValidDate, isoDate, joinDateTime, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveFocusTarget, splitDateTime, timeSlots, todayIso, uniqueId, useTimer, visuallyHidden, weekdayNames };
11228
+ export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, TRANSFORM_ORIGINS, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCalloutComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniComboboxComponent, 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, UniTourComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, clearAnchorName, createListboxNavigation, dayOfWeek, daysInMonth, discreteOverlayTransition, focusableElements, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isToggleOpen, isValidDate, isoDate, joinDateTime, listboxPopupStyles, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveElement, resolveFocusTarget, restoreOverlayFocus, setAnchorName, splitDateTime, spotlightStyles, timeSlots, todayIso, uniqueId, useTimer, visuallyHidden, weekdayNames };
9818
11229
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map