@uni-design-system/uni-angular 8.2.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 */ []));
@@ -3079,6 +3267,389 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3079
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" }]
3080
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"] }] } });
3081
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
+
3082
3653
  class UniCardContentComponent extends BaseComponent {
3083
3654
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCardContentComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3084
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 });
@@ -3342,6 +3913,525 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3342
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" }]
3343
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"] }] } });
3344
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 }] }] } });
4434
+
3345
4435
  class UniDataSearchComponent extends BaseComponent {
3346
4436
  datasource = input(...(ngDevMode ? [undefined, { debugName: "datasource" }] : /* istanbul ignore next */ []));
3347
4437
  placeholder = input('Search', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
@@ -4018,69 +5108,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4018
5108
  args: ['dropdown', { static: true }]
4019
5109
  }] } });
4020
5110
 
4021
- class UniInputBoxComponent extends BaseComponent {
4022
- className = css({ display: 'contents' });
4023
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4024
- error = input(false, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
4025
- minWidth = input('0', ...(ngDevMode ? [{ debugName: "minWidth" }] : /* istanbul ignore next */ []));
4026
- /** Override the themed field height, e.g. `'auto'` for multi-line fields. */
4027
- height = input(undefined, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
4028
- color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
4029
- border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
4030
- shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
4031
- inputBoxClass = computed(() => css([
4032
- this.disabled() && {
4033
- ...this.theme.color(this.componentOptions().disabledTextColor),
4034
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4035
- cursor: 'not-allowed !important',
4036
- },
4037
- {
4038
- '& input, select, textarea': {
4039
- ...removeInputPlatformStyling,
4040
- height: '100%',
4041
- ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
4042
- ...this.theme.color(this.componentOptions().textColor),
4043
- // `typeFace` is the deprecated casing; themes that still set it win
4044
- // only when the canonical key is absent.
4045
- ...this.theme.typeface(this.componentOptions().typeface ?? this.componentOptions().typeFace),
4046
- },
4047
- // Multi-line fields size themselves (rows/resize), not from the box.
4048
- '& textarea': {
4049
- height: 'auto',
4050
- ...this.theme.paddingTop('xs'),
4051
- ...this.theme.paddingBottom('xs'),
4052
- },
4053
- '&:has(input:disabled, select:disabled, textarea:disabled)': {
4054
- ...this.theme.color(this.componentOptions().disabledTextColor),
4055
- ...this.theme.backgroundColor(this.componentOptions().disabledColor),
4056
- },
4057
- '& input:disabled, select:disabled, textarea:disabled': {
4058
- cursor: 'not-allowed !important',
4059
- },
4060
- '&:has(input:focus, select:focus, textarea:focus)': {
4061
- outline: this.componentOptions().focusOutline,
4062
- outlineOffset: this.componentOptions().focusOutlineOffset,
4063
- // Optional focus chrome (border/ring/background). It yields to the
4064
- // error state, so a flagged field stays visibly flagged while the
4065
- // user is in it correcting the value.
4066
- ...(this.error()
4067
- ? {}
4068
- : {
4069
- ...this.theme.border(this.componentOptions().focusBorder),
4070
- ...this.theme.boxShadow(this.componentOptions().focusShadow),
4071
- ...this.theme.backgroundColor(this.componentOptions().focusColor),
4072
- }),
4073
- },
4074
- },
4075
- ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
4076
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4077
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4078
- }
4079
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
4080
- type: Component,
4081
- args: [{ selector: 'uni-input-box', imports: [UniRowComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [color]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
4082
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
4083
-
4084
5111
  /**
4085
5112
  * Date field with free-typed parsing and a popup calendar. Type `aug 20`,
4086
5113
  * `8/20/2026` or `2026-08-20`, or pick from the grid — the form gets the
@@ -4671,32 +5698,9 @@ class UniTimeInputComponent extends BaseComponent {
4671
5698
  embeddedClass = computed(() => css({ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }), ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4672
5699
  listClass = computed(() => {
4673
5700
  const options = this.componentOptions();
4674
- return css({
4675
- position: 'absolute',
4676
- top: '100%',
4677
- left: 0,
4678
- right: 0,
4679
- zIndex: 20,
4680
- margin: '4px 0 0',
4681
- padding: 4,
4682
- listStyle: 'none',
5701
+ return css(listboxPopupStyles(this.theme, options, {
4683
5702
  maxHeight: (options.maxVisibleOptions ?? 7) * 36,
4684
- overflowY: 'auto',
4685
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
4686
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
4687
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
4688
- '& [role="option"]': {
4689
- padding: '8px 12px',
4690
- cursor: 'pointer',
4691
- ...this.theme.typeface('label'),
4692
- ...this.theme.color('on-primary-surface'),
4693
- ...this.theme.radius('xxs'),
4694
- '&.active, &:hover': {
4695
- ...this.theme.backgroundColor('primary-container'),
4696
- ...this.theme.color('on-primary-container'),
4697
- },
4698
- },
4699
- });
5703
+ }));
4700
5704
  }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4701
5705
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4702
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 });
@@ -6829,34 +7833,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
6829
7833
  * This file exports all public-facing elements of the paginator component.
6830
7834
  */
6831
7835
 
6832
- 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 {
6833
7854
  renderer = inject(Renderer2);
6834
- theme = inject(ThemeService);
7855
+ destroyRef = inject(DestroyRef);
6835
7856
  placement = input('bottom', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
6836
7857
  /** Light-dismiss on outside click / Escape (native popover="auto"). */
6837
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();
6838
7882
  panelId = uniqueId('uni-popover');
7883
+ headerId = uniqueId('uni-popover-header');
6839
7884
  anchorName = newAnchorName();
6840
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 */ []));
6841
7888
  triggerRef = viewChild.required('trigger');
6842
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;
6843
7895
  constructor() {
6844
- // Wire the ARIA disclosure contract onto the focusable trigger element
7896
+ super();
6845
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;
6846
7906
  const target = resolveFocusTarget(this.triggerRef().nativeElement);
6847
- this.renderer.setAttribute(target, 'aria-expanded', 'false');
6848
- 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
+ }
6849
7938
  });
6850
7939
  }
6851
- onToggle(event) {
6852
- const open = event.newState === 'open';
6853
- this.showing.set(open);
6854
- this.renderer.setAttribute(resolveFocusTarget(this.triggerRef().nativeElement), 'aria-expanded', `${open}`);
6855
- }
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 */ []));
6856
7942
  showPopover() {
7943
+ if (this.showing())
7944
+ return;
7945
+ this.applyAnchor();
6857
7946
  this.panelRef().nativeElement.showPopover();
6858
7947
  }
6859
7948
  hidePopover() {
7949
+ if (!this.showing())
7950
+ return;
6860
7951
  this.panelRef().nativeElement.hidePopover();
6861
7952
  }
6862
7953
  togglePopover(event) {
@@ -6866,60 +7957,198 @@ class UniPopoverComponent {
6866
7957
  else {
6867
7958
  this.showPopover();
6868
7959
  }
6869
- event.stopPropagation();
7960
+ event?.stopPropagation();
6870
7961
  }
6871
- triggerClassName = css({
6872
- display: 'inline-block',
6873
- anchorName: this.anchorName,
6874
- });
6875
- popoverClassName = computed(() => css([
6876
- {
6877
- ...this.theme.colorPair('primary-surface'),
6878
- ...this.theme.radius('xs'),
6879
- ...this.theme.boxShadow('raised'),
6880
- ...this.theme.typeface('label'),
6881
- ...this.theme.border('quaternary'),
6882
- 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,
6883
8090
  width: 'max-content',
8091
+ maxWidth: this.resolvedMaxWidth(),
6884
8092
  overflow: 'visible',
6885
- ...anchorStyles(this.anchorName, this.placement(), { mainAxis: 7 }),
6886
- // Fade via discrete transitions across the top layer
6887
- transitionProperty: 'opacity, display, overlay',
6888
- transitionDuration: '250ms',
6889
- transitionBehavior: 'allow-discrete',
6890
- opacity: 0,
6891
- '&:popover-open': {
6892
- opacity: 1,
6893
- },
6894
- '@starting-style': {
6895
- '&:popover-open': {
6896
- opacity: 0,
6897
- },
6898
- },
6899
- },
6900
- ]), ...(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 */ []));
6901
8129
  arrowClassName = computed(() => {
8130
+ const options = this.componentOptions();
6902
8131
  const side = this.placement().split('-')[0];
6903
- // 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
6904
8133
  const borders = {
6905
- top: { ...this.theme.borderRight('quaternary'), ...this.theme.borderBottom('quaternary') },
6906
- bottom: { ...this.theme.borderLeft('quaternary'), ...this.theme.borderTop('quaternary') },
6907
- left: { ...this.theme.borderRight('quaternary'), ...this.theme.borderTop('quaternary') },
6908
- 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) },
6909
8138
  };
6910
8139
  return css({
6911
- ...this.theme.colorPair('primary-surface'),
6912
- ...anchorArrowStyles(this.placement()),
8140
+ ...this.theme.colorPair(options.color),
8141
+ ...anchorArrowStyles(this.placement(), options.arrowSize),
6913
8142
  ...borders[side],
6914
8143
  });
6915
8144
  }, ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
6916
8145
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniPopoverComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6917
- 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 });
6918
8147
  }
6919
8148
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniPopoverComponent, decorators: [{
6920
8149
  type: Component,
6921
- 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" }]
6922
- }], 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 }] }] } });
6923
8152
 
6924
8153
  /**
6925
8154
  * UniPopoverComponent Barrel File
@@ -7236,35 +8465,7 @@ class UniSearchInputComponent extends BaseComponent {
7236
8465
  ...this.theme.paddingLeft('sm'),
7237
8466
  },
7238
8467
  }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
7239
- listClass = computed(() => {
7240
- const options = this.componentOptions();
7241
- return css({
7242
- position: 'absolute',
7243
- top: '100%',
7244
- left: 0,
7245
- right: 0,
7246
- zIndex: 20,
7247
- margin: '4px 0 0',
7248
- padding: 4,
7249
- listStyle: 'none',
7250
- maxHeight: 280,
7251
- overflowY: 'auto',
7252
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
7253
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
7254
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
7255
- '& [role="option"]': {
7256
- padding: '8px 12px',
7257
- cursor: 'pointer',
7258
- ...this.theme.typeface('label'),
7259
- ...this.theme.color('on-primary-surface'),
7260
- ...this.theme.radius('xxs'),
7261
- '&.active, &:hover': {
7262
- ...this.theme.backgroundColor('primary-container'),
7263
- ...this.theme.color('on-primary-container'),
7264
- },
7265
- },
7266
- });
7267
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
8468
+ listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions())), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
7268
8469
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7269
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 });
7270
8471
  }
@@ -8534,35 +9735,7 @@ class UniTagInputComponent extends BaseComponent {
8534
9735
  font: 'inherit',
8535
9736
  padding: 0,
8536
9737
  }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
8537
- listClass = computed(() => {
8538
- const options = this.componentOptions();
8539
- return css({
8540
- position: 'absolute',
8541
- top: '100%',
8542
- left: 0,
8543
- right: 0,
8544
- zIndex: 20,
8545
- margin: '4px 0 0',
8546
- padding: 4,
8547
- listStyle: 'none',
8548
- maxHeight: 280,
8549
- overflowY: 'auto',
8550
- ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
8551
- ...this.theme.boxShadow(options.listShadow ?? 'menu'),
8552
- ...this.theme.radius(options.listBorderRadius ?? 'xs'),
8553
- '& [role="option"]': {
8554
- padding: '8px 12px',
8555
- cursor: 'pointer',
8556
- ...this.theme.typeface('label'),
8557
- ...this.theme.color('on-primary-surface'),
8558
- ...this.theme.radius('xxs'),
8559
- '&.active, &:hover': {
8560
- ...this.theme.backgroundColor('primary-container'),
8561
- ...this.theme.color('on-primary-container'),
8562
- },
8563
- },
8564
- });
8565
- }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
9738
+ listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions())), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
8566
9739
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
8567
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 });
8568
9741
  }
@@ -9836,9 +11009,221 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
9836
11009
  }]
9837
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 }] }] } });
9838
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
+
9839
11224
  /**
9840
11225
  * Generated bundle index. Do not edit.
9841
11226
  */
9842
11227
 
9843
- export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, createListboxNavigation, dayOfWeek, daysInMonth, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isValidDate, isoDate, joinDateTime, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, parseDateText, parseTimeText, resolveFocusTarget, splitDateTime, timeSlots, todayIso, uniqueId, useTimer, visuallyHidden, weekdayNames };
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 };
9844
11229
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map