@uni-design-system/uni-angular 7.2.0 → 8.0.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, booleanAttribute, viewChildren } 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';
3
3
  import { injectGlobal, css, keyframes } from '@emotion/css';
4
- import { UniThemes, LightTheme, toTypefaces, createThemeFromPalette, Z_INDEX, fadeIn, fadeOut, expandFadeIn, collapseFadeOut, removeInputPlatformStyling, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
4
+ import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, fadeIn, fadeOut, EXPAND_DEFAULT_SPEED, expandDuration, expandFadeIn, collapseFadeOut, removeInputPlatformStyling, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
5
5
  import { NgClass, NgTemplateOutlet, CommonModule } from '@angular/common';
6
6
 
7
7
  let nextUniqueId = 0;
@@ -50,6 +50,112 @@ function motionSafe(styles) {
50
50
  return { '@media (prefers-reduced-motion: no-preference)': styles };
51
51
  }
52
52
 
53
+ /**
54
+ * The keyboard and ARIA bookkeeping shared by every combobox-style popup:
55
+ * open state, the active option index, and the `aria-activedescendant` id
56
+ * wiring. Deliberately owns no markup and no filtering — each control renders
57
+ * its own options and decides what a suggestion *is*.
58
+ *
59
+ * Extracted because three controls need the identical contract
60
+ * (`uni-search-input`, `uni-tag-input`, and the multi-select upgrade), and the
61
+ * parts that silently drift between hand-rolled copies all live here: the
62
+ * wrap-around arithmetic, Home/End, and keeping the active id in sync with the
63
+ * option list.
64
+ *
65
+ * `Enter` and `Escape` are deliberately *not* handled: what they mean depends
66
+ * on the control (submit a search, commit a typed token, clear the field), and
67
+ * `activeIndex()` is all a caller needs to decide.
68
+ */
69
+ class ListboxNavigation {
70
+ config;
71
+ /** Id for the `role="listbox"` element; wire as `aria-controls`. */
72
+ listboxId;
73
+ _open = signal(false, ...(ngDevMode ? [{ debugName: "_open" }] : /* istanbul ignore next */ []));
74
+ _activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "_activeIndex" }] : /* istanbul ignore next */ []));
75
+ wrap;
76
+ /** Whether the popup is showing. Also false when there is nothing to show. */
77
+ open = computed(() => this._open() && this.config.count() > 0, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
78
+ /** Index of the highlighted option, or -1 when none is active. */
79
+ activeIndex = computed(() => {
80
+ const index = this._activeIndex();
81
+ // A shrinking option list must never leave the active id dangling.
82
+ return index < this.config.count() ? index : -1;
83
+ }, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
84
+ /** Value for `aria-activedescendant`, or null when nothing is active. */
85
+ activeDescendantId = computed(() => {
86
+ const index = this.activeIndex();
87
+ return this.open() && index >= 0 ? this.optionId(index) : null;
88
+ }, ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
89
+ constructor(config) {
90
+ this.config = config;
91
+ this.listboxId = uniqueId(config.idPrefix ?? 'uni-listbox');
92
+ this.wrap = config.wrap ?? true;
93
+ }
94
+ /** Stable per-option id, for `role="option"` elements. */
95
+ optionId(index) {
96
+ return `${this.listboxId}-option-${index}`;
97
+ }
98
+ show() {
99
+ this._open.set(true);
100
+ }
101
+ /** Close and drop the active option, so reopening starts clean. */
102
+ hide() {
103
+ this._open.set(false);
104
+ this._activeIndex.set(-1);
105
+ }
106
+ setActive(index) {
107
+ this._activeIndex.set(index);
108
+ }
109
+ /**
110
+ * Handle ArrowDown / ArrowUp / Home / End, opening the popup if needed.
111
+ * Returns true when the key was consumed, so a caller can fall through to
112
+ * its own handling for everything else.
113
+ */
114
+ navigate(event) {
115
+ const count = this.config.count();
116
+ if (count === 0)
117
+ return false;
118
+ const target = this.nextIndex(event.key, count);
119
+ if (target === null)
120
+ return false;
121
+ event.preventDefault();
122
+ this._open.set(true);
123
+ this._activeIndex.set(target);
124
+ return true;
125
+ }
126
+ nextIndex(key, count) {
127
+ const current = this.activeIndex();
128
+ switch (key) {
129
+ case 'ArrowDown':
130
+ return this.step(current, 1, count);
131
+ case 'ArrowUp':
132
+ // Opening with ArrowUp lands on the last option, matching menus.
133
+ return current < 0 ? count - 1 : this.step(current, -1, count);
134
+ case 'Home':
135
+ return 0;
136
+ case 'End':
137
+ return count - 1;
138
+ default:
139
+ return null;
140
+ }
141
+ }
142
+ step(current, delta, count) {
143
+ const next = current + delta;
144
+ if (this.wrap)
145
+ return (next + count) % count;
146
+ return Math.min(Math.max(next, 0), count - 1);
147
+ }
148
+ /** Close when focus leaves the control entirely (not on internal moves). */
149
+ closeOnFocusOut(event) {
150
+ const next = event.relatedTarget;
151
+ const container = event.currentTarget;
152
+ if (!next || !container?.contains(next))
153
+ this.hide();
154
+ }
155
+ }
156
+ /** Factory mirroring the signal-API style used elsewhere in the CDK. */
157
+ const createListboxNavigation = (config) => new ListboxNavigation(config);
158
+
53
159
  /** Returns a document-unique dashed-ident usable as a CSS `anchor-name`. */
54
160
  function newAnchorName() {
55
161
  return `--${uniqueId('uni-anchor')}`;
@@ -648,10 +754,19 @@ class ThemeService {
648
754
  static CUSTOM_KEY = 'CustomTheme';
649
755
  /** localStorage key holding the serialized brand palette config. */
650
756
  static PALETTE_KEY = 'uni-custom-palette';
651
- themes = inject(UNI_THEMES);
652
757
  localStorage = inject(LocalStorageService);
653
- theme = signal(LightTheme, ...(ngDevMode ? [{ debugName: "theme" }] : /* istanbul ignore next */ []));
654
- themeOptions = signal([], ...(ngDevMode ? [{ debugName: "themeOptions" }] : /* istanbul ignore next */ []));
758
+ /**
759
+ * The live theme registry: the injected {@link UNI_THEMES} map (validated
760
+ * at construction) plus anything added via {@link registerTheme} — including
761
+ * the custom brand theme, which registers under {@link CUSTOM_KEY} so it is
762
+ * an ordinary, selectable entry.
763
+ */
764
+ registry = signal({}, ...(ngDevMode ? [{ debugName: "registry" }] : /* istanbul ignore next */ []));
765
+ _theme = signal(LightTheme, ...(ngDevMode ? [{ debugName: "_theme" }] : /* istanbul ignore next */ []));
766
+ /** The active theme. Write via {@link setTheme}/{@link selectTheme} — every write is validated. */
767
+ theme = this._theme.asReadonly();
768
+ /** Selectable themes, derived from the registry — reactive to registration. */
769
+ themeOptions = computed(() => Object.entries(this.registry()).map(([value, theme]) => ({ label: theme.name, value })), ...(ngDevMode ? [{ debugName: "themeOptions" }] : /* istanbul ignore next */ []));
655
770
  components = computed(() => this.theme().components, ...(ngDevMode ? [{ debugName: "components" }] : /* istanbul ignore next */ []));
656
771
  component = (componentName) => computed(() => this.components()[componentName] || {});
657
772
  colors = computed(() => this.theme().colors, ...(ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []));
@@ -677,9 +792,21 @@ class ThemeService {
677
792
  }
678
793
  }
679
794
  `;
680
- this.themeOptions.set(Object.keys(this.themes).map((key) => {
681
- return { label: this.themes[key].name, value: key };
682
- }));
795
+ // Seed the registry from the injected map, excluding (with reasons) any
796
+ // theme that fails the structural contract — a malformed theme would
797
+ // otherwise render as silent `undefined` CSS.
798
+ const injected = inject(UNI_THEMES);
799
+ const registry = {};
800
+ for (const [key, candidate] of Object.entries(injected)) {
801
+ const result = parseTheme(candidate);
802
+ if (result.success) {
803
+ registry[key] = result.theme;
804
+ }
805
+ else {
806
+ console.warn(`Uni theme '${key}' rejected: ${formatThemeIssues(result.issues)}`);
807
+ }
808
+ }
809
+ this.registry.set(registry);
683
810
  // Rehydrate a custom brand theme if one is active — this is what lets a
684
811
  // palette built in one story reskin every other story: each story spins up
685
812
  // a fresh ThemeService, and it reads the persisted config here on init.
@@ -689,14 +816,75 @@ class ThemeService {
689
816
  this.applyPalette(savedPalette, false);
690
817
  }
691
818
  else {
692
- this.selectTheme(savedTheme || Object.keys(this.themes)[0] || 'base');
819
+ this.selectTheme(savedTheme || Object.keys(registry)[0] || 'base');
693
820
  }
694
821
  }
822
+ /**
823
+ * Activate a registered theme. Returns false — touching nothing, persisting
824
+ * nothing — when the name is unknown (previously this silently kept the old
825
+ * theme while still recording the bad key).
826
+ */
695
827
  selectTheme(themeName) {
828
+ const theme = this.registry()[themeName];
829
+ if (!theme)
830
+ return false;
831
+ // Registered themes were validated at registration; set directly.
832
+ this._theme.set(theme);
696
833
  this.selectedThemeKey.set(themeName);
697
- if (this.themes[themeName])
698
- this.theme.set(this.themes[themeName]);
699
834
  this.localStorage.setItem('theme', themeName);
835
+ return true;
836
+ }
837
+ /**
838
+ * Validate and activate a theme without registering it. The result carries
839
+ * acceptance or the complete list of rejection reasons; on rejection the
840
+ * active theme is unchanged.
841
+ */
842
+ setTheme(input) {
843
+ const result = this.accept(input);
844
+ if (result.success) {
845
+ this._theme.set(result.theme);
846
+ this.selectedThemeKey.set(result.theme.id);
847
+ }
848
+ return result;
849
+ }
850
+ /**
851
+ * Validate and add a theme to the registry (keyed by its `id`), making it
852
+ * selectable and listed in {@link themeOptions}. On rejection the registry
853
+ * is unchanged and the result lists every reason.
854
+ */
855
+ registerTheme(input, opts) {
856
+ const result = this.accept(input);
857
+ if (result.success) {
858
+ const theme = result.theme;
859
+ this.registry.update((themes) => ({ ...themes, [theme.id]: theme }));
860
+ if (opts?.select)
861
+ this.selectTheme(theme.id);
862
+ }
863
+ return result;
864
+ }
865
+ /**
866
+ * The gate every externally-supplied theme passes: validate, then restore
867
+ * any built-in icons the payload left out. Themes that travel as JSON (the
868
+ * MCP theme tools, a theme registry) omit the built-in icon set — it is
869
+ * ~71% of the bytes and every consumer already ships it — so hydration
870
+ * applies the same `{...BaseIcons, ...icons}` contract `createTheme` uses:
871
+ * the theme's own icons win, built-ins fill the rest.
872
+ */
873
+ accept(input) {
874
+ const result = parseTheme(input);
875
+ return result.success ? { ...result, theme: hydrateTheme(result.theme) } : result;
876
+ }
877
+ /** Remove a registered theme; falls back to the first remaining theme if it was active. */
878
+ unregisterTheme(id) {
879
+ if (!(id in this.registry()))
880
+ return;
881
+ this.registry.update((themes) => {
882
+ const { [id]: _removed, ...rest } = themes;
883
+ return rest;
884
+ });
885
+ if (this.selectedThemeKey() === id) {
886
+ this.selectTheme(Object.keys(this.registry())[0] || 'base');
887
+ }
700
888
  }
701
889
  /** The live brand palette config, when a custom theme is active. */
702
890
  customPalette = signal(null, ...(ngDevMode ? [{ debugName: "customPalette" }] : /* istanbul ignore next */ []));
@@ -708,18 +896,24 @@ class ThemeService {
708
896
  */
709
897
  applyPalette(config, persist = true) {
710
898
  this.customPalette.set(config);
711
- this.theme.set(createThemeFromPalette({ ...config, id: ThemeService.CUSTOM_KEY, name: 'Your Brand' }));
899
+ // Registering (rather than only setting) makes 'Your Brand' an ordinary
900
+ // registry entry: it appears in themeOptions/uni-theme-switch and can be
901
+ // switched away from and back to.
902
+ const result = this.registerTheme(createThemeFromPalette({ ...config, id: ThemeService.CUSTOM_KEY, name: 'Your Brand' }));
903
+ if (!result.success)
904
+ return;
905
+ this._theme.set(result.theme);
712
906
  this.selectedThemeKey.set(ThemeService.CUSTOM_KEY);
713
907
  if (persist) {
714
908
  this.localStorage.setItem(ThemeService.PALETTE_KEY, config);
715
909
  this.localStorage.setItem('theme', ThemeService.CUSTOM_KEY);
716
910
  }
717
911
  }
718
- /** Drop the custom brand theme and fall back to the first built-in theme. */
912
+ /** Drop the custom brand theme and fall back to the first registered theme. */
719
913
  clearCustomPalette() {
720
914
  this.customPalette.set(null);
721
915
  this.localStorage.removeItem(ThemeService.PALETTE_KEY);
722
- this.selectTheme(Object.keys(this.themes)[0] || 'base');
916
+ this.unregisterTheme(ThemeService.CUSTOM_KEY);
723
917
  }
724
918
  selectedThemeName = computed(() => this.theme().name, ...(ngDevMode ? [{ debugName: "selectedThemeName" }] : /* istanbul ignore next */ []));
725
919
  selectedThemeKey = signal('', ...(ngDevMode ? [{ debugName: "selectedThemeKey" }] : /* istanbul ignore next */ []));
@@ -3308,6 +3502,13 @@ class UniDropdownComponent extends BaseComponent {
3308
3502
  popoverId = uniqueId('uni-dropdown');
3309
3503
  paddingVertical = input(...(ngDevMode ? [undefined, { debugName: "paddingVertical" }] : /* istanbul ignore next */ []));
3310
3504
  paddingHorizontal = input(...(ngDevMode ? [undefined, { debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
3505
+ // Per-instance panel-chrome overrides; undefined falls back to the theme's
3506
+ // `dropdown` options, so hosts like uni-menu can restyle their panel
3507
+ // without forking the shared dropdown entry.
3508
+ border = input(...(ngDevMode ? [undefined, { debugName: "border" }] : /* istanbul ignore next */ []));
3509
+ borderRadius = input(...(ngDevMode ? [undefined, { debugName: "borderRadius" }] : /* istanbul ignore next */ []));
3510
+ shadow = input(...(ngDevMode ? [undefined, { debugName: "shadow" }] : /* istanbul ignore next */ []));
3511
+ color = input(...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
3311
3512
  dropdownShowing = output();
3312
3513
  dropdownHiding = output();
3313
3514
  dropdownRef;
@@ -3432,17 +3633,17 @@ class UniDropdownComponent extends BaseComponent {
3432
3633
  }
3433
3634
  }
3434
3635
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3435
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDropdownComponent, isStandalone: true, selector: "uni-dropdown", inputs: { trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, ariaHasPopup: { classPropertyName: "ariaHasPopup", publicName: "ariaHasPopup", isSignal: true, isRequired: false, transformFunction: null }, paddingVertical: { classPropertyName: "paddingVertical", publicName: "paddingVertical", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dropdownShowing: "dropdownShowing", dropdownHiding: "dropdownHiding" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }], viewQueries: [{ propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
3636
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDropdownComponent, isStandalone: true, selector: "uni-dropdown", inputs: { trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, ariaHasPopup: { classPropertyName: "ariaHasPopup", publicName: "ariaHasPopup", isSignal: true, isRequired: false, transformFunction: null }, paddingVertical: { classPropertyName: "paddingVertical", publicName: "paddingVertical", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null }, border: { classPropertyName: "border", publicName: "border", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, shadow: { classPropertyName: "shadow", publicName: "shadow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dropdownShowing: "dropdownShowing", dropdownHiding: "dropdownHiding" }, providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }], viewQueries: [{ propertyName: "dropdownRef", first: true, predicate: ["dropdown"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: `
3436
3637
  <!-- 1. The native 'popover' attribute brings it to the top layer with native light-dismiss -->
3437
3638
  <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3438
3639
  <div
3439
3640
  box-layout
3440
- [border]="componentOptions().border"
3441
- [borderRadius]="componentOptions().borderRadius"
3641
+ [border]="border() ?? componentOptions().border"
3642
+ [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
3442
3643
  [paddingVertical]="paddingVertical()"
3443
3644
  [paddingHorizontal]="paddingHorizontal()"
3444
- [color]="componentOptions().color"
3445
- [shadow]="componentOptions().shadow"
3645
+ [color]="color() ?? componentOptions().color"
3646
+ [shadow]="shadow() ?? componentOptions().shadow"
3446
3647
  >
3447
3648
  <ng-content></ng-content>
3448
3649
  </div>
@@ -3460,12 +3661,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3460
3661
  <div #dropdown popover="auto" [id]="popoverId" [class]="dropdownClass()">
3461
3662
  <div
3462
3663
  box-layout
3463
- [border]="componentOptions().border"
3464
- [borderRadius]="componentOptions().borderRadius"
3664
+ [border]="border() ?? componentOptions().border"
3665
+ [borderRadius]="borderRadius() ?? componentOptions().borderRadius"
3465
3666
  [paddingVertical]="paddingVertical()"
3466
3667
  [paddingHorizontal]="paddingHorizontal()"
3467
- [color]="componentOptions().color"
3468
- [shadow]="componentOptions().shadow"
3668
+ [color]="color() ?? componentOptions().color"
3669
+ [shadow]="shadow() ?? componentOptions().shadow"
3469
3670
  >
3470
3671
  <ng-content></ng-content>
3471
3672
  </div>
@@ -3473,13 +3674,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3473
3674
  `,
3474
3675
  providers: [{ provide: COMPONENT_NAME, useValue: 'dropdown' }],
3475
3676
  }]
3476
- }], propDecorators: { trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaHasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaHasPopup", required: false }] }], paddingVertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingVertical", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], dropdownShowing: [{ type: i0.Output, args: ["dropdownShowing"] }], dropdownHiding: [{ type: i0.Output, args: ["dropdownHiding"] }], dropdownRef: [{
3677
+ }], propDecorators: { trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], ariaHasPopup: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaHasPopup", required: false }] }], paddingVertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingVertical", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], border: [{ type: i0.Input, args: [{ isSignal: true, alias: "border", required: false }] }], borderRadius: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderRadius", required: false }] }], shadow: [{ type: i0.Input, args: [{ isSignal: true, alias: "shadow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], dropdownShowing: [{ type: i0.Output, args: ["dropdownShowing"] }], dropdownHiding: [{ type: i0.Output, args: ["dropdownHiding"] }], dropdownRef: [{
3477
3678
  type: ViewChild,
3478
3679
  args: ['dropdown', { static: true }]
3479
3680
  }] } });
3480
3681
 
3481
- class UniExpandComponent {
3682
+ class UniExpandComponent extends BaseComponent {
3482
3683
  collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
3684
+ /**
3685
+ * Exact per-instance duration in seconds. Bypasses size-aware scaling —
3686
+ * an explicit number means the consumer wants that number. Omitted, the
3687
+ * duration derives from the `expand` theme options' `transitionSpeed`
3688
+ * scaled by content height (see `duration`).
3689
+ */
3690
+ transitionSpeed = input(...(ngDevMode ? [undefined, { debugName: "transitionSpeed" }] : /* istanbul ignore next */ []));
3483
3691
  /** Referenced by the controlling toggle's aria-controls. */
3484
3692
  regionId = uniqueId('uni-expand');
3485
3693
  /**
@@ -3488,12 +3696,49 @@ class UniExpandComponent {
3488
3696
  * animations only after the first render, i.e. on real state changes.
3489
3697
  */
3490
3698
  ready = signal(false, ...(ngDevMode ? [{ debugName: "ready" }] : /* istanbul ignore next */ []));
3699
+ contentRef = viewChild('content', ...(ngDevMode ? [{ debugName: "contentRef" }] : /* istanbul ignore next */ []));
3700
+ /**
3701
+ * Last observed content height. Retained after collapse (the observer
3702
+ * disconnects but the signal keeps its value) so the leave animation and
3703
+ * the next reveal are timed to the real size. Until the first measurement
3704
+ * lands — one frame into the very first reveal — the duration falls back
3705
+ * to the unscaled token; CSS remaps animation progress when
3706
+ * `animation-duration` updates, so the retime is imperceptible that early.
3707
+ */
3708
+ contentHeight = signal(undefined, ...(ngDevMode ? [{ debugName: "contentHeight" }] : /* istanbul ignore next */ []));
3491
3709
  constructor() {
3710
+ super();
3492
3711
  afterNextRender(() => this.ready.set(true));
3712
+ afterRenderEffect((onCleanup) => {
3713
+ const content = this.contentRef()?.nativeElement;
3714
+ if (!content)
3715
+ return;
3716
+ const observer = new ResizeObserver(() => this.contentHeight.set(content.scrollHeight));
3717
+ observer.observe(content);
3718
+ onCleanup(() => observer.disconnect());
3719
+ });
3493
3720
  }
3494
3721
  toggle() {
3495
3722
  this.collapsed.update((collapsed) => !collapsed);
3496
3723
  }
3724
+ /**
3725
+ * The resolved duration in seconds: the `transitionSpeed` input verbatim,
3726
+ * or the `expand` theme options' `transitionSpeed` scaled by content height
3727
+ * (`expandDuration` — √-of-height, clamped) so short regions stay snappy
3728
+ * and tall ones aren't rushed. The public sync hook: Expand Area binds this
3729
+ * to its toggle so the chevron rotates on the region's clock, and consumers
3730
+ * can bind adjacent styling the same way
3731
+ * (`[style.transition-duration]="expand.duration() + 's'"`).
3732
+ */
3733
+ duration = computed(() => {
3734
+ const override = this.transitionSpeed();
3735
+ if (override !== undefined)
3736
+ return override;
3737
+ const speed = this.componentOptions().transitionSpeed ?? EXPAND_DEFAULT_SPEED;
3738
+ const height = this.contentHeight();
3739
+ return height === undefined ? speed : expandDuration(height, speed);
3740
+ }, ...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
3741
+ cssDuration = computed(() => `${this.duration()}s`, ...(ngDevMode ? [{ debugName: "cssDuration" }] : /* istanbul ignore next */ []));
3497
3742
  /**
3498
3743
  * A custom element is `display: inline` by default, which would lay the
3499
3744
  * animated grid out as a block-in-inline box and distort the revealed
@@ -3519,23 +3764,29 @@ class UniExpandComponent {
3519
3764
  * decorations that legitimately paint outside the region (focus rings,
3520
3765
  * offset outlines). Angular removes a leaving node on the next frame when it
3521
3766
  * detects no animation, so nothing hangs when the guard strips them.
3767
+ *
3768
+ * The classes carry the default duration; the animated div's
3769
+ * `[style.animation-duration]` binding overrides it with the resolved
3770
+ * `duration`, so the classes stay static while timing tracks the theme and
3771
+ * the content's size through signals alone.
3522
3772
  */
3523
3773
  expandAnimation = css(motionSafe({
3524
3774
  overflow: 'hidden',
3525
- animation: `${this.expand} ease-in 350ms`,
3775
+ animation: `${this.expand} ease-in-out ${EXPAND_DEFAULT_SPEED}s`,
3526
3776
  }));
3527
3777
  collapseAnimation = css(motionSafe({
3528
3778
  overflow: 'hidden',
3529
- animation: `${this.collapse} ease-in 350ms`,
3779
+ animation: `${this.collapse} ease-in-out ${EXPAND_DEFAULT_SPEED}s`,
3530
3780
  }));
3531
3781
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniExpandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3532
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniExpandComponent, isStandalone: true, selector: "uni-expand", inputs: { collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange" }, host: { properties: { "attr.id": "regionId", "class": "hostClassName" } }, ngImport: i0, template: `@if (!collapsed()) {
3782
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniExpandComponent, isStandalone: true, selector: "uni-expand", inputs: { collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, transitionSpeed: { classPropertyName: "transitionSpeed", publicName: "transitionSpeed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange" }, host: { properties: { "attr.id": "regionId", "class": "hostClassName" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'expand' }], viewQueries: [{ propertyName: "contentRef", first: true, predicate: ["content"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `@if (!collapsed()) {
3533
3783
  <div
3534
3784
  [animate.enter]="ready() ? expandAnimation : ''"
3535
3785
  [animate.leave]="collapseAnimation"
3536
3786
  [class]="expandClassName"
3787
+ [style.animation-duration]="cssDuration()"
3537
3788
  >
3538
- <div [class]="contentClassName">
3789
+ <div #content [class]="contentClassName">
3539
3790
  <ng-content></ng-content>
3540
3791
  </div>
3541
3792
  </div>
@@ -3547,13 +3798,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3547
3798
  changeDetection: ChangeDetectionStrategy.OnPush,
3548
3799
  selector: 'uni-expand',
3549
3800
  imports: [],
3801
+ providers: [{ provide: COMPONENT_NAME, useValue: 'expand' }],
3550
3802
  template: `@if (!collapsed()) {
3551
3803
  <div
3552
3804
  [animate.enter]="ready() ? expandAnimation : ''"
3553
3805
  [animate.leave]="collapseAnimation"
3554
3806
  [class]="expandClassName"
3807
+ [style.animation-duration]="cssDuration()"
3555
3808
  >
3556
- <div [class]="contentClassName">
3809
+ <div #content [class]="contentClassName">
3557
3810
  <ng-content></ng-content>
3558
3811
  </div>
3559
3812
  </div>
@@ -3563,7 +3816,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3563
3816
  '[class]': 'hostClassName',
3564
3817
  },
3565
3818
  }]
3566
- }], ctorParameters: () => [], propDecorators: { collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }] } });
3819
+ }], ctorParameters: () => [], propDecorators: { collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], transitionSpeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "transitionSpeed", required: false }] }], contentRef: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }] } });
3567
3820
 
3568
3821
  /**
3569
3822
  * UniExpandComponent Barrel File
@@ -3571,182 +3824,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3571
3824
  * This file exports all public-facing elements of the expand component.
3572
3825
  */
3573
3826
 
3574
- class UniTooltipComponent extends BaseComponent {
3575
- elRef = inject(ElementRef);
3576
- renderer = inject(Renderer2);
3577
- timer = useTimer();
3578
- isMouseInside = signal(false, ...(ngDevMode ? [{ debugName: "isMouseInside" }] : /* istanbul ignore next */ []));
3579
- /** Whether the bubble is currently shown (or fading out). */
3580
- visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
3581
- tooltipId = uniqueId('uni-tooltip');
3582
- anchorName = newAnchorName();
3583
- hoverDelay = input(500, ...(ngDevMode ? [{ debugName: "hoverDelay" }] : /* istanbul ignore next */ []));
3584
- label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3585
- placement = input('top', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
3586
- inlineText = input(false, ...(ngDevMode ? [{ debugName: "inlineText" }] : /* istanbul ignore next */ []));
3587
- /** @deprecated The tooltip renders in the native top layer; ignored. */
3588
- appendToBody = input(false, ...(ngDevMode ? [{ debugName: "appendToBody" }] : /* istanbul ignore next */ []));
3589
- tipRef = viewChild.required('tip');
3590
- constructor() {
3591
- super();
3592
- effect(() => {
3593
- const timerActive = this.timer.isActive();
3594
- const mouseInside = this.isMouseInside();
3595
- // If the timer finished and the mouse is still inside, show tooltip
3596
- if (!timerActive && mouseInside) {
3597
- this.showTooltip();
3598
- }
3599
- // If the mouse left and the timer is not running, hide tooltip
3600
- else if (!mouseInside) {
3601
- this.hideTooltip();
3602
- }
3603
- });
3604
- // A tooltip must be reachable by keyboard (WCAG 1.4.13): when the
3605
- // projected content has no focusable element, the host itself joins
3606
- // the tab sequence. The bubble is always in the DOM, so the describedby
3607
- // relationship is wired once, on the element that receives focus.
3608
- afterNextRender(() => {
3609
- const host = this.elRef.nativeElement;
3610
- if (!host.querySelector(FOCUSABLE_SELECTOR) && !host.matches(FOCUSABLE_SELECTOR)) {
3611
- this.renderer.setAttribute(host, 'tabindex', '0');
3612
- }
3613
- this.renderer.setAttribute(resolveFocusTarget(host), 'aria-describedby', this.tooltipId);
3614
- });
3615
- }
3616
- onFocusIn(event) {
3617
- // Only keyboard-driven focus shows the tooltip immediately; mouse
3618
- // interaction keeps the hover-delay behavior.
3619
- if (event.target.matches(':focus-visible')) {
3620
- this.showTooltip();
3621
- }
3622
- }
3623
- onFocusOut() {
3624
- if (!this.isMouseInside()) {
3625
- this.hideTooltip();
3626
- }
3627
- }
3628
- onEscape(event) {
3629
- if (this.visible()) {
3630
- // Dismiss only the tooltip, not an enclosing dialog/popover
3631
- event.stopPropagation();
3632
- this.hideTooltip();
3633
- }
3634
- }
3635
- toggleTooltip() {
3636
- if (this.visible()) {
3637
- this.hideTooltip();
3638
- }
3639
- else {
3640
- this.showTooltip();
3641
- }
3642
- }
3643
- mouseenter() {
3644
- this.isMouseInside.set(true);
3645
- this.timer.start(this.hoverDelay());
3646
- }
3647
- mouseleave() {
3648
- this.isMouseInside.set(false);
3649
- this.timer.stop();
3650
- }
3651
- showTooltip() {
3652
- if (this.visible())
3653
- return;
3654
- const tip = this.tipRef().nativeElement;
3655
- tip.showPopover();
3656
- this.renderer.setAttribute(tip, 'fade', 'in');
3657
- this.visible.set(true);
3658
- }
3659
- hideTooltip() {
3660
- if (!this.visible())
3661
- return;
3662
- this.renderer.setAttribute(this.tipRef().nativeElement, 'fade', 'out');
3663
- }
3664
- onAnimationEnd(event) {
3665
- if (event.animationName.includes(this.tooltipFadeOut)) {
3666
- this.tipRef().nativeElement.hidePopover();
3667
- this.visible.set(false);
3668
- }
3669
- }
3670
- className = computed(() => css({
3671
- display: 'inline-flex',
3672
- anchorName: this.anchorName,
3673
- }, this.inlineText() && {
3674
- cursor: 'help',
3675
- textDecoration: 'underline',
3676
- textDecorationStyle: 'dotted',
3677
- }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
3678
- tooltipFadeIn = keyframes({ ...fadeIn });
3679
- tooltipFadeOut = keyframes({ ...fadeOut });
3680
- tooltipClassName = computed(() => css([
3681
- {
3682
- ...this.theme.colorPair(this.componentOptions().color),
3683
- ...this.theme.radius(this.componentOptions().borderRadius),
3684
- ...this.theme.boxShadow(this.componentOptions().shadow),
3685
- ...this.theme.typeface(this.componentOptions().typeface),
3686
- border: 'none',
3687
- padding: 5,
3688
- width: 'max-content',
3689
- ...anchorStyles(this.anchorName, this.placement(), { mainAxis: 6 }),
3690
- '&[fade="in"]': {
3691
- animation: `${this.tooltipFadeIn} ease-in 350ms`,
3692
- },
3693
- '&[fade="out"]': {
3694
- animation: `${this.tooltipFadeOut} ease-in 350ms`,
3695
- },
3696
- },
3697
- ]), ...(ngDevMode ? [{ debugName: "tooltipClassName" }] : /* istanbul ignore next */ []));
3698
- arrowClassName = computed(() => css({
3699
- ...this.theme.colorPair(this.componentOptions().color),
3700
- ...anchorArrowStyles(this.placement()),
3701
- }), ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
3702
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3703
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.12", type: UniTooltipComponent, isStandalone: true, selector: "uni-tooltip", inputs: { hoverDelay: { classPropertyName: "hoverDelay", publicName: "hoverDelay", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, inlineText: { classPropertyName: "inlineText", publicName: "inlineText", isSignal: true, isRequired: false, transformFunction: null }, appendToBody: { classPropertyName: "appendToBody", publicName: "appendToBody", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "toggleTooltip()", "mouseenter": "mouseenter()", "mouseleave": "mouseleave()", "focusin": "onFocusIn($event)", "focusout": "onFocusOut()", "keydown.escape": "onEscape($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tooltip' }], viewQueries: [{ propertyName: "tipRef", first: true, predicate: ["tip"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `<ng-content></ng-content
3704
- ><span
3705
- #tip
3706
- popover="manual"
3707
- role="tooltip"
3708
- [id]="tooltipId"
3709
- [class]="tooltipClassName()"
3710
- (mouseenter)="isMouseInside.set(true)"
3711
- (mouseleave)="isMouseInside.set(false)"
3712
- (animationend)="onAnimationEnd($event)"
3713
- >{{ label() }}<span [class]="arrowClassName()"></span
3714
- ></span>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
3715
- }
3716
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTooltipComponent, decorators: [{
3717
- type: Component,
3718
- args: [{
3719
- selector: 'uni-tooltip',
3720
- imports: [],
3721
- // The bubble lives declaratively in the template as a manual popover: the
3722
- // top layer escapes any overflow context (which made appendToBody obsolete)
3723
- // and native CSS anchor positioning keeps it attached to the host.
3724
- template: `<ng-content></ng-content
3725
- ><span
3726
- #tip
3727
- popover="manual"
3728
- role="tooltip"
3729
- [id]="tooltipId"
3730
- [class]="tooltipClassName()"
3731
- (mouseenter)="isMouseInside.set(true)"
3732
- (mouseleave)="isMouseInside.set(false)"
3733
- (animationend)="onAnimationEnd($event)"
3734
- >{{ label() }}<span [class]="arrowClassName()"></span
3735
- ></span>`,
3736
- providers: [{ provide: COMPONENT_NAME, useValue: 'tooltip' }],
3737
- changeDetection: ChangeDetectionStrategy.OnPush,
3738
- host: {
3739
- '[class]': 'className()',
3740
- '(click)': 'toggleTooltip()',
3741
- '(mouseenter)': 'mouseenter()',
3742
- '(mouseleave)': 'mouseleave()',
3743
- '(focusin)': 'onFocusIn($event)',
3744
- '(focusout)': 'onFocusOut()',
3745
- '(keydown.escape)': 'onEscape($event)',
3746
- },
3747
- }]
3748
- }], 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 }] }] } });
3749
-
3750
3827
  /**
3751
3828
  * Trigger for a {@link UniExpandComponent} region.
3752
3829
  *
@@ -3765,13 +3842,19 @@ class UniExpandToggleComponent {
3765
3842
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
3766
3843
  /** Muted qualifier beside the label ("for POs & custom orders"). Needs `label`. */
3767
3844
  sublabel = input(...(ngDevMode ? [undefined, { debugName: "sublabel" }] : /* istanbul ignore next */ []));
3845
+ /**
3846
+ * Rotation duration in seconds. Expand Area binds the region's resolved
3847
+ * `duration` here so chevron and reveal share one clock even when the
3848
+ * region is size-scaled or overridden per instance.
3849
+ */
3850
+ transitionSpeed = input(...(ngDevMode ? [undefined, { debugName: "transitionSpeed" }] : /* istanbul ignore next */ []));
3851
+ /** Fallback clock when no `transitionSpeed` is bound: the `expand` theme options' `transitionSpeed`. */
3852
+ expandOptions = inject(ThemeService).getComponentOptions('expand');
3853
+ speed = computed(() => this.transitionSpeed() ?? this.expandOptions().transitionSpeed ?? EXPAND_DEFAULT_SPEED, ...(ngDevMode ? [{ debugName: "speed" }] : /* istanbul ignore next */ []));
3768
3854
  /**
3769
3855
  * The glyph rotates, never the host.
3770
3856
  *
3771
- * The host is the tooltip's positioning box `uni-tooltip` puts its
3772
- * `anchor-name` on its own element, nested inside ours — so rotating the
3773
- * host swings the anchor through the turn and the bubble visibly bobs. The
3774
- * host is also taller than the glyph (an inline-level box reserves baseline
3857
+ * The host is taller than the glyph (an inline-level box reserves baseline
3775
3858
  * descender space), so spinning it about its own centre walks the glyph
3776
3859
  * off-centre. `uni-icon` is a centred square sized to the glyph, which makes
3777
3860
  * it the only box here that rotates symmetrically. It also keeps a label
@@ -3780,7 +3863,9 @@ class UniExpandToggleComponent {
3780
3863
  glyphStyles = computed(() => ({
3781
3864
  '& uni-icon': {
3782
3865
  flexShrink: 0,
3783
- ...motionSafe({ transition: 'transform 350ms ease-in-out' }),
3866
+ ...motionSafe({
3867
+ transition: `transform ${this.speed()}s ease-in-out`,
3868
+ }),
3784
3869
  transform: this.collapsed() ? 'rotate(-180deg)' : 'rotate(0)',
3785
3870
  },
3786
3871
  }), ...(ngDevMode ? [{ debugName: "glyphStyles" }] : /* istanbul ignore next */ []));
@@ -3817,7 +3902,7 @@ class UniExpandToggleComponent {
3817
3902
  this.collapsed.update((collapsed) => !collapsed);
3818
3903
  }
3819
3904
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniExpandToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3820
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniExpandToggleComponent, isStandalone: true, selector: "uni-expand-toggle", inputs: { collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, ariaControls: { classPropertyName: "ariaControls", publicName: "ariaControls", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, sublabel: { classPropertyName: "sublabel", publicName: "sublabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange" }, host: { properties: { "class": "className()", "attr.toggled": "collapsed() || null" } }, ngImport: i0, template: `
3905
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniExpandToggleComponent, isStandalone: true, selector: "uni-expand-toggle", inputs: { collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, ariaControls: { classPropertyName: "ariaControls", publicName: "ariaControls", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, sublabel: { classPropertyName: "sublabel", publicName: "sublabel", isSignal: true, isRequired: false, transformFunction: null }, transitionSpeed: { classPropertyName: "transitionSpeed", publicName: "transitionSpeed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange" }, host: { properties: { "class": "className()", "attr.toggled": "collapsed() || null" } }, ngImport: i0, template: `
3821
3906
  @if (label()) {
3822
3907
  <button
3823
3908
  type="button"
@@ -3835,25 +3920,23 @@ class UniExpandToggleComponent {
3835
3920
  </span>
3836
3921
  </button>
3837
3922
  } @else {
3838
- <uni-tooltip [label]="collapsed() ? 'Expand' : 'Collapse'" placement="right">
3839
- <button
3840
- icon-button
3841
- iconName="chevronUp"
3842
- (click)="toggle()"
3843
- [attr.aria-expanded]="!collapsed()"
3844
- [attr.aria-controls]="ariaControls() || null"
3845
- >
3846
- {{ collapsed() ? 'Expand' : 'Collapse' }}
3847
- </button>
3848
- </uni-tooltip>
3923
+ <button
3924
+ icon-button
3925
+ iconName="chevronUp"
3926
+ (click)="toggle()"
3927
+ [attr.aria-expanded]="!collapsed()"
3928
+ [attr.aria-controls]="ariaControls() || null"
3929
+ >
3930
+ {{ collapsed() ? 'Expand' : 'Collapse' }}
3931
+ </button>
3849
3932
  }
3850
- `, isInline: true, dependencies: [{ kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniTooltipComponent, selector: "uni-tooltip", inputs: ["hoverDelay", "label", "placement", "inlineText", "appendToBody"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3933
+ `, isInline: true, dependencies: [{ kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3851
3934
  }
3852
3935
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniExpandToggleComponent, decorators: [{
3853
3936
  type: Component,
3854
3937
  args: [{
3855
3938
  selector: 'uni-expand-toggle',
3856
- imports: [UniIconComponent, UniIconButtonComponent, UniTextComponent, UniTooltipComponent],
3939
+ imports: [UniIconComponent, UniIconButtonComponent, UniTextComponent],
3857
3940
  template: `
3858
3941
  @if (label()) {
3859
3942
  <button
@@ -3872,17 +3955,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3872
3955
  </span>
3873
3956
  </button>
3874
3957
  } @else {
3875
- <uni-tooltip [label]="collapsed() ? 'Expand' : 'Collapse'" placement="right">
3876
- <button
3877
- icon-button
3878
- iconName="chevronUp"
3879
- (click)="toggle()"
3880
- [attr.aria-expanded]="!collapsed()"
3881
- [attr.aria-controls]="ariaControls() || null"
3882
- >
3883
- {{ collapsed() ? 'Expand' : 'Collapse' }}
3884
- </button>
3885
- </uni-tooltip>
3958
+ <button
3959
+ icon-button
3960
+ iconName="chevronUp"
3961
+ (click)="toggle()"
3962
+ [attr.aria-expanded]="!collapsed()"
3963
+ [attr.aria-controls]="ariaControls() || null"
3964
+ >
3965
+ {{ collapsed() ? 'Expand' : 'Collapse' }}
3966
+ </button>
3886
3967
  }
3887
3968
  `,
3888
3969
  host: {
@@ -3891,7 +3972,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3891
3972
  },
3892
3973
  changeDetection: ChangeDetectionStrategy.OnPush,
3893
3974
  }]
3894
- }], propDecorators: { collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], ariaControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaControls", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], sublabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "sublabel", required: false }] }] } });
3975
+ }], propDecorators: { collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], ariaControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaControls", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], sublabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "sublabel", required: false }] }], transitionSpeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "transitionSpeed", required: false }] }] } });
3895
3976
 
3896
3977
  class UniExpandAreaComponent {
3897
3978
  title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
@@ -3927,6 +4008,7 @@ class UniExpandAreaComponent {
3927
4008
  style="display: inline-flex; margin: -2px"
3928
4009
  [collapsed]="initCollapsed()"
3929
4010
  [ariaControls]="expand.regionId"
4011
+ [transitionSpeed]="expand.duration()"
3930
4012
  />
3931
4013
  </div>
3932
4014
 
@@ -3936,7 +4018,7 @@ class UniExpandAreaComponent {
3936
4018
  </div>
3937
4019
  </uni-expand>
3938
4020
  </div>
3939
- `, isInline: true, dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniExpandToggleComponent, selector: "uni-expand-toggle", inputs: ["collapsed", "ariaControls", "label", "sublabel"], outputs: ["collapsedChange"] }, { kind: "component", type: UniExpandComponent, selector: "uni-expand", inputs: ["collapsed"], outputs: ["collapsedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4021
+ `, isInline: true, dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniExpandToggleComponent, selector: "uni-expand-toggle", inputs: ["collapsed", "ariaControls", "label", "sublabel", "transitionSpeed"], outputs: ["collapsedChange"] }, { kind: "component", type: UniExpandComponent, selector: "uni-expand", inputs: ["collapsed", "transitionSpeed"], outputs: ["collapsedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3940
4022
  }
3941
4023
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniExpandAreaComponent, decorators: [{
3942
4024
  type: Component,
@@ -3968,6 +4050,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3968
4050
  style="display: inline-flex; margin: -2px"
3969
4051
  [collapsed]="initCollapsed()"
3970
4052
  [ariaControls]="expand.regionId"
4053
+ [transitionSpeed]="expand.duration()"
3971
4054
  />
3972
4055
  </div>
3973
4056
 
@@ -4274,32 +4357,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4274
4357
  * This file exports all public-facing elements of the json-view component.
4275
4358
  */
4276
4359
 
4277
- class UniMenuItemComponent extends UniBoxComponent {
4360
+ const isDivider = (item) => 'divider' in item;
4361
+
4362
+ class UniMenuItemComponent {
4363
+ theme = inject(ThemeService);
4278
4364
  _elementRef = inject(ElementRef);
4279
- display = input('flex', ...(ngDevMode ? [{ debugName: "display" }] : /* istanbul ignore next */ []));
4280
- flexDirection = input('row', ...(ngDevMode ? [{ debugName: "flexDirection" }] : /* istanbul ignore next */ []));
4281
- alignItems = input('center', ...(ngDevMode ? [{ debugName: "alignItems" }] : /* istanbul ignore next */ []));
4282
- paddingHorizontal = input('md', ...(ngDevMode ? [{ debugName: "paddingHorizontal" }] : /* istanbul ignore next */ []));
4283
- gap = input('md', ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
4284
4365
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
4285
4366
  template = input(...(ngDevMode ? [undefined, { debugName: "template" }] : /* istanbul ignore next */ []));
4286
4367
  context = input(...(ngDevMode ? [undefined, { debugName: "context" }] : /* istanbul ignore next */ []));
4287
4368
  symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
4288
4369
  active = input(...(ngDevMode ? [undefined, { debugName: "active" }] : /* istanbul ignore next */ []));
4289
- hoverColor = input('primary-container', ...(ngDevMode ? [{ debugName: "hoverColor" }] : /* istanbul ignore next */ []));
4370
+ /** Tone routed through the theme's `menuItem` variants (e.g. 'warn'). */
4371
+ variant = input(...(ngDevMode ? [undefined, { debugName: "variant" }] : /* istanbul ignore next */ []));
4372
+ /** Per-instance override of the theme's `menuItem.hoverColor`. */
4373
+ hoverColor = input(...(ngDevMode ? [undefined, { debugName: "hoverColor" }] : /* istanbul ignore next */ []));
4290
4374
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4291
- menuItemClassName = computed(() => css([
4292
- {
4293
- cursor: 'pointer',
4294
- transition: 'all 0.35s ease',
4295
- height: 38,
4296
- // Roving focus highlights items the same way hover does
4297
- '&:hover, &:focus': {
4298
- ...this.theme.colorPair(this.hoverColor()),
4299
- outline: 'none',
4375
+ options = computed(() => this.theme.getComponentOptions('menuItem')(), ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4376
+ activeSymbol = computed(() => this.options().activeSymbol, ...(ngDevMode ? [{ debugName: "activeSymbol" }] : /* istanbul ignore next */ []));
4377
+ /** Typography comes from the host's themed typeface; the span only lays out. */
4378
+ LabelClassName = css({ display: 'block', whiteSpace: 'nowrap' });
4379
+ menuItemClassName = computed(() => {
4380
+ const options = this.options();
4381
+ const variant = this.variant();
4382
+ const variantStyle = variant
4383
+ ? this.theme.component('menuItem')().variants?.[variant]
4384
+ : undefined;
4385
+ const transitionSpeed = options.transitionSpeed ?? 0;
4386
+ return css([
4387
+ {
4388
+ display: 'flex',
4389
+ flexDirection: 'row',
4390
+ alignItems: 'center',
4391
+ boxSizing: 'border-box',
4392
+ cursor: 'pointer',
4393
+ height: options.height,
4394
+ ...this.theme.horizontalPadding(options.paddingHorizontal),
4395
+ ...this.theme.gap(options.gap),
4396
+ ...this.theme.radius(options.borderRadius),
4397
+ ...this.theme.typeface(options.typeface),
4398
+ ...this.theme.color(options.textColor),
4399
+ // Roving focus highlights items the same way hover does — but only
4400
+ // when focus is keyboard-driven. `onOpened()` focuses an item on every
4401
+ // open, including pointer opens, and plain `:focus` would paint that
4402
+ // as a highlight the mouse user never asked for, reading as a
4403
+ // preselected item. `:focus-visible` excludes programmatic focus that
4404
+ // follows a click while still matching keyboard navigation.
4405
+ [HOVER_OR_KEYBOARD_FOCUS]: {
4406
+ ...this.theme.colorPair(this.hoverColor() ?? options.hoverColor),
4407
+ outline: 'none',
4408
+ },
4409
+ '&[aria-disabled="true"]': {
4410
+ color: this.theme.colors()['on-disabled-surface'],
4411
+ pointerEvents: 'none',
4412
+ },
4300
4413
  },
4301
- },
4302
- ]), ...(ngDevMode ? [{ debugName: "menuItemClassName" }] : /* istanbul ignore next */ []));
4414
+ transitionSpeed > 0 && { transition: `all ${transitionSpeed}s ease` },
4415
+ // Variant tones override the base look. A variant that restyles the
4416
+ // highlight must key it with HOVER_OR_KEYBOARD_FOCUS — Emotion merges by
4417
+ // exact selector text, so a variant spelling it `&:hover, &:focus`
4418
+ // would both miss the override and reintroduce the phantom highlight.
4419
+ { ...variantStyle },
4420
+ ]);
4421
+ }, ...(ngDevMode ? [{ debugName: "menuItemClassName" }] : /* istanbul ignore next */ []));
4303
4422
  /** Host element, used by Menu for roving-focus bookkeeping. */
4304
4423
  get host() {
4305
4424
  return this._elementRef.nativeElement;
@@ -4307,21 +4426,22 @@ class UniMenuItemComponent extends UniBoxComponent {
4307
4426
  focus() {
4308
4427
  this._elementRef.nativeElement.focus();
4309
4428
  }
4310
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMenuItemComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4311
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMenuItemComponent, isStandalone: true, selector: "[uni-menu-item], [menu-item]", inputs: { display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, alignItems: { classPropertyName: "alignItems", publicName: "alignItems", isSignal: true, isRequired: false, transformFunction: null }, paddingHorizontal: { classPropertyName: "paddingHorizontal", publicName: "paddingHorizontal", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, hoverColor: { classPropertyName: "hoverColor", publicName: "hoverColor", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, properties: { "attr.aria-disabled": "disabled() ? 'true' : null", "attr.aria-current": "active() ? 'true' : null", "class": "menuItemClassName()" } }, usesInheritance: true, ngImport: i0, template: "@let tpl = template();\n@let symbol = symbolName();\n@if (symbol) {\n <uni-symbol [name]=\"symbol\"></uni-symbol>\n}\n<div box-layout [grow]=\"1\">\n @if (tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" [ngTemplateOutletContext]=\"context()\"></ng-container>\n } @else {\n <span uni-text=\"label\" display=\"block\" [nowrap]=\"true\">\n {{ label() }}\n </span>\n }\n</div>\n@if (active()) {\n <uni-symbol name=\"check\"></uni-symbol>\n}\n", styles: [":host.disabled{color:#ddd;pointer-events:none}\n"], dependencies: [{ kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4430
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMenuItemComponent, isStandalone: true, selector: "[uni-menu-item], [menu-item]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, hoverColor: { classPropertyName: "hoverColor", publicName: "hoverColor", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menuitem", "tabindex": "-1" }, properties: { "attr.aria-disabled": "disabled() ? 'true' : null", "attr.aria-current": "active() ? 'true' : null", "class": "menuItemClassName()" } }, ngImport: i0, template: "@let tpl = template();\n@let symbol = symbolName();\n@if (symbol) {\n <uni-symbol [name]=\"symbol\"></uni-symbol>\n}\n<div box-layout [grow]=\"1\">\n @if (tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" [ngTemplateOutletContext]=\"context()\"></ng-container>\n } @else {\n <span [class]=\"LabelClassName\">{{ label() }}</span>\n }\n</div>\n@let activeSym = activeSymbol();\n@if (active() && activeSym) {\n <uni-symbol [name]=\"activeSym\"></uni-symbol>\n}\n", dependencies: [{ kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4312
4431
  }
4313
4432
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMenuItemComponent, decorators: [{
4314
4433
  type: Component,
4315
- args: [{ selector: '[uni-menu-item], [menu-item]', imports: [UniTextComponent, UniSymbolComponent, UniBoxComponent, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
4434
+ args: [{ selector: '[uni-menu-item], [menu-item]', imports: [UniSymbolComponent, UniBoxComponent, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
4316
4435
  role: 'menuitem',
4317
4436
  tabindex: '-1',
4318
4437
  '[attr.aria-disabled]': "disabled() ? 'true' : null",
4319
4438
  '[attr.aria-current]': "active() ? 'true' : null",
4320
4439
  '[class]': 'menuItemClassName()',
4321
- }, template: "@let tpl = template();\n@let symbol = symbolName();\n@if (symbol) {\n <uni-symbol [name]=\"symbol\"></uni-symbol>\n}\n<div box-layout [grow]=\"1\">\n @if (tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" [ngTemplateOutletContext]=\"context()\"></ng-container>\n } @else {\n <span uni-text=\"label\" display=\"block\" [nowrap]=\"true\">\n {{ label() }}\n </span>\n }\n</div>\n@if (active()) {\n <uni-symbol name=\"check\"></uni-symbol>\n}\n", styles: [":host.disabled{color:#ddd;pointer-events:none}\n"] }]
4322
- }], propDecorators: { display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], flexDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexDirection", required: false }] }], alignItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignItems", required: false }] }], paddingHorizontal: [{ type: i0.Input, args: [{ isSignal: true, alias: "paddingHorizontal", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], template: [{ type: i0.Input, args: [{ isSignal: true, alias: "template", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], hoverColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverColor", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
4440
+ }, template: "@let tpl = template();\n@let symbol = symbolName();\n@if (symbol) {\n <uni-symbol [name]=\"symbol\"></uni-symbol>\n}\n<div box-layout [grow]=\"1\">\n @if (tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\" [ngTemplateOutletContext]=\"context()\"></ng-container>\n } @else {\n <span [class]=\"LabelClassName\">{{ label() }}</span>\n }\n</div>\n@let activeSym = activeSymbol();\n@if (active() && activeSym) {\n <uni-symbol [name]=\"activeSym\"></uni-symbol>\n}\n" }]
4441
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], template: [{ type: i0.Input, args: [{ isSignal: true, alias: "template", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], hoverColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverColor", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
4323
4442
 
4324
4443
  class UniMenuComponent {
4444
+ theme = inject(ThemeService);
4325
4445
  // Modern Signal Inputs for perfect Zoneless tracking
4326
4446
  menuItems = input.required(...(ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []));
4327
4447
  activeItem = input(...(ngDevMode ? [undefined, { debugName: "activeItem" }] : /* istanbul ignore next */ []));
@@ -4329,6 +4449,21 @@ class UniMenuComponent {
4329
4449
  // Modern Signal Output
4330
4450
  menuItemClicked = output();
4331
4451
  TriggerClassName = css({ display: 'inline-block' });
4452
+ /** Type guard for the template: dividers render as separators, not items. */
4453
+ isDivider = isDivider;
4454
+ menuOptions = computed(() => this.theme.getComponentOptions('menu')(), ...(ngDevMode ? [{ debugName: "menuOptions" }] : /* istanbul ignore next */ []));
4455
+ menuClassName = computed(() => css({ minWidth: this.menuOptions().minWidth }), ...(ngDevMode ? [{ debugName: "menuClassName" }] : /* istanbul ignore next */ []));
4456
+ dividerClassName = computed(() => {
4457
+ const { dividerBorder, dividerSpacing } = this.menuOptions();
4458
+ // Read spacing directly: getSpacing('none') yields the string 'none',
4459
+ // which is invalid as a margin and must resolve to 0 instead.
4460
+ const spacing = this.theme.spacing()[dividerSpacing ?? 'none'];
4461
+ return css({
4462
+ ...this.theme.borderTop(dividerBorder),
4463
+ marginBlock: spacing,
4464
+ marginInline: spacing,
4465
+ });
4466
+ }, ...(ngDevMode ? [{ debugName: "dividerClassName" }] : /* istanbul ignore next */ []));
4332
4467
  itemComponents = viewChildren(UniMenuItemComponent, ...(ngDevMode ? [{ debugName: "itemComponents" }] : /* istanbul ignore next */ []));
4333
4468
  dropdownComponent = viewChild(UniDropdownComponent, ...(ngDevMode ? [{ debugName: "dropdownComponent" }] : /* istanbul ignore next */ []));
4334
4469
  /** Which item receives focus when the menu opens (ArrowUp opens onto the last item). */
@@ -4340,7 +4475,7 @@ class UniMenuComponent {
4340
4475
  });
4341
4476
  }, ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
4342
4477
  handleMenuItemClick(item, dropdown) {
4343
- if (item.action) {
4478
+ if (!isDivider(item) && item.action) {
4344
4479
  item.action();
4345
4480
  }
4346
4481
  this.menuItemClicked.emit(item);
@@ -4437,7 +4572,12 @@ class UniMenuComponent {
4437
4572
  [trigger]="trigger"
4438
4573
  [placement]="placement()"
4439
4574
  ariaHasPopup="menu"
4440
- paddingVertical="xs"
4575
+ [color]="menuOptions().color"
4576
+ [border]="menuOptions().border"
4577
+ [borderRadius]="menuOptions().borderRadius"
4578
+ [shadow]="menuOptions().shadow"
4579
+ [paddingVertical]="menuOptions().paddingVertical"
4580
+ [paddingHorizontal]="menuOptions().paddingHorizontal"
4441
4581
  #dropdown
4442
4582
  (dropdownShowing)="onOpened()"
4443
4583
  >
@@ -4445,23 +4585,29 @@ class UniMenuComponent {
4445
4585
  the focused item (menu-item hosts carry tabindex="-1"), and
4446
4586
  Enter/Space activation is dispatched from onMenuKeydown. -->
4447
4587
  <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
4448
- <div role="menu" (keydown)="onMenuKeydown($event, dropdown)">
4588
+ <div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
4449
4589
  @for (item of menuItems(); track item) {
4450
- <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
4451
- <div
4452
- menu-item
4453
- [label]="item.label"
4454
- [symbolName]="item.symbolName"
4455
- [active]="activeItem() === item"
4456
- [template]="item.template"
4457
- [context]="item.context"
4458
- (click)="handleMenuItemClick(item, dropdown)"
4459
- ></div>
4590
+ @if (isDivider(item)) {
4591
+ <div role="separator" [class]="dividerClassName()"></div>
4592
+ } @else {
4593
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->
4594
+ <div
4595
+ menu-item
4596
+ [label]="item.label"
4597
+ [symbolName]="item.symbolName"
4598
+ [active]="activeItem() === item"
4599
+ [template]="item.template"
4600
+ [context]="item.context"
4601
+ [variant]="item.variant"
4602
+ [disabled]="item.disabled ?? false"
4603
+ (click)="handleMenuItemClick(item, dropdown)"
4604
+ ></div>
4605
+ }
4460
4606
  }
4461
4607
  </div>
4462
4608
  </uni-dropdown>
4463
4609
  }
4464
- `, isInline: true, dependencies: [{ kind: "component", type: UniMenuItemComponent, selector: "[uni-menu-item], [menu-item]", inputs: ["display", "flexDirection", "alignItems", "paddingHorizontal", "gap", "label", "template", "context", "symbolName", "active", "hoverColor", "disabled"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal"], outputs: ["dropdownShowing", "dropdownHiding"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4610
+ `, isInline: true, dependencies: [{ kind: "component", type: UniMenuItemComponent, selector: "[uni-menu-item], [menu-item]", inputs: ["label", "template", "context", "symbolName", "active", "variant", "hoverColor", "disabled"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "color"], outputs: ["dropdownShowing", "dropdownHiding"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4465
4611
  }
4466
4612
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMenuComponent, decorators: [{
4467
4613
  type: Component,
@@ -4482,7 +4628,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4482
4628
  [trigger]="trigger"
4483
4629
  [placement]="placement()"
4484
4630
  ariaHasPopup="menu"
4485
- paddingVertical="xs"
4631
+ [color]="menuOptions().color"
4632
+ [border]="menuOptions().border"
4633
+ [borderRadius]="menuOptions().borderRadius"
4634
+ [shadow]="menuOptions().shadow"
4635
+ [paddingVertical]="menuOptions().paddingVertical"
4636
+ [paddingHorizontal]="menuOptions().paddingHorizontal"
4486
4637
  #dropdown
4487
4638
  (dropdownShowing)="onOpened()"
4488
4639
  >
@@ -4490,18 +4641,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4490
4641
  the focused item (menu-item hosts carry tabindex="-1"), and
4491
4642
  Enter/Space activation is dispatched from onMenuKeydown. -->
4492
4643
  <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
4493
- <div role="menu" (keydown)="onMenuKeydown($event, dropdown)">
4644
+ <div role="menu" [class]="menuClassName()" (keydown)="onMenuKeydown($event, dropdown)">
4494
4645
  @for (item of menuItems(); track item) {
4495
- <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
4496
- <div
4497
- menu-item
4498
- [label]="item.label"
4499
- [symbolName]="item.symbolName"
4500
- [active]="activeItem() === item"
4501
- [template]="item.template"
4502
- [context]="item.context"
4503
- (click)="handleMenuItemClick(item, dropdown)"
4504
- ></div>
4646
+ @if (isDivider(item)) {
4647
+ <div role="separator" [class]="dividerClassName()"></div>
4648
+ } @else {
4649
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->
4650
+ <div
4651
+ menu-item
4652
+ [label]="item.label"
4653
+ [symbolName]="item.symbolName"
4654
+ [active]="activeItem() === item"
4655
+ [template]="item.template"
4656
+ [context]="item.context"
4657
+ [variant]="item.variant"
4658
+ [disabled]="item.disabled ?? false"
4659
+ (click)="handleMenuItemClick(item, dropdown)"
4660
+ ></div>
4661
+ }
4505
4662
  }
4506
4663
  </div>
4507
4664
  </uni-dropdown>
@@ -4578,11 +4735,38 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
4578
4735
  // --- CONFIGURATION ---
4579
4736
  options = input.required(...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
4580
4737
  placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
4738
+ /**
4739
+ * What the field is for, e.g. "Fruits". Rendered visually hidden inside the
4740
+ * trigger so its accessible name reads "Fruits, Apple, Pear" — without it a
4741
+ * screen reader announces only the current selection, with no clue which
4742
+ * field it belongs to.
4743
+ */
4744
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
4745
+ /** Debounce for the filter box, matching the library's input debounce. */
4746
+ debounceTime = input(200, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ []));
4747
+ srOnly = css(visuallyHidden);
4581
4748
  query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
4749
+ queryTimer;
4582
4750
  filteredOptions = computed(() => {
4583
4751
  const filterText = this.query().toLowerCase();
4584
4752
  return this.options().filter((opt) => opt.label.toLowerCase().includes(filterText));
4585
4753
  }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
4754
+ optionRefs = viewChildren('optionRow', ...(ngDevMode ? [{ debugName: "optionRefs" }] : /* istanbul ignore next */ []));
4755
+ /**
4756
+ * Roving focus over the options. The shared helper owns the index
4757
+ * arithmetic — wrapping, Home/End, and never pointing past a list the
4758
+ * filter has narrowed — the same contract `uni-search-input` and
4759
+ * `uni-tag-input` use, so the keys behave identically across all three.
4760
+ */
4761
+ list = createListboxNavigation({
4762
+ count: () => this.filteredOptions().length,
4763
+ idPrefix: 'uni-multi-select',
4764
+ });
4765
+ /** Announced with the selection so the count is not left to guesswork. */
4766
+ selectionSummary = computed(() => {
4767
+ const count = this.value().length;
4768
+ return count === 0 ? 'none selected' : `${count} selected`;
4769
+ }, ...(ngDevMode ? [{ debugName: "selectionSummary" }] : /* istanbul ignore next */ []));
4586
4770
  // Derived display string
4587
4771
  selectedLabelsText = computed(() => {
4588
4772
  const selections = this.value();
@@ -4622,7 +4806,26 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
4622
4806
  },
4623
4807
  }), ...(ngDevMode ? [{ debugName: "searchInputClass" }] : /* istanbul ignore next */ []));
4624
4808
  handleQueryInput(event) {
4625
- this.query.set(event.target.value);
4809
+ const text = event.target.value;
4810
+ // Debounced so a long option list is not re-filtered on every keystroke.
4811
+ clearTimeout(this.queryTimer);
4812
+ this.queryTimer = setTimeout(() => this.query.set(text), this.debounceTime());
4813
+ }
4814
+ /**
4815
+ * Arrow keys walk the options from anywhere in the panel, including the
4816
+ * filter box — previously the only way in was to Tab through every
4817
+ * checkbox.
4818
+ */
4819
+ onPanelKeydown(event) {
4820
+ if (!this.list.navigate(event))
4821
+ return;
4822
+ const row = this.optionRefs()[this.list.activeIndex()]?.nativeElement;
4823
+ row?.querySelector('input, [tabindex]')?.focus();
4824
+ }
4825
+ /** Keeps the active index in step when focus lands on a row by pointer. */
4826
+ onOptionFocus(index) {
4827
+ this.list.show();
4828
+ this.list.setActive(index);
4626
4829
  }
4627
4830
  selectAll() {
4628
4831
  if (this.disabled())
@@ -4657,7 +4860,7 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
4657
4860
  });
4658
4861
  }
4659
4862
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4660
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectDropdownComponent, isStandalone: true, selector: "uni-multi-select-dropdown", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], usesInheritance: true, ngImport: i0, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div stack-layout gap=\"xs\" padding=\"xs\">\n @for (option of filteredOptions(); track option.value) {\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled()\"\n >\n </uni-checkbox>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n</uni-dropdown>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniStackComponent, selector: "[uni-stack-layout], [stack-layout]", inputs: ["display", "flexDirection", "minHeight"] }, { kind: "component", type: UniDividerComponent, selector: "uni-divider", inputs: ["orientation", "border"] }, { kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4863
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectDropdownComponent, isStandalone: true, selector: "uni-multi-select-dropdown", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], viewQueries: [{ propertyName: "optionRefs", predicate: ["optionRow"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n @if (label(); as fieldLabel) {\n <span [class]=\"srOnly\">{{ fieldLabel }},</span>\n }\n <span [class]=\"srOnly\">{{ selectionSummary() }}</span>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div (keydown)=\"onPanelKeydown($event)\">\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div\n stack-layout\n gap=\"xs\"\n padding=\"xs\"\n role=\"group\"\n [attr.aria-label]=\"label() ? label() + ' options' : 'Options'\"\n >\n @for (option of filteredOptions(); track option.value; let i = $index) {\n <div #optionRow (focusin)=\"onOptionFocus(i)\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled()\"\n >\n </uni-checkbox>\n </div>\n }\n @if (filteredOptions().length === 0) {\n <span uni-text=\"label\" role=\"status\">No options match.</span>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n </div>\n</uni-dropdown>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "color"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniStackComponent, selector: "[uni-stack-layout], [stack-layout]", inputs: ["display", "flexDirection", "minHeight"] }, { kind: "component", type: UniDividerComponent, selector: "uni-divider", inputs: ["orientation", "border"] }, { kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4661
4864
  }
4662
4865
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, decorators: [{
4663
4866
  type: Component,
@@ -4672,8 +4875,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
4672
4875
  UniSymbolComponent,
4673
4876
  UniRowComponent,
4674
4877
  UniInputBoxComponent,
4675
- ], providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div stack-layout gap=\"xs\" padding=\"xs\">\n @for (option of filteredOptions(); track option.value) {\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled()\"\n >\n </uni-checkbox>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n</uni-dropdown>\n" }]
4676
- }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }] } });
4878
+ ], providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n @if (label(); as fieldLabel) {\n <span [class]=\"srOnly\">{{ fieldLabel }},</span>\n }\n <span [class]=\"srOnly\">{{ selectionSummary() }}</span>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div (keydown)=\"onPanelKeydown($event)\">\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div\n stack-layout\n gap=\"xs\"\n padding=\"xs\"\n role=\"group\"\n [attr.aria-label]=\"label() ? label() + ' options' : 'Options'\"\n >\n @for (option of filteredOptions(); track option.value; let i = $index) {\n <div #optionRow (focusin)=\"onOptionFocus(i)\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled()\"\n >\n </uni-checkbox>\n </div>\n }\n @if (filteredOptions().length === 0) {\n <span uni-text=\"label\" role=\"status\">No options match.</span>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n </div>\n</uni-dropdown>\n" }]
4879
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], optionRefs: [{ type: i0.ViewChildren, args: ['optionRow', { isSignal: true }] }] } });
4677
4880
 
4678
4881
  class UniNotificationBadgeComponent extends BaseComponent {
4679
4882
  count = input(undefined, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
@@ -5438,71 +5641,59 @@ class UniSearchInputComponent extends BaseComponent {
5438
5641
  search = output();
5439
5642
  suggestionSelected = output();
5440
5643
  field = viewChild.required(UniDebounceInputComponent);
5441
- listboxId = uniqueId('uni-search-listbox');
5442
- listOpen = signal(false, ...(ngDevMode ? [{ debugName: "listOpen" }] : /* istanbul ignore next */ []));
5443
- activeIndex = signal(-1, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
5444
5644
  visibleSuggestions = computed(() => this.suggestions().slice(0, this.componentOptions().maxSuggestions ?? 8), ...(ngDevMode ? [{ debugName: "visibleSuggestions" }] : /* istanbul ignore next */ []));
5445
- showList = computed(() => this.listOpen() && this.visibleSuggestions().length > 0, ...(ngDevMode ? [{ debugName: "showList" }] : /* istanbul ignore next */ []));
5446
- activeOptionId = computed(() => this.activeIndex() >= 0 ? `${this.listboxId}-${this.activeIndex()}` : undefined, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : /* istanbul ignore next */ []));
5645
+ /** Shared combobox bookkeeping: open state, active option, ARIA ids. */
5646
+ list = createListboxNavigation({
5647
+ count: () => this.visibleSuggestions().length,
5648
+ idPrefix: 'uni-search-listbox',
5649
+ });
5650
+ listboxId = this.list.listboxId;
5447
5651
  hasQuery = computed(() => !!this.field()?.value(), ...(ngDevMode ? [{ debugName: "hasQuery" }] : /* istanbul ignore next */ []));
5448
5652
  handleChange(value) {
5449
- this.listOpen.set(true);
5450
- this.activeIndex.set(-1);
5653
+ this.list.show();
5654
+ this.list.setActive(-1);
5451
5655
  this.change.emit(value);
5452
5656
  }
5453
5657
  submit() {
5454
- const active = this.activeIndex();
5455
- if (this.showList() && active >= 0) {
5658
+ const active = this.list.activeIndex();
5659
+ if (this.list.open() && active >= 0) {
5456
5660
  this.select(this.visibleSuggestions()[active]);
5457
5661
  return;
5458
5662
  }
5459
- this.listOpen.set(false);
5663
+ this.list.hide();
5460
5664
  this.search.emit(this.field().value() ?? '');
5461
5665
  }
5462
5666
  select(suggestion) {
5463
5667
  this.field().value.set(suggestion);
5464
- this.listOpen.set(false);
5465
- this.activeIndex.set(-1);
5668
+ this.list.hide();
5466
5669
  this.suggestionSelected.emit(suggestion);
5467
5670
  this.search.emit(suggestion);
5468
5671
  }
5469
5672
  clear() {
5470
5673
  this.field().clear();
5471
- this.listOpen.set(false);
5472
- this.activeIndex.set(-1);
5674
+ this.list.hide();
5473
5675
  this.field().focus();
5474
5676
  }
5475
5677
  onKeydown(event) {
5678
+ // Arrows and Home/End belong to the shared listbox contract.
5679
+ if (this.list.navigate(event))
5680
+ return;
5476
5681
  switch (event.key) {
5477
5682
  case 'Enter':
5478
5683
  event.preventDefault();
5479
5684
  this.submit();
5480
5685
  break;
5481
- case 'ArrowDown':
5482
- case 'ArrowUp': {
5483
- const count = this.visibleSuggestions().length;
5484
- if (count === 0)
5485
- return;
5486
- event.preventDefault();
5487
- this.listOpen.set(true);
5488
- const delta = event.key === 'ArrowDown' ? 1 : -1;
5489
- this.activeIndex.set((this.activeIndex() + delta + count) % count);
5490
- break;
5491
- }
5492
5686
  case 'Escape':
5493
- if (this.showList())
5494
- this.listOpen.set(false);
5687
+ // Escape backs out one layer at a time: close the list, then clear.
5688
+ if (this.list.open())
5689
+ this.list.hide();
5495
5690
  else if (this.hasQuery())
5496
5691
  this.clear();
5497
5692
  break;
5498
5693
  }
5499
5694
  }
5500
5695
  onFocusOut(event) {
5501
- // Close unless focus moved somewhere inside the component.
5502
- const next = event.relatedTarget;
5503
- if (!next || !event.currentTarget.contains(next)) {
5504
- this.listOpen.set(false);
5505
- }
5696
+ this.list.closeOnFocusOut(event);
5506
5697
  }
5507
5698
  className = computed(() => css({
5508
5699
  display: 'block',
@@ -5544,11 +5735,11 @@ class UniSearchInputComponent extends BaseComponent {
5544
5735
  });
5545
5736
  }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
5546
5737
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
5547
- 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]=\"showList()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"activeOptionId()\"\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 (showList()) {\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]=\"listboxId + '-' + i\"\n [class.active]=\"i === activeIndex()\"\n [attr.aria-selected]=\"i === 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 });
5738
+ 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 });
5548
5739
  }
5549
5740
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSearchInputComponent, decorators: [{
5550
5741
  type: Component,
5551
- args: [{ selector: 'uni-search-input', imports: [UniDebounceInputComponent, UniIconButtonComponent, UniSymbolComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], host: { '[class]': 'className()' }, changeDetection: ChangeDetectionStrategy.OnPush, 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]=\"showList()\"\n [ariaControls]=\"listboxId\"\n [ariaActivedescendant]=\"activeOptionId()\"\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 (showList()) {\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]=\"listboxId + '-' + i\"\n [class.active]=\"i === activeIndex()\"\n [attr.aria-selected]=\"i === activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"select(suggestion)\"\n >\n {{ suggestion }}\n </li>\n }\n </ul>\n }\n</div>\n" }]
5742
+ args: [{ selector: 'uni-search-input', imports: [UniDebounceInputComponent, UniIconButtonComponent, UniSymbolComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'searchInput' }], host: { '[class]': 'className()' }, changeDetection: ChangeDetectionStrategy.OnPush, 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" }]
5552
5743
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], search: [{ type: i0.Output, args: ["search"] }], suggestionSelected: [{ type: i0.Output, args: ["suggestionSelected"] }], field: [{ type: i0.ViewChild, args: [i0.forwardRef(() => UniDebounceInputComponent), { isSignal: true }] }] } });
5553
5744
 
5554
5745
  /**
@@ -5618,11 +5809,11 @@ class UniSelectComponent {
5618
5809
  pointerEvents: 'none' /* Crucial for clicking through */,
5619
5810
  });
5620
5811
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5621
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <option [value]=\"i\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5812
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5622
5813
  }
5623
5814
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, decorators: [{
5624
5815
  type: Component,
5625
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-select', imports: [UniInputBoxComponent, UniSymbolComponent], template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <option [value]=\"i\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n" }]
5816
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-select', imports: [UniInputBoxComponent, UniSymbolComponent], template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option [value]=\"i\" [selected]=\"currentSelectedIndex() === i.toString()\">\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n" }]
5626
5817
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
5627
5818
 
5628
5819
  /**
@@ -6279,24 +6470,180 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
6279
6470
  * This file exports all public-facing elements of the sort-header component.
6280
6471
  */
6281
6472
 
6282
- class UniTagComponent {
6473
+ /**
6474
+ * Compact chip for categories, states, filters and tokens.
6475
+ *
6476
+ * Two orthogonal style axes: `variant` picks the colour role and `tone` picks
6477
+ * the archetype (soft / solid / outline). Both live in the theme's `tag` entry,
6478
+ * so a theme restyles every chip in the app without touching markup.
6479
+ *
6480
+ * Structurally a chip is **body + trailing action as siblings**, never nested
6481
+ * buttons: an interactive chip whose body is a `<button>` cannot contain the
6482
+ * remove `<button>` (invalid HTML, and the inner control becomes unreachable
6483
+ * for keyboard users). Both stay independently operable.
6484
+ */
6485
+ class UniTagComponent extends BaseComponent {
6486
+ // Presentation. `variant` comes from BaseComponent; chips default to `md`
6487
+ // rather than the library-wide `lg`, since they sit inside dense content.
6488
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
6489
+ tone = input('soft', ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
6283
6490
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
6284
6491
  value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
6285
- // TODO(v4): rename to closed renaming is breaking
6286
- // eslint-disable-next-line @angular-eslint/no-output-native
6287
- close = output();
6288
- handleClose() {
6289
- const v = this.value();
6290
- if (v)
6291
- this.close.emit(v);
6292
- }
6293
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6294
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniTagComponent, isStandalone: true, selector: "uni-tag", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { close: "close" }, ngImport: i0, template: "<div box-layout color=\"primary\" borderRadius=\"max\" display=\"inline-block\">\n <div row-layout paddingLeft=\"sm\" gap=\"xs\">\n <span uni-text display=\"block\" typeface=\"tag\">{{ label() }}</span>\n <button icon-button symbolName=\"close\" size=\"sm\" (click)=\"handleClose()\">\n Remove {{ label() }}\n </button>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: UniBoxComponent, selector: "[uni-box-layout], [box-layout]", inputs: ["color", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "elevation", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniTextComponent, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniRowComponent, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6492
+ /** Truncation budget, e.g. `'14ch'`. A number is treated as px. */
6493
+ maxWidth = input(...(ngDevMode ? [undefined, { debugName: "maxWidth" }] : /* istanbul ignore next */ []));
6494
+ // Lead convenience inputs. Anything richer goes in the `[tag-lead]` slot.
6495
+ avatarSrc = input(...(ngDevMode ? [undefined, { debugName: "avatarSrc" }] : /* istanbul ignore next */ []));
6496
+ /** Initials fallback when `avatarSrc` is absent or fails to load. */
6497
+ avatarName = input(...(ngDevMode ? [undefined, { debugName: "avatarName" }] : /* istanbul ignore next */ []));
6498
+ /** Theme icon primitive — the preferred glyph path. */
6499
+ iconName = input(...(ngDevMode ? [undefined, { debugName: "iconName" }] : /* istanbul ignore next */ []));
6500
+ /** Material Symbols ligature, for glyphs the theme's icon set doesn't carry. */
6501
+ symbolName = input(...(ngDevMode ? [undefined, { debugName: "symbolName" }] : /* istanbul ignore next */ []));
6502
+ /** Status dot in the current colour. */
6503
+ dot = input(false, ...(ngDevMode ? [{ debugName: "dot" }] : /* istanbul ignore next */ []));
6504
+ // Behaviour
6505
+ removable = input(false, ...(ngDevMode ? [{ debugName: "removable" }] : /* istanbul ignore next */ []));
6506
+ interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
6507
+ /**
6508
+ * Toggle state. Left undefined the chip carries no `aria-pressed` at all —
6509
+ * an interactive chip is not always a toggle (inside a tag input it is
6510
+ * focusable so it can be removed), and announcing "not pressed" on a
6511
+ * recipient chip is worse than announcing nothing.
6512
+ */
6513
+ selected = input(undefined, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
6514
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
6515
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
6516
+ /** Accessible-name override for the remove button. */
6517
+ removeLabel = input(...(ngDevMode ? [undefined, { debugName: "removeLabel" }] : /* istanbul ignore next */ []));
6518
+ /**
6519
+ * Tab position of the chip's controls. A composite that owns its own
6520
+ * roving focus — `uni-tag-input`, where the whole field is one tab stop —
6521
+ * passes `-1` so Tab does not walk through every chip to reach the next
6522
+ * control.
6523
+ */
6524
+ controlTabIndex = input(0, ...(ngDevMode ? [{ debugName: "controlTabIndex" }] : /* istanbul ignore next */ []));
6525
+ removed = output();
6526
+ activated = output();
6527
+ /** A disabled chip wears the theme's `disabled` role, whatever its variant. */
6528
+ resolvedVariant = computed(() => this.disabled() ? 'disabled' : this.variant(), ...(ngDevMode ? [{ debugName: "resolvedVariant" }] : /* istanbul ignore next */ []));
6529
+ themeStyle = computed(() => this.theme.componentStyle('tag', this.resolvedVariant(), this.size())(), ...(ngDevMode ? [{ debugName: "themeStyle" }] : /* istanbul ignore next */ []));
6530
+ /** Lead elements derive from the chip height, so no second size token. */
6531
+ leadSize = computed(() => {
6532
+ const height = Number(this.themeStyle()['height'] ?? 24);
6533
+ return Number.isFinite(height) ? Math.max(height - 6, 0) : 18;
6534
+ }, ...(ngDevMode ? [{ debugName: "leadSize" }] : /* istanbul ignore next */ []));
6535
+ /** Remove glyph, proportional to the chip rather than to the icon-button. */
6536
+ removeGlyphSize = computed(() => Math.max(Math.round(this.leadSize() * 0.8), 10), ...(ngDevMode ? [{ debugName: "removeGlyphSize" }] : /* istanbul ignore next */ []));
6537
+ initials = computed(() => (this.avatarName() ?? '')
6538
+ .split(/\s+/)
6539
+ .filter(Boolean)
6540
+ .slice(0, 2)
6541
+ .map((part) => part[0]?.toUpperCase() ?? '')
6542
+ .join(''), ...(ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []));
6543
+ hostClass = computed(() => {
6544
+ const options = this.componentOptions();
6545
+ return `${css([
6546
+ this.theme.radius(options.borderRadius),
6547
+ { ...this.theme.typeface(options.typeface) },
6548
+ // Theme `fixed` + variant (incl. its nested `&.tone-*` rules) + size.
6549
+ { ...this.themeStyle() },
6550
+ { ...this.theme.gap(options.gap) },
6551
+ {
6552
+ boxSizing: 'border-box',
6553
+ maxWidth: this.maxWidth(),
6554
+ // The chip is not a widget; only its sub-controls take focus.
6555
+ '& > button': this.theme.focusRing(),
6556
+ // The remove control is the trailing counterpart of the lead, so it
6557
+ // sizes from the chip too. Left at the icon-button's own `sm` size it
6558
+ // is 22px inside a 24px chip — and taller than an `sm` chip entirely.
6559
+ '& > button[uni-icon-button]': {
6560
+ flex: 'none',
6561
+ width: this.leadSize(),
6562
+ minWidth: this.leadSize(),
6563
+ height: this.leadSize(),
6564
+ minHeight: this.leadSize(),
6565
+ padding: 0,
6566
+ fontSize: this.removeGlyphSize(),
6567
+ // uni-icon writes width/height as *inline* styles, sized from the
6568
+ // icon-button's own `sm` token (18px — wider than an `sm` chip).
6569
+ // Only !important can reach past an inline style to keep the glyph
6570
+ // proportional to the chip.
6571
+ '& uni-icon': {
6572
+ width: `${this.removeGlyphSize()}px !important`,
6573
+ height: `${this.removeGlyphSize()}px !important`,
6574
+ },
6575
+ },
6576
+ },
6577
+ this.invalid() && {
6578
+ // Colour alone cannot carry "this entry is malformed" (WCAG 1.4.1).
6579
+ textDecoration: 'underline dashed',
6580
+ textUnderlineOffset: 3,
6581
+ },
6582
+ this.disabled() && { pointerEvents: 'none' },
6583
+ ])} tone-${this.tone()}`;
6584
+ }, ...(ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []));
6585
+ /** Truncating label; `title` exposes the full text when a budget is set. */
6586
+ labelClass = css({
6587
+ overflow: 'hidden',
6588
+ textOverflow: 'ellipsis',
6589
+ whiteSpace: 'nowrap',
6590
+ });
6591
+ bodyClass = computed(() => css([
6592
+ {
6593
+ display: 'inline-flex',
6594
+ alignItems: 'center',
6595
+ minWidth: 0,
6596
+ // The body inherits the chip's own colours in every case.
6597
+ font: 'inherit',
6598
+ color: 'inherit',
6599
+ background: 'none',
6600
+ border: 0,
6601
+ padding: 0,
6602
+ ...this.theme.gap(this.componentOptions().gap),
6603
+ },
6604
+ this.interactive() && { cursor: 'pointer' },
6605
+ ]), ...(ngDevMode ? [{ debugName: "bodyClass" }] : /* istanbul ignore next */ []));
6606
+ leadClass = computed(() => css({
6607
+ flex: 'none',
6608
+ display: 'inline-flex',
6609
+ alignItems: 'center',
6610
+ justifyContent: 'center',
6611
+ overflow: 'hidden',
6612
+ width: this.leadSize(),
6613
+ height: this.leadSize(),
6614
+ borderRadius: '50%',
6615
+ fontSize: Math.max(Math.round(this.leadSize() * 0.5), 8),
6616
+ // Initials sit on a wash of the current ink so they read on any tone.
6617
+ backgroundColor: this.avatarSrc() ? undefined : 'rgba(0, 0, 0, 0.12)',
6618
+ '& img': { width: '100%', height: '100%', objectFit: 'cover' },
6619
+ }), ...(ngDevMode ? [{ debugName: "leadClass" }] : /* istanbul ignore next */ []));
6620
+ dotClass = computed(() => css({
6621
+ flex: 'none',
6622
+ width: Math.max(Math.round(this.leadSize() / 3), 6),
6623
+ height: Math.max(Math.round(this.leadSize() / 3), 6),
6624
+ borderRadius: '50%',
6625
+ backgroundColor: 'currentColor',
6626
+ }), ...(ngDevMode ? [{ debugName: "dotClass" }] : /* istanbul ignore next */ []));
6627
+ remove() {
6628
+ if (this.disabled())
6629
+ return;
6630
+ this.removed.emit(this.value());
6631
+ }
6632
+ activate() {
6633
+ if (this.disabled())
6634
+ return;
6635
+ this.activated.emit(this.value());
6636
+ }
6637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
6638
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagComponent, isStandalone: true, selector: "uni-tag", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, avatarSrc: { classPropertyName: "avatarSrc", publicName: "avatarSrc", isSignal: true, isRequired: false, transformFunction: null }, avatarName: { classPropertyName: "avatarName", publicName: "avatarName", isSignal: true, isRequired: false, transformFunction: null }, iconName: { classPropertyName: "iconName", publicName: "iconName", isSignal: true, isRequired: false, transformFunction: null }, symbolName: { classPropertyName: "symbolName", publicName: "symbolName", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, controlTabIndex: { classPropertyName: "controlTabIndex", publicName: "controlTabIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { removed: "removed", activated: "activated" }, host: { properties: { "class": "hostClass()", "attr.aria-invalid": "invalid() ? 'true' : null" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tag' }], usesInheritance: true, ngImport: i0, template: "<!-- Lead + label. Shared so the static and interactive bodies stay identical. -->\n<ng-template #body>\n <ng-content select=\"[tag-lead]\"></ng-content>\n\n @if (selected() && componentOptions().selectedIcon; as selectedIcon) {\n <uni-icon [name]=\"selectedIcon\" [size]=\"leadSize()\" />\n } @else if (avatarSrc(); as src) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">\n <img [src]=\"src\" alt=\"\" />\n </span>\n } @else if (initials(); as text) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">{{ text }}</span>\n } @else if (iconName(); as icon) {\n <uni-icon [name]=\"icon\" [size]=\"leadSize()\" />\n } @else if (symbolName(); as symbol) {\n <uni-symbol [name]=\"symbol\" [style.font-size.px]=\"leadSize()\" />\n } @else if (dot()) {\n <span [class]=\"dotClass()\" aria-hidden=\"true\"></span>\n }\n\n @if (label(); as text) {\n <span [class]=\"labelClass\" [attr.title]=\"maxWidth() ? text : null\">{{ text }}</span>\n } @else {\n <span [class]=\"labelClass\"><ng-content></ng-content></span>\n }\n</ng-template>\n\n<!-- An interactive chip's body is the button; a static chip is plain text with\n no role and no tab stop, so screen readers read it as content. -->\n@if (interactive()) {\n <button\n type=\"button\"\n [class]=\"bodyClass()\"\n [disabled]=\"disabled()\"\n [attr.aria-pressed]=\"selected() ?? null\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"activate()\"\n >\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </button>\n} @else {\n <span [class]=\"bodyClass()\">\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </span>\n}\n\n@if (removable()) {\n <button\n uni-icon-button\n variant=\"ghost\"\n size=\"sm\"\n [iconName]=\"componentOptions().removeIcon ?? 'close'\"\n [disable]=\"disabled()\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"remove()\"\n >\n {{ removeLabel() ?? 'Remove ' + (label() ?? '') }}\n </button>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { 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 });
6295
6639
  }
6296
6640
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagComponent, decorators: [{
6297
6641
  type: Component,
6298
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag', imports: [UniBoxComponent, UniIconButtonComponent, UniTextComponent, UniRowComponent], template: "<div box-layout color=\"primary\" borderRadius=\"max\" display=\"inline-block\">\n <div row-layout paddingLeft=\"sm\" gap=\"xs\">\n <span uni-text display=\"block\" typeface=\"tag\">{{ label() }}</span>\n <button icon-button symbolName=\"close\" size=\"sm\" (click)=\"handleClose()\">\n Remove {{ label() }}\n </button>\n </div>\n</div>\n" }]
6299
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], close: [{ type: i0.Output, args: ["close"] }] } });
6642
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag', imports: [NgTemplateOutlet, UniIconComponent, UniIconButtonComponent, UniSymbolComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tag' }], host: {
6643
+ '[class]': 'hostClass()',
6644
+ '[attr.aria-invalid]': "invalid() ? 'true' : null",
6645
+ }, template: "<!-- Lead + label. Shared so the static and interactive bodies stay identical. -->\n<ng-template #body>\n <ng-content select=\"[tag-lead]\"></ng-content>\n\n @if (selected() && componentOptions().selectedIcon; as selectedIcon) {\n <uni-icon [name]=\"selectedIcon\" [size]=\"leadSize()\" />\n } @else if (avatarSrc(); as src) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">\n <img [src]=\"src\" alt=\"\" />\n </span>\n } @else if (initials(); as text) {\n <span [class]=\"leadClass()\" aria-hidden=\"true\">{{ text }}</span>\n } @else if (iconName(); as icon) {\n <uni-icon [name]=\"icon\" [size]=\"leadSize()\" />\n } @else if (symbolName(); as symbol) {\n <uni-symbol [name]=\"symbol\" [style.font-size.px]=\"leadSize()\" />\n } @else if (dot()) {\n <span [class]=\"dotClass()\" aria-hidden=\"true\"></span>\n }\n\n @if (label(); as text) {\n <span [class]=\"labelClass\" [attr.title]=\"maxWidth() ? text : null\">{{ text }}</span>\n } @else {\n <span [class]=\"labelClass\"><ng-content></ng-content></span>\n }\n</ng-template>\n\n<!-- An interactive chip's body is the button; a static chip is plain text with\n no role and no tab stop, so screen readers read it as content. -->\n@if (interactive()) {\n <button\n type=\"button\"\n [class]=\"bodyClass()\"\n [disabled]=\"disabled()\"\n [attr.aria-pressed]=\"selected() ?? null\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"activate()\"\n >\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </button>\n} @else {\n <span [class]=\"bodyClass()\">\n <ng-container [ngTemplateOutlet]=\"body\"></ng-container>\n </span>\n}\n\n@if (removable()) {\n <button\n uni-icon-button\n variant=\"ghost\"\n size=\"sm\"\n [iconName]=\"componentOptions().removeIcon ?? 'close'\"\n [disable]=\"disabled()\"\n [attr.tabindex]=\"controlTabIndex()\"\n (click)=\"remove()\"\n >\n {{ removeLabel() ?? 'Remove ' + (label() ?? '') }}\n </button>\n}\n" }]
6646
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], maxWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxWidth", required: false }] }], avatarSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarSrc", required: false }] }], avatarName: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarName", required: false }] }], iconName: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconName", required: false }] }], symbolName: [{ type: i0.Input, args: [{ isSignal: true, alias: "symbolName", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], controlTabIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlTabIndex", required: false }] }], removed: [{ type: i0.Output, args: ["removed"] }], activated: [{ type: i0.Output, args: ["activated"] }] } });
6300
6647
 
6301
6648
  /**
6302
6649
  * UniTagComponent Barrel File
@@ -6304,6 +6651,392 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
6304
6651
  * This file exports all public-facing elements of the tag component.
6305
6652
  */
6306
6653
 
6654
+ /** Loose address check — deliberately permissive, matching what mail clients accept. */
6655
+ const EMAIL = /^[^\s@,;]+@[^\s@,;.]+\.[^\s@,;]+$/;
6656
+ /** `Name <a@b.com>` / `"Name" <a@b.com>` → the address, plus the display name. */
6657
+ const unwrapAddress = (raw) => {
6658
+ const match = raw.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>\s*$/);
6659
+ if (!match)
6660
+ return { value: raw.trim() };
6661
+ const label = match[1].trim();
6662
+ return { value: match[2].trim(), ...(label ? { label } : {}) };
6663
+ };
6664
+ class UniTagInputComponent extends BaseComponent {
6665
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
6666
+ value = model([], ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
6667
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
6668
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
6669
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
6670
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
6671
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
6672
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
6673
+ // --- Configuration -------------------------------------------------------
6674
+ /** Accessible name for the field, e.g. "To". */
6675
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
6676
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
6677
+ /** `email` wires an address validator, paste parser and space separator. */
6678
+ preset = input('text', ...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
6679
+ separators = input([',', ';'], ...(ngDevMode ? [{ debugName: "separators" }] : /* istanbul ignore next */ []));
6680
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
6681
+ allowDuplicates = input(false, ...(ngDevMode ? [{ debugName: "allowDuplicates" }] : /* istanbul ignore next */ []));
6682
+ max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
6683
+ validate = input(...(ngDevMode ? [undefined, { debugName: "validate" }] : /* istanbul ignore next */ []));
6684
+ parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
6685
+ // Chip presentation, forwarded to uni-tag.
6686
+ tagVariant = input('primary', ...(ngDevMode ? [{ debugName: "tagVariant" }] : /* istanbul ignore next */ []));
6687
+ tagTone = input('soft', ...(ngDevMode ? [{ debugName: "tagTone" }] : /* istanbul ignore next */ []));
6688
+ tagSize = input(...(ngDevMode ? [undefined, { debugName: "tagSize" }] : /* istanbul ignore next */ []));
6689
+ // --- Autocomplete (same contract as uni-search-input: the app filters) ----
6690
+ suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
6691
+ /** Debounced text the app should filter `suggestions` from. */
6692
+ query = output();
6693
+ debounceTime = input(250, ...(ngDevMode ? [{ debugName: "debounceTime" }] : /* istanbul ignore next */ []));
6694
+ // --- Events --------------------------------------------------------------
6695
+ added = output();
6696
+ removed = output();
6697
+ rejected = output();
6698
+ inputRef = viewChild.required('field');
6699
+ chipRefs = viewChildren('chip', ...(ngDevMode ? [{ debugName: "chipRefs" }] : /* istanbul ignore next */ []));
6700
+ /** Uncommitted text in the field. */
6701
+ draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
6702
+ /** Index of the focused chip, or -1 when focus is in the text input. */
6703
+ focusedChip = signal(-1, ...(ngDevMode ? [{ debugName: "focusedChip" }] : /* istanbul ignore next */ []));
6704
+ /** Announcement for the status region; add/remove are otherwise silent. */
6705
+ announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
6706
+ hintId = uniqueId('uni-tag-input-hint');
6707
+ srOnly = css(visuallyHidden);
6708
+ queryTimer;
6709
+ visibleSuggestions = computed(() => {
6710
+ const taken = new Set(this.value().map((item) => item.value));
6711
+ return this.suggestions()
6712
+ .filter((suggestion) => this.allowDuplicates() || !taken.has(suggestion.value))
6713
+ .slice(0, this.componentOptions().maxSuggestions ?? 8);
6714
+ }, ...(ngDevMode ? [{ debugName: "visibleSuggestions" }] : /* istanbul ignore next */ []));
6715
+ /** Shared combobox bookkeeping — identical contract to uni-search-input. */
6716
+ list = createListboxNavigation({
6717
+ count: () => this.visibleSuggestions().length,
6718
+ idPrefix: 'uni-tag-listbox',
6719
+ });
6720
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
6721
+ separatorKeys = computed(() => this.preset() === 'email' ? [...this.separators(), ' '] : this.separators(), ...(ngDevMode ? [{ debugName: "separatorKeys" }] : /* istanbul ignore next */ []));
6722
+ describedBy = computed(() => [this.ariaDescribedBy(), this.hintId].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
6723
+ labelOf(item) {
6724
+ return item.label ?? item.value;
6725
+ }
6726
+ // --- Committing ----------------------------------------------------------
6727
+ /** Split pasted text into candidate tokens. */
6728
+ parseRaw(raw) {
6729
+ const custom = this.parse();
6730
+ if (custom)
6731
+ return custom(raw);
6732
+ const pattern = this.preset() === 'email' ? /[,;\n\t]+/ : new RegExp(`[${this.separators().map((s) => `\\${s}`).join('')}\n\t]+`);
6733
+ return raw.split(pattern);
6734
+ }
6735
+ isValid(candidate) {
6736
+ const custom = this.validate();
6737
+ if (custom)
6738
+ return custom(candidate);
6739
+ return this.preset() === 'email' ? EMAIL.test(candidate) : true;
6740
+ }
6741
+ /**
6742
+ * Turn typed text into a chip. Invalid entries are kept and flagged rather
6743
+ * than dropped; duplicates and over-max are refused with a reason.
6744
+ */
6745
+ commit(raw) {
6746
+ const trimmed = raw.trim();
6747
+ if (!trimmed)
6748
+ return false;
6749
+ const { value: candidate, label } = this.preset() === 'email' ? unwrapAddress(trimmed) : { value: trimmed, label: undefined };
6750
+ if (!candidate)
6751
+ return false;
6752
+ const current = this.value();
6753
+ if (!this.allowDuplicates() && current.some((item) => item.value === candidate)) {
6754
+ this.reject(candidate, 'duplicate');
6755
+ return false;
6756
+ }
6757
+ const max = this.max();
6758
+ if (max !== undefined && current.length >= max) {
6759
+ this.reject(candidate, 'max');
6760
+ return false;
6761
+ }
6762
+ const match = this.suggestions().find((suggestion) => suggestion.value === candidate);
6763
+ const item = {
6764
+ value: candidate,
6765
+ ...(label || match?.label ? { label: label ?? match?.label } : {}),
6766
+ ...(match?.avatarSrc ? { avatarSrc: match.avatarSrc } : {}),
6767
+ ...(this.isValid(candidate) ? {} : { invalid: true }),
6768
+ };
6769
+ this.value.update((items) => [...items, item]);
6770
+ this.added.emit(item);
6771
+ this.announce(`${this.labelOf(item)} added. ${this.value().length} ${this.countNoun()}.`);
6772
+ return true;
6773
+ }
6774
+ reject(raw, reason) {
6775
+ this.rejected.emit({ raw, reason });
6776
+ // The visual cue is a brief pulse a screen reader cannot see.
6777
+ this.announce(reason === 'duplicate' ? `${raw} is already added.` : `${raw} was not added: limit reached.`);
6778
+ }
6779
+ removeAt(index, focus = 'input') {
6780
+ const item = this.value()[index];
6781
+ if (!item || item.disabled)
6782
+ return;
6783
+ this.value.update((items) => items.filter((_, i) => i !== index));
6784
+ this.removed.emit(item);
6785
+ this.announce(`${this.labelOf(item)} removed. ${this.value().length} ${this.countNoun()}.`);
6786
+ const remaining = this.value().length;
6787
+ if (focus === 'left' && index > 0)
6788
+ this.focusChip(index - 1);
6789
+ else if (focus === 'right' && index < remaining)
6790
+ this.focusChip(index);
6791
+ else
6792
+ this.focusInput();
6793
+ }
6794
+ countNoun() {
6795
+ return this.value().length === 1 ? 'item' : 'items';
6796
+ }
6797
+ announce(message) {
6798
+ // Re-announce identical text by breaking the string equality.
6799
+ this.announcement.set(this.announcement() === message ? `${message} ` : message);
6800
+ }
6801
+ // --- Focus ---------------------------------------------------------------
6802
+ focusInput() {
6803
+ this.focusedChip.set(-1);
6804
+ queueMicrotask(() => this.inputRef().nativeElement.focus());
6805
+ }
6806
+ focusChip(index) {
6807
+ const clamped = Math.max(0, Math.min(index, this.value().length - 1));
6808
+ this.focusedChip.set(clamped);
6809
+ queueMicrotask(() => {
6810
+ const chip = this.chipRefs()[clamped]?.nativeElement;
6811
+ chip?.querySelector('button')?.focus();
6812
+ });
6813
+ }
6814
+ // --- Keyboard: focus in the text input -----------------------------------
6815
+ onInputKeydown(event) {
6816
+ if (this.list.navigate(event))
6817
+ return;
6818
+ const input = this.inputRef().nativeElement;
6819
+ const empty = input.value === '';
6820
+ if (this.separatorKeys().includes(event.key) && !empty) {
6821
+ event.preventDefault();
6822
+ this.commitDraft();
6823
+ return;
6824
+ }
6825
+ switch (event.key) {
6826
+ case 'Enter': {
6827
+ event.preventDefault();
6828
+ const active = this.list.activeIndex();
6829
+ if (this.list.open() && active >= 0)
6830
+ this.selectSuggestion(this.visibleSuggestions()[active]);
6831
+ else
6832
+ this.commitDraft();
6833
+ break;
6834
+ }
6835
+ case 'Tab':
6836
+ // Never trap: commit what is typed, then let focus move on.
6837
+ if (!empty)
6838
+ this.commitDraft();
6839
+ break;
6840
+ case 'Backspace':
6841
+ if (empty && this.value().length) {
6842
+ // Focus the last chip rather than deleting blind — a second
6843
+ // Backspace, now on the chip, removes it.
6844
+ event.preventDefault();
6845
+ this.focusChip(this.value().length - 1);
6846
+ }
6847
+ break;
6848
+ case 'ArrowLeft':
6849
+ if (input.selectionStart === 0 && this.value().length) {
6850
+ event.preventDefault();
6851
+ this.focusChip(this.value().length - 1);
6852
+ }
6853
+ break;
6854
+ case 'Escape':
6855
+ if (this.list.open())
6856
+ this.list.hide();
6857
+ else
6858
+ this.setDraft('');
6859
+ break;
6860
+ }
6861
+ }
6862
+ onInput(value) {
6863
+ this.setDraft(value);
6864
+ this.list.show();
6865
+ this.list.setActive(-1);
6866
+ clearTimeout(this.queryTimer);
6867
+ this.queryTimer = setTimeout(() => this.query.emit(value), this.debounceTime());
6868
+ }
6869
+ onPaste(event) {
6870
+ const text = event.clipboardData?.getData('text') ?? '';
6871
+ if (!text)
6872
+ return;
6873
+ event.preventDefault();
6874
+ const tokens = this.parseRaw(text);
6875
+ // A trailing fragment with no separator after it is still being typed, so
6876
+ // it stays in the field rather than committing as a half-address. With a
6877
+ // custom `parse` the app owns tokenization and every token is complete.
6878
+ const separators = [...this.separatorKeys(), '\n', '\t'].map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
6879
+ const endsWithSeparator = new RegExp(`[${separators.join('')}]\\s*$`).test(text);
6880
+ const tail = this.parse() || endsWithSeparator ? '' : (tokens.pop() ?? '');
6881
+ tokens.forEach((token) => this.commit(token));
6882
+ this.setDraft(tail.trim());
6883
+ }
6884
+ onBlur() {
6885
+ this.touched.set(true);
6886
+ if (this.commitOnBlur())
6887
+ this.commitDraft();
6888
+ }
6889
+ onFocusOut(event) {
6890
+ this.list.closeOnFocusOut(event);
6891
+ }
6892
+ // --- Keyboard: focus on a chip -------------------------------------------
6893
+ onChipKeydown(event, index) {
6894
+ switch (event.key) {
6895
+ case 'ArrowLeft':
6896
+ event.preventDefault();
6897
+ if (index > 0)
6898
+ this.focusChip(index - 1);
6899
+ break;
6900
+ case 'ArrowRight':
6901
+ event.preventDefault();
6902
+ if (index < this.value().length - 1)
6903
+ this.focusChip(index + 1);
6904
+ else
6905
+ this.focusInput();
6906
+ break;
6907
+ case 'Home':
6908
+ event.preventDefault();
6909
+ this.focusChip(0);
6910
+ break;
6911
+ case 'End':
6912
+ event.preventDefault();
6913
+ this.focusChip(this.value().length - 1);
6914
+ break;
6915
+ case 'Backspace':
6916
+ // Two deletion keys with different focus outcomes: hold Backspace to
6917
+ // eat backwards, Delete to eat forwards, without the cursor jumping.
6918
+ event.preventDefault();
6919
+ this.removeAt(index, 'left');
6920
+ break;
6921
+ case 'Delete':
6922
+ event.preventDefault();
6923
+ this.removeAt(index, 'right');
6924
+ break;
6925
+ case 'Enter':
6926
+ case 'F2':
6927
+ event.preventDefault();
6928
+ this.editChip(index);
6929
+ break;
6930
+ case 'Escape':
6931
+ event.preventDefault();
6932
+ this.focusInput();
6933
+ break;
6934
+ default:
6935
+ // A printable key means the user wants to type, not navigate chips.
6936
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {
6937
+ this.setDraft(this.draft() + event.key);
6938
+ this.focusInput();
6939
+ }
6940
+ }
6941
+ }
6942
+ /** Lift a chip back into the input for correction. */
6943
+ editChip(index) {
6944
+ const item = this.value()[index];
6945
+ if (!item || item.disabled)
6946
+ return;
6947
+ this.value.update((items) => items.filter((_, i) => i !== index));
6948
+ this.removed.emit(item);
6949
+ const text = item.label && item.label !== item.value ? `${item.label} <${item.value}>` : item.value;
6950
+ this.setDraft(text);
6951
+ this.focusInput();
6952
+ }
6953
+ selectSuggestion(suggestion) {
6954
+ this.commit(suggestion.value);
6955
+ this.setDraft('');
6956
+ this.list.hide();
6957
+ this.focusInput();
6958
+ }
6959
+ commitDraft() {
6960
+ if (this.commit(this.draft()))
6961
+ this.setDraft('');
6962
+ else
6963
+ this.setDraft('');
6964
+ this.list.hide();
6965
+ }
6966
+ setDraft(text) {
6967
+ this.draft.set(text);
6968
+ const input = this.inputRef?.().nativeElement;
6969
+ if (input)
6970
+ input.value = text;
6971
+ }
6972
+ // --- Styling -------------------------------------------------------------
6973
+ className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
6974
+ fieldClass = computed(() => {
6975
+ const options = this.componentOptions();
6976
+ return css({
6977
+ display: 'flex',
6978
+ flexWrap: 'wrap',
6979
+ alignItems: 'center',
6980
+ width: '100%',
6981
+ listStyle: 'none',
6982
+ margin: 0,
6983
+ padding: 0,
6984
+ ...this.theme.gap(options.chipGap),
6985
+ });
6986
+ }, ...(ngDevMode ? [{ debugName: "fieldClass" }] : /* istanbul ignore next */ []));
6987
+ inputClass = computed(() => css({
6988
+ flex: 1,
6989
+ minWidth: this.componentOptions().minInputWidth ?? '12ch',
6990
+ border: 0,
6991
+ outline: 'none',
6992
+ background: 'transparent',
6993
+ color: 'inherit',
6994
+ font: 'inherit',
6995
+ padding: 0,
6996
+ }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
6997
+ listClass = computed(() => {
6998
+ const options = this.componentOptions();
6999
+ return css({
7000
+ position: 'absolute',
7001
+ top: '100%',
7002
+ left: 0,
7003
+ right: 0,
7004
+ zIndex: 20,
7005
+ margin: '4px 0 0',
7006
+ padding: 4,
7007
+ listStyle: 'none',
7008
+ maxHeight: 280,
7009
+ overflowY: 'auto',
7010
+ ...this.theme.backgroundColor(options.listColor ?? 'primary-surface'),
7011
+ ...this.theme.boxShadow(options.listShadow ?? 'menu'),
7012
+ ...this.theme.radius(options.listBorderRadius ?? 'xs'),
7013
+ '& [role="option"]': {
7014
+ padding: '8px 12px',
7015
+ cursor: 'pointer',
7016
+ ...this.theme.typeface('label'),
7017
+ ...this.theme.color('on-primary-surface'),
7018
+ ...this.theme.radius('xxs'),
7019
+ '&.active, &:hover': {
7020
+ ...this.theme.backgroundColor('primary-container'),
7021
+ ...this.theme.color('on-primary-container'),
7022
+ },
7023
+ },
7024
+ });
7025
+ }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
7026
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7027
+ 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 });
7028
+ }
7029
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, decorators: [{
7030
+ type: Component,
7031
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag-input', imports: [UniInputBoxComponent, UniTagComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], host: { '[class]': 'className()' }, 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" }]
7032
+ }], 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 }] }], preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", required: false }] }], separators: [{ type: i0.Input, args: [{ isSignal: true, alias: "separators", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], parse: [{ type: i0.Input, args: [{ isSignal: true, alias: "parse", required: false }] }], tagVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagVariant", required: false }] }], tagTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagTone", required: false }] }], tagSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagSize", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], query: [{ type: i0.Output, args: ["query"] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], chipRefs: [{ type: i0.ViewChildren, args: ['chip', { isSignal: true }] }] } });
7033
+
7034
+ /**
7035
+ * UniTagInputComponent Barrel File
7036
+ *
7037
+ * This file exports all public-facing elements of the tag-input component.
7038
+ */
7039
+
6307
7040
  const SCHEMES = [
6308
7041
  'monochromatic',
6309
7042
  'analogous',
@@ -7359,9 +8092,211 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
7359
8092
  args: [{ selector: 'uni-toggle', imports: [UniTextComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'toggle' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label [class]=\"toggleLabel()\">\n <input\n type=\"checkbox\"\n role=\"switch\"\n [class]=\"toggleInput()\"\n [checked]=\"checked()\"\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=\"toggle-switch\">\n <div class=\"toggle-slider\"></div>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n" }]
7360
8093
  }], 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 }] }] } });
7361
8094
 
8095
+ class UniTooltipComponent extends BaseComponent {
8096
+ elRef = inject(ElementRef);
8097
+ renderer = inject(Renderer2);
8098
+ timer = useTimer();
8099
+ isMouseInside = signal(false, ...(ngDevMode ? [{ debugName: "isMouseInside" }] : /* istanbul ignore next */ []));
8100
+ /** Whether the bubble is currently shown (or fading out). */
8101
+ visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
8102
+ /**
8103
+ * Set when the wrapped control is activated: clicking a button is not a
8104
+ * request to toggle the bubble, and re-showing it right away makes a
8105
+ * state-flipping label ("Expand" → "Collapse") blink back mid-interaction.
8106
+ * Re-arms when the pointer leaves or focus moves away.
8107
+ */
8108
+ suppressed = signal(false, ...(ngDevMode ? [{ debugName: "suppressed" }] : /* istanbul ignore next */ []));
8109
+ tooltipId = uniqueId('uni-tooltip');
8110
+ anchorName = newAnchorName();
8111
+ hoverDelay = input(500, ...(ngDevMode ? [{ debugName: "hoverDelay" }] : /* istanbul ignore next */ []));
8112
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
8113
+ placement = input('top', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
8114
+ inlineText = input(false, ...(ngDevMode ? [{ debugName: "inlineText" }] : /* istanbul ignore next */ []));
8115
+ /** @deprecated The tooltip renders in the native top layer; ignored. */
8116
+ appendToBody = input(false, ...(ngDevMode ? [{ debugName: "appendToBody" }] : /* istanbul ignore next */ []));
8117
+ tipRef = viewChild.required('tip');
8118
+ constructor() {
8119
+ super();
8120
+ effect(() => {
8121
+ const timerActive = this.timer.isActive();
8122
+ const mouseInside = this.isMouseInside();
8123
+ // If the timer finished and the mouse is still inside, show tooltip
8124
+ if (!timerActive && mouseInside && !this.suppressed()) {
8125
+ this.showTooltip();
8126
+ }
8127
+ // If the mouse left and the timer is not running, hide tooltip
8128
+ else if (!mouseInside) {
8129
+ this.hideTooltip();
8130
+ }
8131
+ });
8132
+ // A tooltip must be reachable by keyboard (WCAG 1.4.13): when the
8133
+ // projected content has no focusable element, the host itself joins
8134
+ // the tab sequence. The bubble is always in the DOM, so the describedby
8135
+ // relationship is wired once, on the element that receives focus.
8136
+ afterNextRender(() => {
8137
+ const host = this.elRef.nativeElement;
8138
+ if (!host.querySelector(FOCUSABLE_SELECTOR) && !host.matches(FOCUSABLE_SELECTOR)) {
8139
+ this.renderer.setAttribute(host, 'tabindex', '0');
8140
+ }
8141
+ this.renderer.setAttribute(resolveFocusTarget(host), 'aria-describedby', this.tooltipId);
8142
+ });
8143
+ }
8144
+ onFocusIn(event) {
8145
+ // Only keyboard-driven focus shows the tooltip immediately; mouse
8146
+ // interaction keeps the hover-delay behavior.
8147
+ if (event.target.matches(':focus-visible')) {
8148
+ this.showTooltip();
8149
+ }
8150
+ }
8151
+ onFocusOut() {
8152
+ this.suppressed.set(false);
8153
+ if (!this.isMouseInside()) {
8154
+ this.hideTooltip();
8155
+ }
8156
+ }
8157
+ onEscape(event) {
8158
+ if (this.visible()) {
8159
+ // Dismiss only the tooltip, not an enclosing dialog/popover
8160
+ event.stopPropagation();
8161
+ this.hideTooltip();
8162
+ }
8163
+ }
8164
+ /**
8165
+ * A click on an interactive element inside the host is an activation of
8166
+ * that control — hide the bubble and stay suppressed until re-arm. A click
8167
+ * anywhere else (inline-text tooltips, tap on a non-interactive host)
8168
+ * keeps the tap-to-toggle behavior.
8169
+ */
8170
+ onClick(event) {
8171
+ const host = this.elRef.nativeElement;
8172
+ const control = event.target.closest(FOCUSABLE_SELECTOR);
8173
+ if (control && control !== host && host.contains(control)) {
8174
+ this.suppressed.set(true);
8175
+ this.hideTooltip();
8176
+ }
8177
+ else {
8178
+ this.toggleTooltip();
8179
+ }
8180
+ }
8181
+ toggleTooltip() {
8182
+ if (this.visible()) {
8183
+ this.hideTooltip();
8184
+ }
8185
+ else {
8186
+ this.showTooltip();
8187
+ }
8188
+ }
8189
+ mouseenter() {
8190
+ this.isMouseInside.set(true);
8191
+ this.timer.start(this.hoverDelay());
8192
+ }
8193
+ mouseleave() {
8194
+ this.isMouseInside.set(false);
8195
+ this.suppressed.set(false);
8196
+ this.timer.stop();
8197
+ }
8198
+ showTooltip() {
8199
+ if (this.visible())
8200
+ return;
8201
+ const tip = this.tipRef().nativeElement;
8202
+ tip.showPopover();
8203
+ this.renderer.setAttribute(tip, 'fade', 'in');
8204
+ this.visible.set(true);
8205
+ }
8206
+ hideTooltip() {
8207
+ if (!this.visible())
8208
+ return;
8209
+ this.renderer.setAttribute(this.tipRef().nativeElement, 'fade', 'out');
8210
+ }
8211
+ onAnimationEnd(event) {
8212
+ if (event.animationName.includes(this.tooltipFadeOut)) {
8213
+ this.tipRef().nativeElement.hidePopover();
8214
+ this.visible.set(false);
8215
+ }
8216
+ }
8217
+ className = computed(() => css({
8218
+ display: 'inline-flex',
8219
+ anchorName: this.anchorName,
8220
+ }, this.inlineText() && {
8221
+ cursor: 'help',
8222
+ textDecoration: 'underline',
8223
+ textDecorationStyle: 'dotted',
8224
+ }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
8225
+ tooltipFadeIn = keyframes({ ...fadeIn });
8226
+ tooltipFadeOut = keyframes({ ...fadeOut });
8227
+ tooltipClassName = computed(() => css([
8228
+ {
8229
+ ...this.theme.colorPair(this.componentOptions().color),
8230
+ ...this.theme.radius(this.componentOptions().borderRadius),
8231
+ ...this.theme.boxShadow(this.componentOptions().shadow),
8232
+ ...this.theme.typeface(this.componentOptions().typeface),
8233
+ border: 'none',
8234
+ padding: 5,
8235
+ width: 'max-content',
8236
+ ...anchorStyles(this.anchorName, this.placement(), { mainAxis: 6 }),
8237
+ '&[fade="in"]': {
8238
+ animation: `${this.tooltipFadeIn} ease-in 350ms`,
8239
+ },
8240
+ '&[fade="out"]': {
8241
+ animation: `${this.tooltipFadeOut} ease-in 350ms`,
8242
+ },
8243
+ },
8244
+ ]), ...(ngDevMode ? [{ debugName: "tooltipClassName" }] : /* istanbul ignore next */ []));
8245
+ arrowClassName = computed(() => css({
8246
+ ...this.theme.colorPair(this.componentOptions().color),
8247
+ ...anchorArrowStyles(this.placement()),
8248
+ }), ...(ngDevMode ? [{ debugName: "arrowClassName" }] : /* istanbul ignore next */ []));
8249
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8250
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.12", type: UniTooltipComponent, isStandalone: true, selector: "uni-tooltip", inputs: { hoverDelay: { classPropertyName: "hoverDelay", publicName: "hoverDelay", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, inlineText: { classPropertyName: "inlineText", publicName: "inlineText", isSignal: true, isRequired: false, transformFunction: null }, appendToBody: { classPropertyName: "appendToBody", publicName: "appendToBody", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "onClick($event)", "mouseenter": "mouseenter()", "mouseleave": "mouseleave()", "focusin": "onFocusIn($event)", "focusout": "onFocusOut()", "keydown.escape": "onEscape($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tooltip' }], viewQueries: [{ propertyName: "tipRef", first: true, predicate: ["tip"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `<ng-content></ng-content
8251
+ ><span
8252
+ #tip
8253
+ popover="manual"
8254
+ role="tooltip"
8255
+ [id]="tooltipId"
8256
+ [class]="tooltipClassName()"
8257
+ (mouseenter)="isMouseInside.set(true)"
8258
+ (mouseleave)="isMouseInside.set(false)"
8259
+ (animationend)="onAnimationEnd($event)"
8260
+ >{{ label() }}<span [class]="arrowClassName()"></span
8261
+ ></span>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
8262
+ }
8263
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTooltipComponent, decorators: [{
8264
+ type: Component,
8265
+ args: [{
8266
+ selector: 'uni-tooltip',
8267
+ imports: [],
8268
+ // The bubble lives declaratively in the template as a manual popover: the
8269
+ // top layer escapes any overflow context (which made appendToBody obsolete)
8270
+ // and native CSS anchor positioning keeps it attached to the host.
8271
+ template: `<ng-content></ng-content
8272
+ ><span
8273
+ #tip
8274
+ popover="manual"
8275
+ role="tooltip"
8276
+ [id]="tooltipId"
8277
+ [class]="tooltipClassName()"
8278
+ (mouseenter)="isMouseInside.set(true)"
8279
+ (mouseleave)="isMouseInside.set(false)"
8280
+ (animationend)="onAnimationEnd($event)"
8281
+ >{{ label() }}<span [class]="arrowClassName()"></span
8282
+ ></span>`,
8283
+ providers: [{ provide: COMPONENT_NAME, useValue: 'tooltip' }],
8284
+ changeDetection: ChangeDetectionStrategy.OnPush,
8285
+ host: {
8286
+ '[class]': 'className()',
8287
+ '(click)': 'onClick($event)',
8288
+ '(mouseenter)': 'mouseenter()',
8289
+ '(mouseleave)': 'mouseleave()',
8290
+ '(focusin)': 'onFocusIn($event)',
8291
+ '(focusout)': 'onFocusOut()',
8292
+ '(keydown.escape)': 'onEscape($event)',
8293
+ },
8294
+ }]
8295
+ }], 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 }] }] } });
8296
+
7362
8297
  /**
7363
8298
  * Generated bundle index. Do not edit.
7364
8299
  */
7365
8300
 
7366
- export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, anchorArrowStyles, anchorStyles, getFileExtension, motionSafe, newAnchorName, resolveFocusTarget, uniqueId, useTimer, visuallyHidden };
8301
+ export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, ThemeService, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxComponent, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterComponent, UniCheckboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaComponent, UniGridComponent, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniRadioComponent, UniRecordDatasource, UniRowComponent, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackComponent, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextComponent, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniToggleComponent, UniTooltipComponent, UniWrapComponent, acceptableFile, anchorArrowStyles, anchorStyles, createListboxNavigation, getFileExtension, isDivider, motionSafe, newAnchorName, resolveFocusTarget, uniqueId, useTimer, visuallyHidden };
7367
8302
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map