@uni-design-system/uni-angular 10.0.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, inject, DestroyRef, computed, linkedSignal, resource, Injectable, InjectionToken, input, ChangeDetectionStrategy, Component, ElementRef, Directive, model, afterRenderEffect, Renderer2, output, viewChild, ViewChild, viewChildren, effect, untracked, isDevMode, contentChildren, afterNextRender, booleanAttribute } from '@angular/core';
2
+ import { signal, inject, DestroyRef, computed, linkedSignal, resource, Injectable, InjectionToken, input, ChangeDetectionStrategy, Component, ElementRef, Directive, model, afterRenderEffect, Renderer2, output, viewChild, ViewChild, viewChildren, effect, untracked, isDevMode, contentChildren, afterNextRender, forwardRef, booleanAttribute } from '@angular/core';
3
3
  import { injectGlobal, css, keyframes } from '@emotion/css';
4
4
  import { UniThemes, LightTheme, toTypefaces, parseTheme, formatThemeIssues, hydrateTheme, createThemeFromPalette, Z_INDEX, removeInputPlatformStyling, fadeIn, fadeOut, expandDuration, expandFadeIn, collapseFadeOut, HOVER_OR_KEYBOARD_FOCUS, ShapeRadii, generatePalette, emitThemeFile, emitDtcgTokens } from '@uni-design-system/uni-core';
5
5
  import { NgTemplateOutlet, NgClass, CommonModule } from '@angular/common';
@@ -30,9 +30,23 @@ function resolveFocusTarget(element) {
30
30
  /**
31
31
  * Visually hides content while keeping it available to screen readers.
32
32
  * Use for text alternatives (e.g. badge counts, icon-only affordances).
33
+ *
34
+ * `fixed`, not `absolute`, and that is load-bearing. An absolutely positioned
35
+ * box resolves its containing block to the nearest *positioned* ancestor —
36
+ * which, since the controls emitting these spans are `position: static`, is
37
+ * whatever positioned box happens to be above them in the consumer's layout,
38
+ * often several scroll containers up. The span then skips every intervening
39
+ * `overflow: auto` and lands in that distant ancestor's scrollable overflow,
40
+ * turning 1x1 of invisible text into real scrollable distance in a box that
41
+ * never opted into scrolling. A fixed box's containing block is the viewport,
42
+ * so it joins no ancestor's scrollable overflow at all.
43
+ *
44
+ * Caveat: inside a `transform`ed (or `filter`ed/`contain`ed) ancestor a fixed
45
+ * box re-anchors to that ancestor. Harmless here — the element is 1x1 and
46
+ * clipped to nothing, so where it lands never matters, only what it overflows.
33
47
  */
34
48
  const visuallyHidden = {
35
- position: 'absolute',
49
+ position: 'fixed',
36
50
  width: 1,
37
51
  height: 1,
38
52
  padding: 0,
@@ -1017,8 +1031,17 @@ function createPressRepeat(config) {
1017
1031
  if (event) {
1018
1032
  // Keeps the pointer stream on the button even when the finger slides
1019
1033
  // off it, so `pointerup` still arrives and the run still ends.
1034
+ //
1035
+ // This also suppresses the browser's default focus handling, which is
1036
+ // why `focus` exists: a spinner button that leaves focus nowhere means
1037
+ // the arrow keys stop working the moment you click `+`, exactly when a
1038
+ // user is most likely to reach for them.
1020
1039
  event.preventDefault();
1021
1040
  const target = event.currentTarget;
1041
+ // The button is handed over as a fallback, for controls that have no
1042
+ // text field to focus (a read-only quantity stepper, where the buttons
1043
+ // are themselves the tab stops).
1044
+ config.focus?.(target instanceof HTMLElement ? target : null);
1022
1045
  // jsdom and older engines lack the method entirely.
1023
1046
  if (target instanceof Element && typeof target.setPointerCapture === 'function') {
1024
1047
  try {
@@ -2170,10 +2193,53 @@ class ThemeService {
2170
2193
  componentStyle = (componentName, variant, size) => computed(() => {
2171
2194
  const component = this.component(componentName)();
2172
2195
  const { fixed, variants, sizes } = component;
2196
+ this.warnUnthemedVariant(componentName, variant, component);
2173
2197
  const variantStyle = variants && variants[variant];
2174
2198
  const sizeStyle = sizes && sizes[size];
2175
2199
  return { ...fixed, ...variantStyle, ...sizeStyle };
2176
2200
  });
2201
+ /**
2202
+ * The active variant's roles from a component's `variantOptions` map — the
2203
+ * per-variant data a component *reads* rather than CSS it applies. See
2204
+ * `ComponentTheme.variantOptions`.
2205
+ */
2206
+ variantRoles = (componentName, variant) => computed(() => {
2207
+ const component = this.component(componentName)();
2208
+ this.warnUnthemedVariant(componentName, variant, component);
2209
+ return component.variantOptions?.[variant];
2210
+ });
2211
+ warnedVariants = new Set();
2212
+ /**
2213
+ * Say something when a component is asked for a variant its theme does not
2214
+ * define, once per component/variant pair.
2215
+ *
2216
+ * `Variant` is an open registry, so a name the theme never styled cannot be a
2217
+ * compile error — and the miss is silent by construction, because an absent
2218
+ * style spreads to nothing and an absent role falls back. That is exactly the
2219
+ * ordinary state of a work in progress: a designer registers `destructive`,
2220
+ * uses it, and has not written its theme block yet. Same reasoning as
2221
+ * {@link resolveSpacing}, whose open scale has the same problem.
2222
+ *
2223
+ * A component that themes no variants at all is not missing anything — most
2224
+ * of the library never varies by intent — so silence there is correct.
2225
+ */
2226
+ warnUnthemedVariant(componentName, variant, component) {
2227
+ const { variants, variantOptions } = component;
2228
+ if (!variants && !variantOptions)
2229
+ return;
2230
+ if (variants?.[variant] !== undefined || variantOptions?.[variant] !== undefined)
2231
+ return;
2232
+ const key = `${componentName}/${variant}`;
2233
+ if (!(typeof ngDevMode === 'undefined' || ngDevMode) || this.warnedVariants.has(key))
2234
+ return;
2235
+ this.warnedVariants.add(key);
2236
+ const defined = [
2237
+ ...new Set([...Object.keys(variants ?? {}), ...Object.keys(variantOptions ?? {})]),
2238
+ ];
2239
+ console.warn(`[uni] Unknown variant "${variant}" on "${componentName}": the active theme does not ` +
2240
+ `define it, so the component falls back to its default appearance. Add it to the ` +
2241
+ `theme's \`components.${componentName}\` entry, or use one of: ${defined.join(', ')}.`);
2242
+ }
2177
2243
  /**
2178
2244
  * Resolve a spacing token to its CSS value. `'none'` resolves through the
2179
2245
  * scale like any other token — it is `0`, not the string `'none'`, which is
@@ -2416,12 +2482,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
2416
2482
  }], ctorParameters: () => [] });
2417
2483
 
2418
2484
  const COMPONENT_NAME = new InjectionToken('');
2485
+ // `V` defaults to `unknown` alongside `T`: passing the class as a value erases
2486
+ // its parameters to `unknown`, so an `object` default made
2487
+ // `TestBed.createComponent(BaseComponent)` disagree with `ComponentFixture<BaseComponent>`.
2419
2488
  class BaseComponent {
2420
2489
  componentName = inject(COMPONENT_NAME);
2421
2490
  theme = inject(ThemeService);
2422
2491
  variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ [])); // TODO: Make Variant support undefined
2423
2492
  size = input('lg', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
2424
2493
  componentTheme = computed(() => this.theme.getComponentTheme(this.componentName)(), ...(ngDevMode ? [{ debugName: "componentTheme" }] : /* istanbul ignore next */ []));
2494
+ /**
2495
+ * The active variant's roles from the theme's `variantOptions` map, for
2496
+ * components whose accent lands on several interior elements and so cannot
2497
+ * be expressed as a single applied style. Undefined when the theme does not
2498
+ * define this variant — which also raises a dev warning.
2499
+ */
2500
+ variantRoles = computed(() => this.theme.variantRoles(this.componentName, this.variant())(), ...(ngDevMode ? [{ debugName: "variantRoles" }] : /* istanbul ignore next */ []));
2425
2501
  componentOptions = computed(() => this.theme.getComponentOptions(this.componentName)(), ...(ngDevMode ? [{ debugName: "componentOptions" }] : /* istanbul ignore next */ []));
2426
2502
  style = computed(() => this.theme.componentStyle(this.componentName, this.variant(), this.size())(), ...(ngDevMode ? [{ debugName: "style" }] : /* istanbul ignore next */ []));
2427
2503
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: BaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
@@ -2533,8 +2609,15 @@ class UniCheckboxComponent extends BaseComponent {
2533
2609
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2534
2610
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
2535
2611
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
2536
- /** Synced from required() validators by the Signal Forms [field] directive. */
2612
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
2537
2613
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
2614
+ /**
2615
+ * Accent colour token, overriding the variant's themed accent for one
2616
+ * instance. Mirrors `uni-toggle`'s input of the same name — it exists because
2617
+ * `variant` defaults to `'primary'`, so the component cannot tell "set to
2618
+ * primary" from "not set".
2619
+ */
2620
+ checkedColor = input(...(ngDevMode ? [undefined, { debugName: "checkedColor" }] : /* istanbul ignore next */ []));
2538
2621
  /**
2539
2622
  * Id(s) of external element(s) describing this control — typically your
2540
2623
  * app-rendered error message — exposed as aria-describedby.
@@ -2578,8 +2661,8 @@ class UniCheckboxComponent extends BaseComponent {
2578
2661
  display: 'block',
2579
2662
  },
2580
2663
  '& .checkbox svg .checkbox-box': {
2581
- fill: this.getThemeColor(this.componentOptions().boxColor ?? 'surface'),
2582
- stroke: this.getThemeColor(this.variant()),
2664
+ fill: this.boxColor(),
2665
+ stroke: this.accent().fill,
2583
2666
  strokeWidth: 2,
2584
2667
  rx: this.componentOptions().borderRadius || 2,
2585
2668
  ry: this.componentOptions().borderRadius || 2,
@@ -2588,7 +2671,7 @@ class UniCheckboxComponent extends BaseComponent {
2588
2671
  // Check/dash draw on the variant-filled box, so they wear its on-color.
2589
2672
  '& .checkbox svg .checkbox-check': {
2590
2673
  fill: 'none',
2591
- stroke: this.getOnColor(this.variant()),
2674
+ stroke: this.accent().on,
2592
2675
  strokeWidth: 2,
2593
2676
  strokeLinecap: 'round',
2594
2677
  strokeLinejoin: 'round',
@@ -2597,7 +2680,7 @@ class UniCheckboxComponent extends BaseComponent {
2597
2680
  transition: 'all 0.5s ease',
2598
2681
  },
2599
2682
  '& .checkbox svg .checkbox-dash': {
2600
- stroke: this.getOnColor(this.variant()),
2683
+ stroke: this.accent().on,
2601
2684
  strokeWidth: 2,
2602
2685
  strokeLinecap: 'round',
2603
2686
  opacity: 0,
@@ -2611,16 +2694,16 @@ class UniCheckboxComponent extends BaseComponent {
2611
2694
  height: 0,
2612
2695
  opacity: 0,
2613
2696
  '&:checked + .checkbox': {
2614
- borderColor: this.getThemeColor(this.variant()),
2697
+ borderColor: this.accent().fill,
2615
2698
  },
2616
2699
  '&:checked + .checkbox svg .checkbox-box': {
2617
- fill: this.getThemeColor(this.variant()),
2700
+ fill: this.accent().fill,
2618
2701
  },
2619
2702
  '&:checked + .checkbox svg .checkbox-check': {
2620
2703
  strokeDashoffset: 0,
2621
2704
  },
2622
2705
  '&:indeterminate + .checkbox svg .checkbox-box': {
2623
- fill: this.getThemeColor(this.variant()),
2706
+ fill: this.accent().fill,
2624
2707
  },
2625
2708
  '&:indeterminate + .checkbox svg .checkbox-dash': {
2626
2709
  opacity: 1,
@@ -2633,26 +2716,37 @@ class UniCheckboxComponent extends BaseComponent {
2633
2716
  // 4px to round proportionally (as the original hand-drawn ring did) —
2634
2717
  // without it the corners gap away from the box.
2635
2718
  '&:focus + .checkbox': {
2636
- ...this.theme.focusRingStyle(this.getThemeColor(this.variant()), this.componentOptions().focusRingGap),
2719
+ ...this.theme.focusRingStyle(this.accent().fill, this.componentOptions().focusRingGap),
2637
2720
  borderRadius: `${(Number(this.componentOptions().borderRadius) || 2) + 2}px`,
2638
2721
  },
2639
2722
  }), ...(ngDevMode ? [{ debugName: "checkboxInput" }] : /* istanbul ignore next */ []));
2640
- getThemeColor(token) {
2641
- const colors = this.theme.colors();
2642
- return colors[token] ? colors[token] : colors['primary'];
2643
- }
2644
- /** The content color paired with a variant fill (on-primary, on-warn, …). */
2645
- getOnColor(variant) {
2723
+ /**
2724
+ * The accent and its paired content colour, from the theme's variant roles.
2725
+ *
2726
+ * Previously the variant *name* was looked up as a colour token, which held
2727
+ * together only because every variant happened to also be a colour. With the
2728
+ * registry open that coincidence ends by design: `variant="destructive"`
2729
+ * would have missed and silently rendered primary. The theme now says which
2730
+ * colour draws the intent, and an unthemed variant warns rather than lying.
2731
+ *
2732
+ * `primary` is the last resort because it is a reserved variant name — the
2733
+ * default every component inherits.
2734
+ */
2735
+ accent = computed(() => {
2646
2736
  const colors = this.theme.colors();
2647
- return colors[`on-${variant}`] ?? colors['on-primary'];
2648
- }
2737
+ const roles = this.variantRoles();
2738
+ const accent = this.checkedColor() ?? roles?.accent ?? 'primary';
2739
+ const onAccent = roles?.onAccent ?? `on-${accent}`;
2740
+ return { fill: colors[accent], on: colors[onAccent] };
2741
+ }, ...(ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []));
2742
+ boxColor = computed(() => this.theme.colors()[this.componentOptions().boxColor ?? 'surface'], ...(ngDevMode ? [{ debugName: "boxColor" }] : /* istanbul ignore next */ []));
2649
2743
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCheckboxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2650
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniCheckboxComponent, isStandalone: true, selector: "uni-checkbox", inputs: { checked: { classPropertyName: "checked", publicName: "checked", 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: false, transformFunction: null }, indeterminate: { classPropertyName: "indeterminate", publicName: "indeterminate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touched: "touchedChange", indeterminate: "indeterminateChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], usesInheritance: true, ngImport: i0, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n", dependencies: [{ kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2744
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniCheckboxComponent, isStandalone: true, selector: "uni-checkbox", inputs: { checked: { classPropertyName: "checked", publicName: "checked", 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 }, checkedColor: { classPropertyName: "checkedColor", publicName: "checkedColor", 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: false, transformFunction: null }, indeterminate: { classPropertyName: "indeterminate", publicName: "indeterminate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touched: "touchedChange", indeterminate: "indeterminateChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], usesInheritance: true, ngImport: i0, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n", dependencies: [{ kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2651
2745
  }
2652
2746
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniCheckboxComponent, decorators: [{
2653
2747
  type: Component,
2654
2748
  args: [{ selector: 'uni-checkbox', imports: [UniTextDirective], providers: [{ provide: COMPONENT_NAME, useValue: 'checkbox' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<label [class]=\"checkboxLabel()\">\n <input\n type=\"checkbox\"\n [class]=\"checkboxInput()\"\n [checked]=\"checked()\"\n [indeterminate]=\"indeterminate()\"\n (change)=\"handleChange($event)\"\n [disabled]=\"disabled()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n />\n <div class=\"checkbox\">\n <svg viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <rect class=\"checkbox-box\" x=\"1\" y=\"1\" width=\"18\" height=\"18\"></rect>\n <polyline class=\"checkbox-check\" points=\"4 11 8 15 16 6\"></polyline>\n <line class=\"checkbox-dash\" x1=\"5\" y1=\"10\" x2=\"15\" y2=\"10\"></line>\n </svg>\n </div>\n @if (label()) {\n <span uni-text=\"label\">{{ label() }}</span>\n }\n</label>\n" }]
2655
- }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }] } });
2749
+ }], 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 }] }], checkedColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkedColor", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }, { type: i0.Output, args: ["indeterminateChange"] }] } });
2656
2750
 
2657
2751
  /**
2658
2752
  * Whether the browser can keep a top-layer popup attached to its field.
@@ -5311,7 +5405,7 @@ class UniInputComponent {
5311
5405
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5312
5406
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5313
5407
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5314
- /** Synced from required() validators by the Signal Forms [field] directive. */
5408
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
5315
5409
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
5316
5410
  /**
5317
5411
  * Id(s) of external element(s) describing this control — typically your
@@ -5327,7 +5421,7 @@ class UniInputComponent {
5327
5421
  */
5328
5422
  type = input('text', ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
5329
5423
  // --- CONSTRAINTS ---
5330
- // These are Signal Forms' own optional control inputs, so the `[field]`
5424
+ // These are Signal Forms' own optional control inputs, so the `[formField]`
5331
5425
  // directive syncs them from the field's validators the same way it syncs
5332
5426
  // `required` — and they are reflected onto the native element so the browser
5333
5427
  // can do its part (number steppers, length limits, on-screen keyboards).
@@ -5548,7 +5642,7 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
5548
5642
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5549
5643
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5550
5644
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5551
- /** Synced from required() validators by the Signal Forms [field] directive. */
5645
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
5552
5646
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
5553
5647
  /**
5554
5648
  * Id(s) of external element(s) describing this control — typically your
@@ -5699,7 +5793,7 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
5699
5793
  });
5700
5794
  }
5701
5795
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
5702
- 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() || !!option.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: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "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", "containerColor"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "directive", type: UniStackDirective, 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: "directive", type: UniTextDirective, 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: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5796
+ 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() || !!option.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", "checkedColor", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "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", "containerColor"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "directive", type: UniStackDirective, 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: "directive", type: UniTextDirective, 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: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5703
5797
  }
5704
5798
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, decorators: [{
5705
5799
  type: Component,
@@ -5765,7 +5859,7 @@ class UniMultiSelectComponent {
5765
5859
  width: '100%',
5766
5860
  });
5767
5861
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5768
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectComponent, isStandalone: true, selector: "uni-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, selections: { classPropertyName: "selections", publicName: "selections", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, checkboxGap: { classPropertyName: "checkboxGap", publicName: "checkboxGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updates: "updates" }, host: { properties: { "class": "className" } }, ngImport: i0, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n <!-- track $index: option values may be objects and consumers rebuild the\n array, so identity tracking recreated every node (NG0956). -->\n @for (option of options(); track $index) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n [disabled]=\"!!option.disabled\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5862
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectComponent, isStandalone: true, selector: "uni-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, selections: { classPropertyName: "selections", publicName: "selections", isSignal: true, isRequired: false, transformFunction: null }, flexDirection: { classPropertyName: "flexDirection", publicName: "flexDirection", isSignal: true, isRequired: false, transformFunction: null }, checkboxGap: { classPropertyName: "checkboxGap", publicName: "checkboxGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { updates: "updates" }, host: { properties: { "class": "className" } }, ngImport: i0, template: "<div box-layout display=\"flex\" [flexDirection]=\"flexDirection()\" [gap]=\"checkboxGap()\">\n <!-- track $index: option values may be objects and consumers rebuild the\n array, so identity tracking recreated every node (NG0956). -->\n @for (option of options(); track $index) {\n <div [class]=\"optionWrapper\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"values().indexOf(option.value) > -1\"\n (checkedChange)=\"handleCheck($event, option.value)\"\n variant=\"primary\"\n [disabled]=\"!!option.disabled\"\n >\n </uni-checkbox>\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "checkedColor", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5769
5863
  }
5770
5864
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectComponent, decorators: [{
5771
5865
  type: Component,
@@ -5779,8 +5873,14 @@ class UniRadioComponent extends BaseComponent {
5779
5873
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5780
5874
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5781
5875
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5782
- /** Synced from required() validators by the Signal Forms [field] directive. */
5876
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
5783
5877
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
5878
+ /**
5879
+ * Accent colour token, overriding the variant's themed accent for one
5880
+ * instance. Mirrors the input of the same name on `uni-checkbox` and
5881
+ * `uni-toggle`.
5882
+ */
5883
+ checkedColor = input(...(ngDevMode ? [undefined, { debugName: "checkedColor" }] : /* istanbul ignore next */ []));
5784
5884
  /**
5785
5885
  * Id(s) of external element(s) describing this control — typically your
5786
5886
  * app-rendered error message — exposed as aria-describedby.
@@ -5835,18 +5935,18 @@ class UniRadioComponent extends BaseComponent {
5835
5935
  height: outerCircleSize,
5836
5936
  borderRadius: '50%',
5837
5937
  border: `2px solid ${this.disabled()
5838
- ? this.getThemeColor('on-disabled')
5839
- : this.getThemeColor(this.componentOptions().ringColor ?? 'outline')}`,
5938
+ ? this.color('on-disabled')
5939
+ : this.color(this.componentOptions().ringColor ?? 'outline')}`,
5840
5940
  position: 'relative',
5841
5941
  transition: ringTransition,
5842
- backgroundColor: this.getThemeColor(this.componentOptions().fillColor ?? 'surface'),
5942
+ backgroundColor: this.color(this.componentOptions().fillColor ?? 'surface'),
5843
5943
  flexShrink: 0,
5844
5944
  },
5845
5945
  '& .radio-inner': {
5846
5946
  width: innerCircleSize,
5847
5947
  height: innerCircleSize,
5848
5948
  borderRadius: '50%',
5849
- backgroundColor: this.getThemeColor(this.variant()),
5949
+ backgroundColor: this.accent(),
5850
5950
  position: 'absolute',
5851
5951
  top: innerCircleOffset,
5852
5952
  left: innerCircleOffset,
@@ -5856,13 +5956,13 @@ class UniRadioComponent extends BaseComponent {
5856
5956
  '&:hover .radio-button': this.disabled()
5857
5957
  ? {}
5858
5958
  : {
5859
- borderColor: this.getThemeColor(this.variant()),
5959
+ borderColor: this.accent(),
5860
5960
  },
5861
5961
  '&.disabled': {
5862
5962
  cursor: 'not-allowed',
5863
5963
  opacity: 0.6,
5864
5964
  '& .radio-button': {
5865
- borderColor: this.getThemeColor('on-disabled'),
5965
+ borderColor: this.color('on-disabled'),
5866
5966
  },
5867
5967
  },
5868
5968
  });
@@ -5874,31 +5974,44 @@ class UniRadioComponent extends BaseComponent {
5874
5974
  height: 0,
5875
5975
  opacity: 0,
5876
5976
  '&:checked + .radio-button': {
5877
- borderColor: this.getThemeColor(this.variant()),
5977
+ borderColor: this.accent(),
5878
5978
  },
5879
5979
  '&:checked + .radio-button .radio-inner': {
5880
5980
  transform: 'scale(1)',
5881
5981
  },
5882
5982
  // The shared, themable focus indicator, keyed off the hidden input.
5883
5983
  '&:focus + .radio-button': {
5884
- ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
5984
+ ...this.theme.focusRingStyle(this.accent()),
5885
5985
  },
5886
5986
  }), ...(ngDevMode ? [{ debugName: "radioInputClass" }] : /* istanbul ignore next */ []));
5887
5987
  handleRadioChange(optionValue) {
5888
5988
  this.value.set(optionValue);
5889
5989
  this.markAsTouched();
5890
5990
  }
5891
- getThemeColor(token) {
5892
- const colors = this.theme.colors();
5893
- return colors[token] ? colors[token] : colors['primary'];
5991
+ /**
5992
+ * The accent colour, from the theme's variant roles rather than by treating
5993
+ * the variant name as a colour token see `uni-checkbox` for why that had
5994
+ * to stop. `primary` is the last resort: a reserved variant name.
5995
+ */
5996
+ /**
5997
+ * A chrome colour by token. Unlike the `getThemeColor` this replaces, there
5998
+ * is no silent fallback to primary: these are tokens the theme is required
5999
+ * to define, so a miss should be visible rather than disguised.
6000
+ */
6001
+ color(token) {
6002
+ return this.theme.colors()[token];
5894
6003
  }
6004
+ accent = computed(() => {
6005
+ const accent = this.checkedColor() ?? this.variantRoles()?.accent ?? 'primary';
6006
+ return this.theme.colors()[accent];
6007
+ }, ...(ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []));
5895
6008
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniRadioComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
5896
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniRadioComponent, isStandalone: true, selector: "uni-radio", 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 }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'radio' }], usesInheritance: true, ngImport: i0, template: "<div\n [class]=\"radioGroupClass\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label() ? groupLabelId : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n>\n @if (label()) {\n <span uni-text=\"label\" [attr.id]=\"groupLabelId\">{{ label() }}</span>\n }\n\n @for (option of options(); track option.value) {\n <label [class]=\"radioOptionClass()\" [class.disabled]=\"disabled() || option.disabled\">\n <input\n type=\"radio\"\n [class]=\"radioInputClass()\"\n [value]=\"option.value\"\n [name]=\"name()\"\n [checked]=\"value() === option.value\"\n [disabled]=\"disabled() || option.disabled\"\n (change)=\"handleRadioChange(option.value)\"\n />\n <div class=\"radio-button\">\n <div class=\"radio-inner\"></div>\n </div>\n <span uni-text=\"label\">{{ option.label }}</span>\n </label>\n }\n</div>\n", dependencies: [{ kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6009
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniRadioComponent, isStandalone: true, selector: "uni-radio", 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 }, checkedColor: { classPropertyName: "checkedColor", publicName: "checkedColor", 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 }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'radio' }], usesInheritance: true, ngImport: i0, template: "<div\n [class]=\"radioGroupClass\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label() ? groupLabelId : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n>\n @if (label()) {\n <span uni-text=\"label\" [attr.id]=\"groupLabelId\">{{ label() }}</span>\n }\n\n @for (option of options(); track option.value) {\n <label [class]=\"radioOptionClass()\" [class.disabled]=\"disabled() || option.disabled\">\n <input\n type=\"radio\"\n [class]=\"radioInputClass()\"\n [value]=\"option.value\"\n [name]=\"name()\"\n [checked]=\"value() === option.value\"\n [disabled]=\"disabled() || option.disabled\"\n (change)=\"handleRadioChange(option.value)\"\n />\n <div class=\"radio-button\">\n <div class=\"radio-inner\"></div>\n </div>\n <span uni-text=\"label\">{{ option.label }}</span>\n </label>\n }\n</div>\n", dependencies: [{ kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5897
6010
  }
5898
6011
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniRadioComponent, decorators: [{
5899
6012
  type: Component,
5900
6013
  args: [{ selector: 'uni-radio', imports: [UniTextDirective], providers: [{ provide: COMPONENT_NAME, useValue: 'radio' }], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n [class]=\"radioGroupClass\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label() ? groupLabelId : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n>\n @if (label()) {\n <span uni-text=\"label\" [attr.id]=\"groupLabelId\">{{ label() }}</span>\n }\n\n @for (option of options(); track option.value) {\n <label [class]=\"radioOptionClass()\" [class.disabled]=\"disabled() || option.disabled\">\n <input\n type=\"radio\"\n [class]=\"radioInputClass()\"\n [value]=\"option.value\"\n [name]=\"name()\"\n [checked]=\"value() === option.value\"\n [disabled]=\"disabled() || option.disabled\"\n (change)=\"handleRadioChange(option.value)\"\n />\n <div class=\"radio-button\">\n <div class=\"radio-inner\"></div>\n </div>\n <span uni-text=\"label\">{{ option.label }}</span>\n </label>\n }\n</div>\n" }]
5901
- }], 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 }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }] } });
6014
+ }], 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 }] }], checkedColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkedColor", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }] } });
5902
6015
 
5903
6016
  /**
5904
6017
  * Text input that emits `change` only after the user pauses typing. Wears the
@@ -6090,7 +6203,7 @@ class UniSelectComponent {
6090
6203
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
6091
6204
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
6092
6205
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
6093
- /** Synced from required() validators by the Signal Forms [field] directive. */
6206
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
6094
6207
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
6095
6208
  /**
6096
6209
  * Id(s) of external element(s) describing this control — typically your
@@ -6537,6 +6650,9 @@ class UniNumberInputComponent extends BaseComponent {
6537
6650
  onRelease: () => this.announceValue(),
6538
6651
  disabled: () => this.disabled() || this.readOnly() || this.atMax(),
6539
6652
  repeat: () => this.repeat(),
6653
+ // A native spinner leaves focus in its field; without this the arrow keys
6654
+ // go dead the moment you click a stepper.
6655
+ focus: () => this.inputRef().nativeElement.focus(),
6540
6656
  timing: this.repeatTiming,
6541
6657
  });
6542
6658
  decrement = createPressRepeat({
@@ -6544,6 +6660,9 @@ class UniNumberInputComponent extends BaseComponent {
6544
6660
  onRelease: () => this.announceValue(),
6545
6661
  disabled: () => this.disabled() || this.readOnly() || this.atMin(),
6546
6662
  repeat: () => this.repeat(),
6663
+ // A native spinner leaves focus in its field; without this the arrow keys
6664
+ // go dead the moment you click a stepper.
6665
+ focus: () => this.inputRef().nativeElement.focus(),
6547
6666
  timing: this.repeatTiming,
6548
6667
  });
6549
6668
  // --- Input events ---------------------------------------------------------
@@ -7947,11 +8066,13 @@ class UniQuantityStepperComponent extends BaseComponent {
7947
8066
  increment = createPressRepeat({
7948
8067
  onStep: () => this.applyStep(1, false),
7949
8068
  onRelease: () => this.announceValue(),
8069
+ focus: (button) => this.focusField(button),
7950
8070
  disabled: () => this.disabled() || this.atMax(),
7951
8071
  });
7952
8072
  decrement = createPressRepeat({
7953
8073
  onStep: () => this.applyStep(-1, false),
7954
8074
  onRelease: () => this.announceValue(),
8075
+ focus: (button) => this.focusField(button),
7955
8076
  disabled: () => this.disabled() || this.atMin(),
7956
8077
  });
7957
8078
  // --- Keyboard -------------------------------------------------------------
@@ -7985,8 +8106,17 @@ class UniQuantityStepperComponent extends BaseComponent {
7985
8106
  this.increment.cancel();
7986
8107
  this.decrement.cancel();
7987
8108
  }
7988
- focusField() {
7989
- this.inputRef()?.nativeElement.focus();
8109
+ /**
8110
+ * Focus the field a stepper press should land in. With `editable=false` there
8111
+ * is no field and the buttons are the tab stops, so the pressed button takes
8112
+ * it instead.
8113
+ */
8114
+ focusField(fallback) {
8115
+ const input = this.inputRef()?.nativeElement;
8116
+ if (input)
8117
+ input.focus();
8118
+ else
8119
+ fallback?.focus();
7990
8120
  }
7991
8121
  // --- Styling --------------------------------------------------------------
7992
8122
  className = computed(() => css({ display: 'inline-block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
@@ -8000,7 +8130,6 @@ class UniQuantityStepperComponent extends BaseComponent {
8000
8130
  */
8001
8131
  fieldChrome = this.theme.getComponentOptions('input');
8002
8132
  rootClass = computed(() => {
8003
- const options = this.componentOptions();
8004
8133
  const colors = this.theme.colors();
8005
8134
  const chrome = this.fieldChrome();
8006
8135
  return css({
@@ -8011,9 +8140,9 @@ class UniQuantityStepperComponent extends BaseComponent {
8011
8140
  // field beside it rather than 32 plus its border.
8012
8141
  boxSizing: 'border-box',
8013
8142
  overflow: 'hidden',
8014
- ...this.theme.backgroundColor(this.disabled() ? 'disabled-surface' : (options.color ?? 'primary-surface')),
8015
- ...this.theme.border(options.border ?? 'light'),
8016
- ...this.theme.radius(options.borderRadius ?? 'xs'),
8143
+ ...this.theme.backgroundColor(this.disabled() ? chrome.disabledColor : this.containerColor()),
8144
+ ...this.theme.border(this.containerBorder()),
8145
+ ...this.theme.radius(this.containerRadius()),
8017
8146
  ...(this.showError() ? { borderColor: colors['warn'] } : {}),
8018
8147
  ...(this.disabled() ? { cursor: 'not-allowed' } : {}),
8019
8148
  // The middle input clears its own outline (removeInputPlatformStyling),
@@ -8061,8 +8190,30 @@ class UniQuantityStepperComponent extends BaseComponent {
8061
8190
  ...this.theme.focusRing(),
8062
8191
  });
8063
8192
  }, ...(ngDevMode ? [{ debugName: "buttonClass" }] : /* istanbul ignore next */ []));
8193
+ /**
8194
+ * Container chrome, defaulting to the shared `input` entry rather than to
8195
+ * hardcoded tokens. It is not a field, but it sits beside them in carts and
8196
+ * table rows, so a theme that restyles `input` must carry it along — the
8197
+ * options below stay as per-component overrides for a deliberately different
8198
+ * look.
8199
+ */
8200
+ containerColor = computed(() => this.componentOptions().color ?? this.fieldChrome().color, ...(ngDevMode ? [{ debugName: "containerColor" }] : /* istanbul ignore next */ []));
8201
+ containerBorder = computed(() => this.componentOptions().border ?? this.fieldChrome().border, ...(ngDevMode ? [{ debugName: "containerBorder" }] : /* istanbul ignore next */ []));
8202
+ containerRadius = computed(() => this.componentOptions().borderRadius ?? this.fieldChrome().borderRadius, ...(ngDevMode ? [{ debugName: "containerRadius" }] : /* istanbul ignore next */ []));
8064
8203
  /** The rules either side of the value, matching the frame around it. */
8065
- dividerBorder = computed(() => this.componentOptions().border ?? 'light', ...(ngDevMode ? [{ debugName: "dividerBorder" }] : /* istanbul ignore next */ []));
8204
+ dividerBorder = computed(() => this.containerBorder(), ...(ngDevMode ? [{ debugName: "dividerBorder" }] : /* istanbul ignore next */ []));
8205
+ /**
8206
+ * Characters the value cell asks the browser to size itself for.
8207
+ *
8208
+ * Load-bearing: a bare `<input>` defaults to `size="20"`, and `flex-basis:
8209
+ * auto` resolves to that intrinsic width — so the control claimed ~230px
8210
+ * instead of the ~100px its buttons and `valueWidth` need, and stole track
8211
+ * width from anything beside it in a grid (`1fr` is `minmax(auto, 1fr)`, and
8212
+ * the `auto` floor includes this). Tracking the content keeps the cell honest
8213
+ * while still letting it grow with the digits, which a fixed `width` would
8214
+ * not. `valueWidth` remains the floor, via `min-width`.
8215
+ */
8216
+ valueSize = computed(() => Math.max(this.displayText().length, 1), ...(ngDevMode ? [{ debugName: "valueSize" }] : /* istanbul ignore next */ []));
8066
8217
  valueBase() {
8067
8218
  const options = this.componentOptions();
8068
8219
  const colors = this.theme.colors();
@@ -8095,11 +8246,11 @@ class UniQuantityStepperComponent extends BaseComponent {
8095
8246
  readoutClass = computed(() => css([this.valueBase(), { display: 'grid', placeItems: 'center' }]), ...(ngDevMode ? [{ debugName: "readoutClass" }] : /* istanbul ignore next */ []));
8096
8247
  glyphSize = computed(() => Math.max(12, Math.round(this.height() / 2)), ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
8097
8248
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniQuantityStepperComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
8098
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniQuantityStepperComponent, isStandalone: true, selector: "uni-quantity-stepper, QuantityStepper", 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 }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, deleteAtMin: { classPropertyName: "deleteAtMin", publicName: "deleteAtMin", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", removed: "removed" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'quantityStepper' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- With an editable middle the input is the tab stop and the buttons are\n pointer affordances (tabindex=\"-1\"), exactly as in uni-number-input. With a\n read-only middle there is nothing else to focus, so the buttons become the\n tab stops and the group carries the name. -->\n<div\n [class]=\"rootClass()\"\n [attr.role]=\"editable() ? null : 'group'\"\n [attr.aria-label]=\"editable() ? null : label()\"\n>\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || (atMin() && !showDelete())\"\n [attr.aria-label]=\"decrementLabel()\"\n (pointerdown)=\"onDecrementPress($event)\"\n (pointerup)=\"decrement.release()\"\n (pointercancel)=\"decrement.cancel()\"\n (lostpointercapture)=\"decrement.release()\"\n (click)=\"onDecrementClick()\"\n >\n <uni-icon [name]=\"decrementIcon()\" [size]=\"glyphSize()\" />\n </button>\n\n @if (editable()) {\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n inputmode=\"numeric\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-valuenow]=\"value() ?? null\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuetext]=\"displayText() || 'Empty'\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n } @else {\n <!-- Text, not a control: no role and no tab stop, so a reader reads it as\n the group's content. -->\n <span [class]=\"readoutClass()\">{{ displayText() }}</span>\n }\n\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || atMax()\"\n [attr.aria-label]=\"'Increase ' + label()\"\n (pointerdown)=\"increment.press($event)\"\n (pointerup)=\"increment.release()\"\n (pointercancel)=\"increment.cancel()\"\n (lostpointercapture)=\"increment.release()\"\n >\n <uni-icon [name]=\"componentOptions().incrementIcon ?? 'plus'\" [size]=\"glyphSize()\" />\n </button>\n</div>\n\n@if (editable()) {\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Use the up and down arrow keys to change the quantity.\n </span>\n}\n\n<!-- Fences, clamps and removals are otherwise silent. Held stepping announces\n once, on release. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n", dependencies: [{ kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8249
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniQuantityStepperComponent, isStandalone: true, selector: "uni-quantity-stepper, QuantityStepper", 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 }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, deleteAtMin: { classPropertyName: "deleteAtMin", publicName: "deleteAtMin", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", removed: "removed" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'quantityStepper' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- With an editable middle the input is the tab stop and the buttons are\n pointer affordances (tabindex=\"-1\"), exactly as in uni-number-input. With a\n read-only middle there is nothing else to focus, so the buttons become the\n tab stops and the group carries the name. -->\n<div\n [class]=\"rootClass()\"\n [attr.role]=\"editable() ? null : 'group'\"\n [attr.aria-label]=\"editable() ? null : label()\"\n>\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || (atMin() && !showDelete())\"\n [attr.aria-label]=\"decrementLabel()\"\n (pointerdown)=\"onDecrementPress($event)\"\n (pointerup)=\"decrement.release()\"\n (pointercancel)=\"decrement.cancel()\"\n (lostpointercapture)=\"decrement.release()\"\n (click)=\"onDecrementClick()\"\n >\n <uni-icon [name]=\"decrementIcon()\" [size]=\"glyphSize()\" />\n </button>\n\n @if (editable()) {\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n inputmode=\"numeric\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [attr.size]=\"valueSize()\"\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-valuenow]=\"value() ?? null\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuetext]=\"displayText() || 'Empty'\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n } @else {\n <!-- Text, not a control: no role and no tab stop, so a reader reads it as\n the group's content. -->\n <span [class]=\"readoutClass()\">{{ displayText() }}</span>\n }\n\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || atMax()\"\n [attr.aria-label]=\"'Increase ' + label()\"\n (pointerdown)=\"increment.press($event)\"\n (pointerup)=\"increment.release()\"\n (pointercancel)=\"increment.cancel()\"\n (lostpointercapture)=\"increment.release()\"\n >\n <uni-icon [name]=\"componentOptions().incrementIcon ?? 'plus'\" [size]=\"glyphSize()\" />\n </button>\n</div>\n\n@if (editable()) {\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Use the up and down arrow keys to change the quantity.\n </span>\n}\n\n<!-- Fences, clamps and removals are otherwise silent. Held stepping announces\n once, on release. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n", dependencies: [{ kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8099
8250
  }
8100
8251
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniQuantityStepperComponent, decorators: [{
8101
8252
  type: Component,
8102
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-quantity-stepper, QuantityStepper', imports: [UniIconComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'quantityStepper' }], host: { '[class]': 'className()' }, template: "<!-- With an editable middle the input is the tab stop and the buttons are\n pointer affordances (tabindex=\"-1\"), exactly as in uni-number-input. With a\n read-only middle there is nothing else to focus, so the buttons become the\n tab stops and the group carries the name. -->\n<div\n [class]=\"rootClass()\"\n [attr.role]=\"editable() ? null : 'group'\"\n [attr.aria-label]=\"editable() ? null : label()\"\n>\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || (atMin() && !showDelete())\"\n [attr.aria-label]=\"decrementLabel()\"\n (pointerdown)=\"onDecrementPress($event)\"\n (pointerup)=\"decrement.release()\"\n (pointercancel)=\"decrement.cancel()\"\n (lostpointercapture)=\"decrement.release()\"\n (click)=\"onDecrementClick()\"\n >\n <uni-icon [name]=\"decrementIcon()\" [size]=\"glyphSize()\" />\n </button>\n\n @if (editable()) {\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n inputmode=\"numeric\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-valuenow]=\"value() ?? null\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuetext]=\"displayText() || 'Empty'\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n } @else {\n <!-- Text, not a control: no role and no tab stop, so a reader reads it as\n the group's content. -->\n <span [class]=\"readoutClass()\">{{ displayText() }}</span>\n }\n\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || atMax()\"\n [attr.aria-label]=\"'Increase ' + label()\"\n (pointerdown)=\"increment.press($event)\"\n (pointerup)=\"increment.release()\"\n (pointercancel)=\"increment.cancel()\"\n (lostpointercapture)=\"increment.release()\"\n >\n <uni-icon [name]=\"componentOptions().incrementIcon ?? 'plus'\" [size]=\"glyphSize()\" />\n </button>\n</div>\n\n@if (editable()) {\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Use the up and down arrow keys to change the quantity.\n </span>\n}\n\n<!-- Fences, clamps and removals are otherwise silent. Held stepping announces\n once, on release. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n" }]
8253
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-quantity-stepper, QuantityStepper', imports: [UniIconComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'quantityStepper' }], host: { '[class]': 'className()' }, template: "<!-- With an editable middle the input is the tab stop and the buttons are\n pointer affordances (tabindex=\"-1\"), exactly as in uni-number-input. With a\n read-only middle there is nothing else to focus, so the buttons become the\n tab stops and the group carries the name. -->\n<div\n [class]=\"rootClass()\"\n [attr.role]=\"editable() ? null : 'group'\"\n [attr.aria-label]=\"editable() ? null : label()\"\n>\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || (atMin() && !showDelete())\"\n [attr.aria-label]=\"decrementLabel()\"\n (pointerdown)=\"onDecrementPress($event)\"\n (pointerup)=\"decrement.release()\"\n (pointercancel)=\"decrement.cancel()\"\n (lostpointercapture)=\"decrement.release()\"\n (click)=\"onDecrementClick()\"\n >\n <uni-icon [name]=\"decrementIcon()\" [size]=\"glyphSize()\" />\n </button>\n\n @if (editable()) {\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n inputmode=\"numeric\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [attr.size]=\"valueSize()\"\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-valuenow]=\"value() ?? null\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuetext]=\"displayText() || 'Empty'\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n />\n } @else {\n <!-- Text, not a control: no role and no tab stop, so a reader reads it as\n the group's content. -->\n <span [class]=\"readoutClass()\">{{ displayText() }}</span>\n }\n\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [attr.tabindex]=\"editable() ? -1 : null\"\n [disabled]=\"disabled() || atMax()\"\n [attr.aria-label]=\"'Increase ' + label()\"\n (pointerdown)=\"increment.press($event)\"\n (pointerup)=\"increment.release()\"\n (pointercancel)=\"increment.cancel()\"\n (lostpointercapture)=\"increment.release()\"\n >\n <uni-icon [name]=\"componentOptions().incrementIcon ?? 'plus'\" [size]=\"glyphSize()\" />\n </button>\n</div>\n\n@if (editable()) {\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Use the up and down arrow keys to change the quantity.\n </span>\n}\n\n<!-- Fences, clamps and removals are otherwise silent. Held stepping announces\n once, on release. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n" }]
8103
8254
  }], 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 }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], deleteAtMin: [{ type: i0.Input, args: [{ isSignal: true, alias: "deleteAtMin", required: false }] }], removed: [{ type: i0.Output, args: ["removed"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
8104
8255
 
8105
8256
  /**
@@ -8667,7 +8818,7 @@ class UniTextareaComponent {
8667
8818
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
8668
8819
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
8669
8820
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
8670
- /** Synced from required() validators by the Signal Forms [field] directive. */
8821
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
8671
8822
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
8672
8823
  /**
8673
8824
  * Id(s) of external element(s) describing this control — typically your
@@ -8679,7 +8830,7 @@ class UniTextareaComponent {
8679
8830
  placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
8680
8831
  /** Visible text rows. Defaults to the theme's `textarea` options. */
8681
8832
  rows = input(undefined, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
8682
- // Signal Forms' own optional control inputs: the `[field]` directive syncs
8833
+ // Signal Forms' own optional control inputs: the `[formField]` directive syncs
8683
8834
  // them from the field's validators, as it does `required`.
8684
8835
  readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
8685
8836
  name = input('', ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
@@ -8714,6 +8865,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
8714
8865
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-textarea', imports: [UniInputBoxComponent], template: "<uni-input-box [error]=\"showError()\" height=\"auto\" [width]=\"width()\" [fullWidth]=\"fullWidth()\" [grow]=\"grow()\">\n <textarea\n [value]=\"value()\"\n (input)=\"handleInput($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (blur)=\"markAsTouched()\"\n [rows]=\"resolvedRows()\"\n [class]=\"textareaClass()\"\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.minlength]=\"minLength() ?? null\"\n [attr.maxlength]=\"maxLength() ?? null\"\n [attr.spellcheck]=\"spellcheck() ?? null\"\n [attr.aria-label]=\"label()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n ></textarea>\n</uni-input-box>\n" }]
8715
8866
  }], 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 }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], minLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minLength", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], spellcheck: [{ type: i0.Input, args: [{ isSignal: true, alias: "spellcheck", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], grow: [{ type: i0.Input, args: [{ isSignal: true, alias: "grow", required: false }] }] } });
8716
8867
 
8868
+ /**
8869
+ * Everything a switch needs, from the three numbers a theme actually states.
8870
+ *
8871
+ * `travel` is why this is derived rather than written down: the knob starts at
8872
+ * `inset` and must end the same distance from the far edge, so it moves
8873
+ * `width - inset - knob - inset`, which reduces to `width - height`. The old
8874
+ * code hardcoded a translate of one track height, which was correct only while
8875
+ * the width was locked at 2x and the knob at 0.8x.
8876
+ */
8877
+ function geometry(width, height, inset) {
8878
+ return { width, height, inset, knob: height - inset * 2, travel: width - height, radius: height / 2 };
8879
+ }
8717
8880
  class UniToggleComponent extends BaseComponent {
8718
8881
  // --- REQUIRED SIGNALS (populated by FormCheckboxControl) ---
8719
8882
  checked = model(false, ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
@@ -8721,7 +8884,7 @@ class UniToggleComponent extends BaseComponent {
8721
8884
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
8722
8885
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
8723
8886
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
8724
- /** Synced from required() validators by the Signal Forms [field] directive. */
8887
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
8725
8888
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
8726
8889
  /**
8727
8890
  * Id(s) of external element(s) describing this control — typically your
@@ -8730,6 +8893,16 @@ class UniToggleComponent extends BaseComponent {
8730
8893
  ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
8731
8894
  // --- CONFIGURATION ---
8732
8895
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
8896
+ /**
8897
+ * Checked-state track color token, overriding the theme's
8898
+ * `toggle.behavior.checkedColor`.
8899
+ *
8900
+ * This exists alongside the theme option because `variant` — where this color
8901
+ * used to live exclusively — defaults to `'primary'`, so the component cannot
8902
+ * tell "set to primary" from "not set". Without an input, a theme-level
8903
+ * `checkedColor` would silently make per-instance `variant` inert.
8904
+ */
8905
+ checkedColor = input(...(ngDevMode ? [undefined, { debugName: "checkedColor" }] : /* istanbul ignore next */ []));
8733
8906
  // Only show errors if the user has actually interacted with the field
8734
8907
  showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
8735
8908
  markAsTouched() {
@@ -8739,18 +8912,56 @@ class UniToggleComponent extends BaseComponent {
8739
8912
  this.checked.set(event.target.checked);
8740
8913
  this.markAsTouched();
8741
8914
  }
8915
+ /**
8916
+ * Track and knob geometry for the active `size`, read out of the theme's
8917
+ * `sizes` block as data — `width`, `height` and the knob's inset `padding`.
8918
+ *
8919
+ * Read rather than spread: `padding` must not reach the track as real CSS or
8920
+ * it would double up with the knob's own `top`/`left` offsets. `uni-calendar`
8921
+ * treats its size block the same way.
8922
+ */
8742
8923
  metrics = computed(() => {
8743
- const toggleSize = this.componentOptions().size || 20;
8744
- const sliderSize = toggleSize * 0.8;
8924
+ // The legacy single-number token wins when a theme still sets it: that
8925
+ // theme opted into the old derived-ratio geometry before `sizes` existed,
8926
+ // and it applies to every instance regardless of the `size` input.
8927
+ const legacy = this.componentOptions().size;
8928
+ if (legacy != null) {
8929
+ const height = Number(legacy);
8930
+ return geometry(height * 2, height, (height - height * 0.8) / 2);
8931
+ }
8932
+ const size = this.style();
8933
+ const height = Number(size['height'] ?? 20);
8934
+ const width = Number(size['width'] ?? height * 2);
8935
+ const inset = Number(size['padding'] ?? (height - height * 0.8) / 2);
8936
+ return geometry(width, height, inset);
8937
+ }, ...(ngDevMode ? [{ debugName: "metrics" }] : /* istanbul ignore next */ []));
8938
+ /**
8939
+ * The resolved checked/accent color: the per-instance input, then the
8940
+ * variant's themed accent, then the theme option.
8941
+ *
8942
+ * The last link used to be `this.variant()` — the variant *name* resolved as
8943
+ * a colour token. That only ever worked because every variant happened to
8944
+ * also be a colour; with the registry open, `variant="destructive"` would
8945
+ * have missed and silently rendered primary. `primary` is the last resort
8946
+ * because it is a reserved variant name.
8947
+ */
8948
+ accent = computed(() => this.checkedColor() ??
8949
+ this.variantRoles()?.accent ??
8950
+ this.componentOptions().checkedColor ??
8951
+ 'primary', ...(ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []));
8952
+ /** Knob slide and track color change, as a motion token — never `all`. */
8953
+ transitions = computed(() => {
8954
+ const motion = this.theme.motion(this.componentOptions().motion ?? 'control');
8955
+ const speed = motion.duration / 1000;
8745
8956
  return {
8746
- toggleSize,
8747
- toggleWidth: toggleSize * 2,
8748
- sliderSize,
8749
- sliderOffset: (toggleSize - sliderSize) / 2,
8957
+ // Scoped, never `all`: the focus ring must apply instantly rather than
8958
+ // interpolating its outline color from a stale value.
8959
+ track: `background-color ${speed}s ${motion.easing}, border-color ${speed}s ${motion.easing}`,
8960
+ knob: `transform ${speed}s ${motion.easing}, background-color ${speed}s ${motion.easing}`,
8750
8961
  };
8751
- }, ...(ngDevMode ? [{ debugName: "metrics" }] : /* istanbul ignore next */ []));
8962
+ }, ...(ngDevMode ? [{ debugName: "transitions" }] : /* istanbul ignore next */ []));
8752
8963
  toggleLabel = computed(() => {
8753
- const { toggleSize, toggleWidth, sliderSize, sliderOffset } = this.metrics();
8964
+ const { height, width, knob, inset, radius } = this.metrics();
8754
8965
  return css({
8755
8966
  userSelect: 'none',
8756
8967
  cursor: this.disabled() ? 'not-allowed' : 'pointer',
@@ -8760,26 +8971,24 @@ class UniToggleComponent extends BaseComponent {
8760
8971
  gap: 8,
8761
8972
  opacity: this.disabled() ? 0.6 : 1,
8762
8973
  '& .toggle-switch': {
8763
- width: toggleWidth,
8764
- height: toggleSize,
8974
+ width,
8975
+ height,
8765
8976
  backgroundColor: this.disabled()
8766
- ? this.getThemeColor('disabled')
8767
- : this.getThemeColor(this.componentOptions().trackColor ?? 'surface-variant'),
8768
- borderRadius: toggleSize / 2,
8977
+ ? this.color('disabled')
8978
+ : this.color(this.componentOptions().trackColor ?? 'surface-variant'),
8979
+ borderRadius: radius,
8769
8980
  position: 'relative',
8770
- // Scoped, never `all`: the focus ring must apply instantly rather
8771
- // than interpolating its outline color from a stale value.
8772
- transition: 'background-color 0.3s ease, border-color 0.3s ease',
8981
+ transition: this.transitions().track,
8773
8982
  },
8774
8983
  '& .toggle-slider': {
8775
- width: sliderSize,
8776
- height: sliderSize,
8777
- backgroundColor: this.getThemeColor(this.componentOptions().knobColor ?? 'surface'),
8984
+ width: knob,
8985
+ height: knob,
8986
+ backgroundColor: this.color(this.componentOptions().knobColor ?? 'surface'),
8778
8987
  borderRadius: '50%',
8779
8988
  position: 'absolute',
8780
- top: sliderOffset,
8781
- left: sliderOffset,
8782
- transition: 'transform 0.3s ease, background-color 0.3s ease',
8989
+ top: inset,
8990
+ left: inset,
8991
+ transition: this.transitions().knob,
8783
8992
  ...this.theme.boxShadow('raised'),
8784
8993
  },
8785
8994
  // Hover darkens whatever the token resolves to — the button convention.
@@ -8791,7 +9000,8 @@ class UniToggleComponent extends BaseComponent {
8791
9000
  });
8792
9001
  }, ...(ngDevMode ? [{ debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
8793
9002
  toggleInput = computed(() => {
8794
- const { toggleSize } = this.metrics();
9003
+ const { travel } = this.metrics();
9004
+ const accent = this.color(this.accent());
8795
9005
  return css({
8796
9006
  position: 'absolute',
8797
9007
  zIndex: -1,
@@ -8799,32 +9009,34 @@ class UniToggleComponent extends BaseComponent {
8799
9009
  height: 0,
8800
9010
  opacity: 0,
8801
9011
  '&:checked + .toggle-switch': {
8802
- backgroundColor: this.getThemeColor(this.variant()),
8803
- borderColor: this.getThemeColor(this.variant()),
9012
+ backgroundColor: accent,
9013
+ borderColor: accent,
8804
9014
  },
8805
9015
  '&:checked + .toggle-switch .toggle-slider': {
8806
- transform: `translateX(${toggleSize}px)`,
9016
+ transform: `translateX(${travel}px)`,
8807
9017
  },
8808
9018
  '&:disabled + .toggle-switch': {
8809
9019
  cursor: 'not-allowed',
8810
9020
  },
8811
- // The shared, themable focus indicator, keyed off the hidden input.
9021
+ // The shared, themable focus indicator, keyed off the hidden input. It
9022
+ // wears the checked color rather than the variant, so a themed on-state
9023
+ // is not paired with a ring in some other role's color.
8812
9024
  '&:focus + .toggle-switch': {
8813
- ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
9025
+ ...this.theme.focusRingStyle(accent),
8814
9026
  },
8815
9027
  });
8816
9028
  }, ...(ngDevMode ? [{ debugName: "toggleInput" }] : /* istanbul ignore next */ []));
8817
- getThemeColor(token) {
8818
- const colors = this.theme.colors();
8819
- return colors[token] ? colors[token] : colors['primary'];
9029
+ /** A chrome colour by token — no silent fallback; see `uni-radio`. */
9030
+ color(token) {
9031
+ return this.theme.colors()[token];
8820
9032
  }
8821
9033
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniToggleComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
8822
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniToggleComponent, isStandalone: true, selector: "uni-toggle", inputs: { checked: { classPropertyName: "checked", publicName: "checked", 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: false, transformFunction: null } }, outputs: { checked: "checkedChange", touched: "touchedChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'toggle' }], usesInheritance: true, ngImport: i0, 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", dependencies: [{ kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9034
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniToggleComponent, isStandalone: true, selector: "uni-toggle", inputs: { checked: { classPropertyName: "checked", publicName: "checked", 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: false, transformFunction: null }, checkedColor: { classPropertyName: "checkedColor", publicName: "checkedColor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", touched: "touchedChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'toggle' }], usesInheritance: true, ngImport: i0, 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", dependencies: [{ kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8823
9035
  }
8824
9036
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniToggleComponent, decorators: [{
8825
9037
  type: Component,
8826
9038
  args: [{ selector: 'uni-toggle', imports: [UniTextDirective], 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" }]
8827
- }], 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 }] }] } });
9039
+ }], 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 }] }], checkedColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "checkedColor", required: false }] }] } });
8828
9040
 
8829
9041
  /**
8830
9042
  * Every layout and typography attribute directive, for
@@ -10461,8 +10673,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
10461
10673
  args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: '[uni-dialog-header]', imports: [UniBoxDirective, UniIconButtonComponent, UniTextDirective, UniRowDirective], providers: [{ provide: COMPONENT_NAME, useValue: 'dialogHeader' }], template: "<div\n row-layout\n [containerColor]=\"componentOptions().color\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [height]=\"componentOptions().height\"\n alignItems=\"center\"\n [paddingHorizontal]=\"componentOptions().paddingHorizontal ?? 'sm'\"\n>\n <!-- Balance spacer: only centered titles need to offset the close button. -->\n @if ((componentOptions().textAlign || 'center') === 'center') {\n <div box-layout [width]=\"26\"></div>\n }\n <div box-layout [grow]=\"1\" [attr.id]=\"titleId\">\n <span uni-text\n [typeface]=\"componentOptions().textRole\"\n [color]=\"componentOptions().textColor\"\n display=\"block\"\n [align]=\"componentOptions().textAlign || 'center'\"\n ><ng-content></ng-content\n ></span>\n </div>\n <button\n icon-button\n [iconName]=\"componentOptions().closeButtonIcon\"\n [symbolName]=\"componentOptions().closeButtonSymbol\"\n variant=\"ghost\"\n (click)=\"closeDialog()\"\n [size]=\"componentOptions().closeButtonSize || 'md'\"\n >\n Close\n </button>\n</div>\n" }]
10462
10674
  }], ctorParameters: () => [] });
10463
10675
 
10676
+ const DRAWER_PANEL = new InjectionToken('uni-drawer-panel');
10677
+
10464
10678
  /**
10465
- * Navigation drawer with two modes sharing one content slot:
10679
+ * The drawer's pinned footer action row the save bar of an editor panel.
10680
+ *
10681
+ * Sits outside the scrolling body, so the actions stay reachable however long
10682
+ * the form is. Mirrors `[dialog-buttons]`; the difference is posture, which
10683
+ * lives in the `drawerButtons` theme options rather than here.
10684
+ */
10685
+ class UniDrawerButtonsComponent extends BaseComponent {
10686
+ drawer = inject(DRAWER_PANEL, { optional: true });
10687
+ confirmButtonText = input(...(ngDevMode ? [undefined, { debugName: "confirmButtonText" }] : /* istanbul ignore next */ []));
10688
+ confirmButtonVariant = input(...(ngDevMode ? [undefined, { debugName: "confirmButtonVariant" }] : /* istanbul ignore next */ []));
10689
+ cancelButtonText = input(...(ngDevMode ? [undefined, { debugName: "cancelButtonText" }] : /* istanbul ignore next */ []));
10690
+ cancelButtonVariant = input(...(ngDevMode ? [undefined, { debugName: "cancelButtonVariant" }] : /* istanbul ignore next */ []));
10691
+ disableConfirm = input(...(ngDevMode ? [undefined, { debugName: "disableConfirm" }] : /* istanbul ignore next */ []));
10692
+ padding = input(...(ngDevMode ? [undefined, { debugName: "padding" }] : /* istanbul ignore next */ []));
10693
+ justifyContent = input(...(ngDevMode ? [undefined, { debugName: "justifyContent" }] : /* istanbul ignore next */ []));
10694
+ confirmed = output();
10695
+ // Inputs win over theme options; the trailing literal is the fallback.
10696
+ confirmVariant = computed(() => this.confirmButtonVariant() ?? this.componentOptions().confirmButtonVariant ?? 'primary', ...(ngDevMode ? [{ debugName: "confirmVariant" }] : /* istanbul ignore next */ []));
10697
+ cancelVariant = computed(() => this.cancelButtonVariant() ?? this.componentOptions().cancelButtonVariant ?? 'quaternary', ...(ngDevMode ? [{ debugName: "cancelVariant" }] : /* istanbul ignore next */ []));
10698
+ paddingValue = computed(() => this.padding() ?? this.componentOptions().padding ?? 'md', ...(ngDevMode ? [{ debugName: "paddingValue" }] : /* istanbul ignore next */ []));
10699
+ justifyContentValue = computed(() => this.justifyContent() ?? this.componentOptions().justifyContent ?? 'flex-end', ...(ngDevMode ? [{ debugName: "justifyContentValue" }] : /* istanbul ignore next */ []));
10700
+ gapValue = computed(() => this.componentOptions().gap ?? 'sm', ...(ngDevMode ? [{ debugName: "gapValue" }] : /* istanbul ignore next */ []));
10701
+ buttonSize = computed(() => this.componentOptions().buttonSize ?? 'md', ...(ngDevMode ? [{ debugName: "buttonSize" }] : /* istanbul ignore next */ []));
10702
+ /** A pinned row, sized by its content rather than by the body beside it. */
10703
+ hostClass = computed(() => css({
10704
+ flex: 'none',
10705
+ ...this.theme.borderTop(this.componentOptions().divider),
10706
+ }), ...(ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []));
10707
+ className = computed(() => css([
10708
+ this.componentTheme().fixed,
10709
+ this.componentOptions().stretch && {
10710
+ width: '100%',
10711
+ minWidth: '100%',
10712
+ '& > button': { flex: '1 1 50%', maxWidth: '50%' },
10713
+ },
10714
+ ]), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
10715
+ /**
10716
+ * Cancel routes through the drawer's own close decision, so a panel with
10717
+ * unsaved changes can veto it exactly as it vetoes Escape.
10718
+ */
10719
+ closeDrawer() {
10720
+ this.drawer?.requestClose('close-button');
10721
+ }
10722
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerButtonsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
10723
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDrawerButtonsComponent, isStandalone: true, selector: "[uni-drawer-buttons], [drawer-buttons]", inputs: { confirmButtonText: { classPropertyName: "confirmButtonText", publicName: "confirmButtonText", isSignal: true, isRequired: false, transformFunction: null }, confirmButtonVariant: { classPropertyName: "confirmButtonVariant", publicName: "confirmButtonVariant", isSignal: true, isRequired: false, transformFunction: null }, cancelButtonText: { classPropertyName: "cancelButtonText", publicName: "cancelButtonText", isSignal: true, isRequired: false, transformFunction: null }, cancelButtonVariant: { classPropertyName: "cancelButtonVariant", publicName: "cancelButtonVariant", isSignal: true, isRequired: false, transformFunction: null }, disableConfirm: { classPropertyName: "disableConfirm", publicName: "disableConfirm", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, justifyContent: { classPropertyName: "justifyContent", publicName: "justifyContent", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { confirmed: "confirmed" }, host: { properties: { "class": "hostClass()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'drawerButtons' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n [gap]=\"gapValue()\"\n [padding]=\"paddingValue()\"\n [justifyContent]=\"justifyContentValue()\"\n [flexDirection]=\"componentOptions().reverseOrder ? 'row-reverse' : 'row'\"\n [class]=\"className()\"\n>\n <button text-button [variant]=\"cancelVariant()\" [size]=\"buttonSize()\" (click)=\"closeDrawer()\">\n {{ cancelButtonText() || 'Cancel' }}\n </button>\n <button\n text-button\n [variant]=\"confirmVariant()\"\n [size]=\"buttonSize()\"\n (click)=\"confirmed.emit()\"\n [disable]=\"disableConfirm()\"\n >\n {{ confirmButtonText() || 'Save' }}\n </button>\n</div>\n", dependencies: [{ kind: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10724
+ }
10725
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerButtonsComponent, decorators: [{
10726
+ type: Component,
10727
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: '[uni-drawer-buttons], [drawer-buttons]', imports: [UniRowDirective, UniButtonComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'drawerButtons' }], host: { '[class]': 'hostClass()' }, template: "<div\n row-layout\n [gap]=\"gapValue()\"\n [padding]=\"paddingValue()\"\n [justifyContent]=\"justifyContentValue()\"\n [flexDirection]=\"componentOptions().reverseOrder ? 'row-reverse' : 'row'\"\n [class]=\"className()\"\n>\n <button text-button [variant]=\"cancelVariant()\" [size]=\"buttonSize()\" (click)=\"closeDrawer()\">\n {{ cancelButtonText() || 'Cancel' }}\n </button>\n <button\n text-button\n [variant]=\"confirmVariant()\"\n [size]=\"buttonSize()\"\n (click)=\"confirmed.emit()\"\n [disable]=\"disableConfirm()\"\n >\n {{ confirmButtonText() || 'Save' }}\n </button>\n</div>\n" }]
10728
+ }], propDecorators: { confirmButtonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmButtonText", required: false }] }], confirmButtonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "confirmButtonVariant", required: false }] }], cancelButtonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelButtonText", required: false }] }], cancelButtonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelButtonVariant", required: false }] }], disableConfirm: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableConfirm", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], justifyContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "justifyContent", required: false }] }], confirmed: [{ type: i0.Output, args: ["confirmed"] }] } });
10729
+
10730
+ /**
10731
+ * The drawer's pinned header row: a title, and optionally a close button.
10732
+ *
10733
+ * Sits outside the scrolling body, so it stays put while the form beneath it
10734
+ * moves. Reached either by projecting it — `<div uni-drawer-header>` — or
10735
+ * implicitly, by giving `uni-drawer` a `headline`, in which case the drawer
10736
+ * renders one of these itself.
10737
+ */
10738
+ class UniDrawerHeaderComponent extends BaseComponent {
10739
+ drawer = inject(DRAWER_PANEL, { optional: true });
10740
+ /** Title text. Falls back to the drawer's `headline`; projected content wins over both. */
10741
+ headline = input(...(ngDevMode ? [undefined, { debugName: "headline" }] : /* istanbul ignore next */ []));
10742
+ /** Attached to the title so the drawer is labelled by it. */
10743
+ titleId = this.drawer?.titleId ?? null;
10744
+ title = computed(() => this.headline() ?? this.drawer?.headline() ?? '', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
10745
+ showClose = computed(() => this.drawer?.defaultCloseButton() ?? true, ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
10746
+ constructor() {
10747
+ super();
10748
+ // Tells the drawer it is labelled by this row rather than by `ariaLabel`.
10749
+ this.drawer?.hasHeader.set(true);
10750
+ }
10751
+ /** Never a bare close: the drawer decides, so a veto is honoured here too. */
10752
+ closeDrawer() {
10753
+ this.drawer?.requestClose('close-button');
10754
+ }
10755
+ className = computed(() => css({
10756
+ // A pinned row: it is a flex child of the panel and must not be sized
10757
+ // by the scrolling body beside it.
10758
+ flex: 'none',
10759
+ ...this.theme.borderBottom(this.componentOptions().divider),
10760
+ }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
10761
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10762
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDrawerHeaderComponent, isStandalone: true, selector: "[uni-drawer-header]", inputs: { headline: { classPropertyName: "headline", publicName: "headline", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'drawerHeader' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n [containerColor]=\"componentOptions().color\"\n [height]=\"componentOptions().height\"\n alignItems=\"center\"\n gap=\"sm\"\n [padding]=\"componentOptions().padding ?? 'md'\"\n>\n <div box-layout [grow]=\"1\" [minWidth]=\"0\" [attr.id]=\"titleId\">\n <span\n uni-text\n [typeface]=\"componentOptions().textRole\"\n [color]=\"componentOptions().textColor\"\n display=\"block\"\n [align]=\"componentOptions().textAlign || 'left'\"\n ><ng-content>{{ title() }}</ng-content></span\n >\n </div>\n @if (showClose()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeButtonIcon\"\n [symbolName]=\"componentOptions().closeButtonSymbol\"\n variant=\"ghost\"\n (click)=\"closeDrawer()\"\n [size]=\"componentOptions().closeButtonSize || 'md'\"\n >\n Close\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "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: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10763
+ }
10764
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerHeaderComponent, decorators: [{
10765
+ type: Component,
10766
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: '[uni-drawer-header]', imports: [UniBoxDirective, UniIconButtonComponent, UniTextDirective, UniRowDirective], providers: [{ provide: COMPONENT_NAME, useValue: 'drawerHeader' }], host: { '[class]': 'className()' }, template: "<div\n row-layout\n [containerColor]=\"componentOptions().color\"\n [height]=\"componentOptions().height\"\n alignItems=\"center\"\n gap=\"sm\"\n [padding]=\"componentOptions().padding ?? 'md'\"\n>\n <div box-layout [grow]=\"1\" [minWidth]=\"0\" [attr.id]=\"titleId\">\n <span\n uni-text\n [typeface]=\"componentOptions().textRole\"\n [color]=\"componentOptions().textColor\"\n display=\"block\"\n [align]=\"componentOptions().textAlign || 'left'\"\n ><ng-content>{{ title() }}</ng-content></span\n >\n </div>\n @if (showClose()) {\n <button\n icon-button\n [iconName]=\"componentOptions().closeButtonIcon\"\n [symbolName]=\"componentOptions().closeButtonSymbol\"\n variant=\"ghost\"\n (click)=\"closeDrawer()\"\n [size]=\"componentOptions().closeButtonSize || 'md'\"\n >\n Close\n </button>\n }\n</div>\n" }]
10767
+ }], ctorParameters: () => [], propDecorators: { headline: [{ type: i0.Input, args: [{ isSignal: true, alias: "headline", required: false }] }] } });
10768
+
10769
+ /**
10770
+ * Drawer with two modes sharing one three-row layout:
10466
10771
  *
10467
10772
  * - `side` — an in-flow `<aside>` that pushes content (dashboard sidenav);
10468
10773
  * opening/closing animates its width, and the divider border primitive
@@ -10471,17 +10776,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
10471
10776
  * scrim backdrop come from the platform (same machinery as `uni-dialog`),
10472
10777
  * sliding in from its edge.
10473
10778
  *
10474
- * Surface, width, divider, elevation, padding and backdrop all resolve from
10475
- * `drawer` theme tokens.
10779
+ * **The panel is never the scroll container.** It is a flex column of three
10780
+ * rows — an optional `[uni-drawer-header]`, the projected body, an optional
10781
+ * `[uni-drawer-buttons]` — and only the body scrolls. The panel itself is
10782
+ * `overflow: clip` on both axes. That is what lets a header and a save bar pin
10783
+ * while a long form scrolls between them, and it is why the theme's `padding`
10784
+ * option lands on the body row rather than the panel: padding on a scrolling
10785
+ * box scrolls away with its content.
10786
+ *
10787
+ * Surface, width, divider, elevation, padding, backdrop, scrim and background
10788
+ * all resolve from `drawer` theme tokens.
10476
10789
  */
10477
10790
  class UniDrawerComponent extends BaseComponent {
10478
10791
  /** Two-way bindable open state: [(open)]. */
10479
10792
  open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
10480
10793
  mode = input('side', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
10481
10794
  position = input('start', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
10482
- /** Accessible name for the overlay mode's dialog. */
10483
- ariaLabel = input('Navigation', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
10484
- contentTemplate = viewChild.required('content');
10795
+ /**
10796
+ * Accessible name for the overlay mode. Only consulted when the drawer has
10797
+ * no header to be labelled by.
10798
+ *
10799
+ * There is deliberately no default. A drawer used as an editor panel that
10800
+ * inherited the literal "Navigation" would announce itself as something it
10801
+ * is not, and a wrong accessible name is worse than a missing one — the
10802
+ * missing one is at least caught by any audit.
10803
+ */
10804
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
10805
+ /**
10806
+ * Title for the drawer's header row. Shorthand for projecting a
10807
+ * `[uni-drawer-header]`; project one instead when the header needs more
10808
+ * than a title (a record counter, prev/next navigation).
10809
+ */
10810
+ headline = input(...(ngDevMode ? [undefined, { debugName: "headline" }] : /* istanbul ignore next */ []));
10811
+ /** Whether the header row renders a close button. */
10812
+ defaultCloseButton = input(true, ...(ngDevMode ? [{ debugName: "defaultCloseButton" }] : /* istanbul ignore next */ []));
10813
+ /** Panel width in px, overriding the theme's `drawer.behavior.width`. */
10814
+ width = input(...(ngDevMode ? [undefined, { debugName: "width" }] : /* istanbul ignore next */ []));
10815
+ /**
10816
+ * Whether the overlay dims the page behind it, overriding the theme's
10817
+ * `drawer.behavior.scrim`. False leaves the backdrop transparent so the page
10818
+ * stays legible while the panel is open — an editor panel beside a board the
10819
+ * user is still reading.
10820
+ *
10821
+ * This does not make the drawer non-modal: focus is still trapped and the
10822
+ * page behind is still inert. It is a visibility choice, not a modality one.
10823
+ */
10824
+ scrim = input(...(ngDevMode ? [undefined, { debugName: "scrim" }] : /* istanbul ignore next */ []));
10825
+ /**
10826
+ * CSS selector for the element to focus when the overlay opens. The native
10827
+ * default is the first focusable element, which in an editor panel is
10828
+ * usually the close button rather than the first field.
10829
+ */
10830
+ initialFocus = input(...(ngDevMode ? [undefined, { debugName: "initialFocus" }] : /* istanbul ignore next */ []));
10831
+ /**
10832
+ * The drawer is *asking* to close — Escape, the backdrop, or a close/cancel
10833
+ * button. Pair with `disableAutoClose` to hold the panel open while an async
10834
+ * confirmation runs.
10835
+ */
10836
+ closeRequest = output();
10837
+ /**
10838
+ * When true the drawer never closes itself; it only emits `closeRequest` and
10839
+ * waits for the consumer to set `open`. Off by default, so a drawer that
10840
+ * ignores `closeRequest` behaves exactly as it always has.
10841
+ */
10842
+ disableAutoClose = input(false, ...(ngDevMode ? [{ debugName: "disableAutoClose" }] : /* istanbul ignore next */ []));
10843
+ /** Set by a projected `[uni-drawer-header]` so it can pin flush to the top. */
10844
+ hasHeader = signal(false, ...(ngDevMode ? [{ debugName: "hasHeader" }] : /* istanbul ignore next */ []));
10845
+ /** Id referenced by aria-labelledby; the header row attaches it to its title. */
10846
+ titleId = uniqueId('uni-drawer-title');
10847
+ labelledBy = computed(() => (this.hasHeader() ? this.titleId : null), ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
10848
+ headerTemplate = viewChild.required('header');
10849
+ bodyTemplate = viewChild.required('body');
10850
+ footerTemplate = viewChild.required('footer');
10485
10851
  overlay = viewChild('overlay', ...(ngDevMode ? [{ debugName: "overlay" }] : /* istanbul ignore next */ []));
10486
10852
  constructor() {
10487
10853
  super();
@@ -10493,6 +10859,9 @@ class UniDrawerComponent extends BaseComponent {
10493
10859
  if (!dialog.open) {
10494
10860
  dialog.removeAttribute('closing');
10495
10861
  dialog.showModal();
10862
+ const selector = this.initialFocus();
10863
+ if (selector)
10864
+ dialog.querySelector(selector)?.focus();
10496
10865
  }
10497
10866
  }
10498
10867
  else if (dialog.open) {
@@ -10501,14 +10870,24 @@ class UniDrawerComponent extends BaseComponent {
10501
10870
  }
10502
10871
  });
10503
10872
  }
10873
+ /**
10874
+ * The one place a close is decided, so every route in — Escape, the
10875
+ * backdrop, the header's close button, the footer's cancel — behaves
10876
+ * identically and is equally vetoable.
10877
+ */
10878
+ requestClose(reason) {
10879
+ this.closeRequest.emit({ reason });
10880
+ if (!this.disableAutoClose())
10881
+ this.open.set(false);
10882
+ }
10504
10883
  onBackdropClick(event) {
10505
10884
  if (event.target.nodeName === 'DIALOG')
10506
- this.open.set(false);
10885
+ this.requestClose('backdrop');
10507
10886
  }
10508
10887
  /** Route Escape through the animated close, keeping `open` in sync. */
10509
10888
  onCancel(event) {
10510
10889
  event.preventDefault();
10511
- this.open.set(false);
10890
+ this.requestClose('escape');
10512
10891
  }
10513
10892
  onAnimationEnd(event) {
10514
10893
  const dialog = this.overlay()?.nativeElement;
@@ -10522,53 +10901,129 @@ class UniDrawerComponent extends BaseComponent {
10522
10901
  edge = computed(() => (this.position() === 'start' ? '-100%' : '100%'), ...(ngDevMode ? [{ debugName: "edge" }] : /* istanbul ignore next */ []));
10523
10902
  slideIn = computed(() => keyframes({ from: { transform: `translateX(${this.edge()})` }, to: { transform: 'translateX(0)' } }), ...(ngDevMode ? [{ debugName: "slideIn" }] : /* istanbul ignore next */ []));
10524
10903
  slideOut = computed(() => keyframes({ from: { transform: 'translateX(0)' }, to: { transform: `translateX(${this.edge()})` } }), ...(ngDevMode ? [{ debugName: "slideOut" }] : /* istanbul ignore next */ []));
10904
+ /** Input wins over the theme option; the literal is the last-resort default. */
10905
+ panelWidth = computed(() => this.width() ?? this.componentOptions().width ?? 280, ...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
10906
+ showScrim = computed(() => this.scrim() ?? this.componentOptions().scrim ?? true, ...(ngDevMode ? [{ debugName: "showScrim" }] : /* istanbul ignore next */ []));
10907
+ /**
10908
+ * The panel's surface. `solid` is the plain color pair; `glass` and
10909
+ * `gradient` derive from it, so a theme swaps treatment without restating
10910
+ * the color.
10911
+ */
10912
+ surface = computed(() => {
10913
+ const options = this.componentOptions();
10914
+ const pair = this.theme.colorPair(options.color);
10915
+ const base = pair?.backgroundColor;
10916
+ const treatment = options.background ?? 'solid';
10917
+ if (!base || treatment === 'solid')
10918
+ return pair;
10919
+ if (treatment === 'glass') {
10920
+ return {
10921
+ ...pair,
10922
+ backgroundColor: `color-mix(in srgb, ${base} 72%, transparent)`,
10923
+ backdropFilter: 'blur(12px) saturate(1.4)',
10924
+ };
10925
+ }
10926
+ // A vertical tint toward the content color: always visible, and it never
10927
+ // lets the page show through the way a fade to transparent would.
10928
+ return {
10929
+ ...pair,
10930
+ backgroundImage: `linear-gradient(to bottom, ${base} 0%, color-mix(in srgb, ${base} 92%, ${pair?.color ?? 'transparent'} 8%) 100%)`,
10931
+ };
10932
+ }, ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
10933
+ /** The shared flex column: three rows, and never a scroll container itself. */
10934
+ shell = {
10935
+ boxSizing: 'border-box',
10936
+ display: 'flex',
10937
+ flexDirection: 'column',
10938
+ // Both axes, explicitly, and never the shorthand. Setting one axis alone
10939
+ // computes the other to `auto` — which is precisely how a panel becomes
10940
+ // an accidental scroll container.
10941
+ overflowX: 'clip',
10942
+ overflowY: 'clip',
10943
+ };
10525
10944
  sideClass = computed(() => {
10526
10945
  const options = this.componentOptions();
10527
- const width = options.width ?? 280;
10946
+ const width = this.panelWidth();
10528
10947
  const start = this.position() === 'start';
10529
10948
  return css({
10530
- display: 'block',
10531
- boxSizing: 'border-box',
10949
+ ...this.shell,
10532
10950
  height: '100%',
10533
10951
  flex: 'none',
10534
- overflowX: 'hidden',
10535
- overflowY: 'auto',
10536
10952
  transition: 'width 0.25s ease, visibility 0.25s',
10537
- ...this.theme.colorPair(options.color),
10953
+ ...this.surface(),
10538
10954
  ...(start
10539
10955
  ? this.theme.borderRight(options.divider)
10540
10956
  : this.theme.borderLeft(options.divider)),
10541
10957
  ...(this.open()
10542
- ? { width, visibility: 'visible', ...this.theme.padding(options.padding) }
10543
- : { width: 0, visibility: 'hidden', padding: 0, border: 'none' }),
10958
+ ? { width, visibility: 'visible' }
10959
+ : { width: 0, visibility: 'hidden', border: 'none' }),
10544
10960
  });
10545
10961
  }, ...(ngDevMode ? [{ debugName: "sideClass" }] : /* istanbul ignore next */ []));
10546
10962
  overClass = computed(() => {
10547
10963
  const options = this.componentOptions();
10548
10964
  const start = this.position() === 'start';
10549
10965
  return css({
10550
- boxSizing: 'border-box',
10551
- width: options.width ?? 280,
10966
+ ...this.shell,
10967
+ width: this.panelWidth(),
10552
10968
  maxWidth: '90vw',
10553
10969
  height: '100dvh',
10554
10970
  maxHeight: '100dvh',
10555
10971
  border: 'none',
10972
+ padding: 0,
10556
10973
  margin: start ? '0 auto 0 0' : '0 0 0 auto',
10557
- overflowY: 'auto',
10558
- ...this.theme.colorPair(options.color),
10559
- ...this.theme.padding(options.padding),
10974
+ ...this.surface(),
10560
10975
  ...this.theme.boxShadow(options.elevation),
10561
- '&::backdrop': { ...options.backdrop },
10976
+ // The UA stylesheet hides a closed dialog with `display: none`, which the
10977
+ // shell's `display: flex` would otherwise beat on specificity — leaving
10978
+ // the panel sitting in normal flow behind the page whenever it is shut.
10979
+ // The closing animation still runs: `open` is only removed after it ends.
10980
+ '&:not([open])': { display: 'none' },
10981
+ // `scrim: false` keeps the modality — focus trap, inert page — but stops
10982
+ // the drawer dimming what it covers.
10983
+ '&::backdrop': this.showScrim() ? { ...options.backdrop } : { background: 'transparent' },
10562
10984
  '&[open]': { animation: `${this.slideIn()} 250ms ease-out` },
10563
10985
  '&[closing]': { animation: `${this.slideOut()} 250ms ease-in` },
10564
10986
  });
10565
10987
  }, ...(ngDevMode ? [{ debugName: "overClass" }] : /* istanbul ignore next */ []));
10988
+ /** The only scrolling row, and the only padded one. */
10989
+ bodyClass = computed(() => css({
10990
+ flex: '1 1 auto',
10991
+ minHeight: 0,
10992
+ // Defence in depth: a positioned body is the containing block for any
10993
+ // stray absolute descendant, so nothing can re-home into an ancestor
10994
+ // and inflate its scrollHeight. Only safe because the shell above is
10995
+ // `overflow: clip` — on its own this would move the phantom overflow
10996
+ // into this scroller instead of out of the panel.
10997
+ position: 'relative',
10998
+ overflowX: 'hidden',
10999
+ overflowY: 'auto',
11000
+ overscrollBehavior: 'contain',
11001
+ ...this.theme.padding(this.componentOptions().padding),
11002
+ }), ...(ngDevMode ? [{ debugName: "bodyClass" }] : /* istanbul ignore next */ []));
10566
11003
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10567
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDrawerComponent, isStandalone: true, selector: "uni-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'drawer' }], viewQueries: [{ propertyName: "contentTemplate", first: true, predicate: ["content"], descendants: true, isSignal: true }, { propertyName: "overlay", first: true, predicate: ["overlay"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
10568
- <ng-template #content><ng-content /></ng-template>
11004
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDrawerComponent, isStandalone: true, selector: "uni-drawer", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, headline: { classPropertyName: "headline", publicName: "headline", isSignal: true, isRequired: false, transformFunction: null }, defaultCloseButton: { classPropertyName: "defaultCloseButton", publicName: "defaultCloseButton", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, scrim: { classPropertyName: "scrim", publicName: "scrim", isSignal: true, isRequired: false, transformFunction: null }, initialFocus: { classPropertyName: "initialFocus", publicName: "initialFocus", isSignal: true, isRequired: false, transformFunction: null }, disableAutoClose: { classPropertyName: "disableAutoClose", publicName: "disableAutoClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", closeRequest: "closeRequest" }, providers: [
11005
+ { provide: COMPONENT_NAME, useValue: 'drawer' },
11006
+ { provide: DRAWER_PANEL, useExisting: forwardRef(() => UniDrawerComponent) },
11007
+ ], viewQueries: [{ propertyName: "headerTemplate", first: true, predicate: ["header"], descendants: true, isSignal: true }, { propertyName: "bodyTemplate", first: true, predicate: ["body"], descendants: true, isSignal: true }, { propertyName: "footerTemplate", first: true, predicate: ["footer"], descendants: true, isSignal: true }, { propertyName: "overlay", first: true, predicate: ["overlay"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
11008
+ <!-- One <ng-content> per slot, each parked in a template so both modes can
11009
+ render the same projected nodes. The catch-all is declared last so the
11010
+ two selective slots claim their content first. -->
11011
+ <ng-template #header>
11012
+ @if (headline()) {
11013
+ <div uni-drawer-header></div>
11014
+ }
11015
+ <ng-content select="[uni-drawer-header]" />
11016
+ </ng-template>
11017
+ <ng-template #footer>
11018
+ <ng-content select="[uni-drawer-buttons], [drawer-buttons]" />
11019
+ </ng-template>
11020
+ <ng-template #body><ng-content /></ng-template>
11021
+
10569
11022
  @if (mode() === 'side') {
10570
11023
  <aside [class]="sideClass()" [attr.aria-hidden]="open() ? null : 'true'">
10571
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
11024
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
11025
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
11026
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10572
11027
  </aside>
10573
11028
  } @else {
10574
11029
  <!-- Click handles the ::backdrop only (target check); keyboard closing
@@ -10578,28 +11033,49 @@ class UniDrawerComponent extends BaseComponent {
10578
11033
  <dialog
10579
11034
  #overlay
10580
11035
  [class]="overClass()"
10581
- [attr.aria-label]="ariaLabel()"
11036
+ [attr.aria-labelledby]="labelledBy()"
11037
+ [attr.aria-label]="labelledBy() ? null : ariaLabel()"
10582
11038
  (click)="onBackdropClick($event)"
10583
11039
  (cancel)="onCancel($event)"
10584
11040
  (animationend)="onAnimationEnd($event)"
10585
11041
  >
10586
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
11042
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
11043
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
11044
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10587
11045
  </dialog>
10588
11046
  }
10589
- `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11047
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniDrawerHeaderComponent, selector: "[uni-drawer-header]", inputs: ["headline"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10590
11048
  }
10591
11049
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerComponent, decorators: [{
10592
11050
  type: Component,
10593
11051
  args: [{
10594
11052
  changeDetection: ChangeDetectionStrategy.OnPush,
10595
11053
  selector: 'uni-drawer',
10596
- imports: [NgTemplateOutlet],
10597
- providers: [{ provide: COMPONENT_NAME, useValue: 'drawer' }],
11054
+ imports: [NgTemplateOutlet, UniDrawerHeaderComponent],
11055
+ providers: [
11056
+ { provide: COMPONENT_NAME, useValue: 'drawer' },
11057
+ { provide: DRAWER_PANEL, useExisting: forwardRef(() => UniDrawerComponent) },
11058
+ ],
10598
11059
  template: `
10599
- <ng-template #content><ng-content /></ng-template>
11060
+ <!-- One <ng-content> per slot, each parked in a template so both modes can
11061
+ render the same projected nodes. The catch-all is declared last so the
11062
+ two selective slots claim their content first. -->
11063
+ <ng-template #header>
11064
+ @if (headline()) {
11065
+ <div uni-drawer-header></div>
11066
+ }
11067
+ <ng-content select="[uni-drawer-header]" />
11068
+ </ng-template>
11069
+ <ng-template #footer>
11070
+ <ng-content select="[uni-drawer-buttons], [drawer-buttons]" />
11071
+ </ng-template>
11072
+ <ng-template #body><ng-content /></ng-template>
11073
+
10600
11074
  @if (mode() === 'side') {
10601
11075
  <aside [class]="sideClass()" [attr.aria-hidden]="open() ? null : 'true'">
10602
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
11076
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
11077
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
11078
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10603
11079
  </aside>
10604
11080
  } @else {
10605
11081
  <!-- Click handles the ::backdrop only (target check); keyboard closing
@@ -10609,17 +11085,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
10609
11085
  <dialog
10610
11086
  #overlay
10611
11087
  [class]="overClass()"
10612
- [attr.aria-label]="ariaLabel()"
11088
+ [attr.aria-labelledby]="labelledBy()"
11089
+ [attr.aria-label]="labelledBy() ? null : ariaLabel()"
10613
11090
  (click)="onBackdropClick($event)"
10614
11091
  (cancel)="onCancel($event)"
10615
11092
  (animationend)="onAnimationEnd($event)"
10616
11093
  >
10617
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
11094
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
11095
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
11096
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10618
11097
  </dialog>
10619
11098
  }
10620
11099
  `,
10621
11100
  }]
10622
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], contentTemplate: [{ type: i0.ViewChild, args: ['content', { isSignal: true }] }], overlay: [{ type: i0.ViewChild, args: ['overlay', { isSignal: true }] }] } });
11101
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], headline: [{ type: i0.Input, args: [{ isSignal: true, alias: "headline", required: false }] }], defaultCloseButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultCloseButton", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], scrim: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrim", required: false }] }], initialFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialFocus", required: false }] }], closeRequest: [{ type: i0.Output, args: ["closeRequest"] }], disableAutoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableAutoClose", required: false }] }], headerTemplate: [{ type: i0.ViewChild, args: ['header', { isSignal: true }] }], bodyTemplate: [{ type: i0.ViewChild, args: ['body', { isSignal: true }] }], footerTemplate: [{ type: i0.ViewChild, args: ['footer', { isSignal: true }] }], overlay: [{ type: i0.ViewChild, args: ['overlay', { isSignal: true }] }] } });
10623
11102
 
10624
11103
  class UniExpandComponent extends BaseComponent {
10625
11104
  collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
@@ -14263,5 +14742,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
14263
14742
  * Generated bundle index. Do not edit.
14264
14743
  */
14265
14744
 
14266
- export { BodyRenderDirective, ConfirmationDialogComponent, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, TRANSFORM_ORIGINS, ThemeService, UNI_FORMS, UNI_LAYOUT, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxDirective, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCalloutComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterDirective, UniCheckboxComponent, UniComboboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaDirective, UniGridDirective, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniNumberInputComponent, UniNumberRangeInputComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniQuantityStepperComponent, UniRadioComponent, UniRecordDatasource, UniRowDirective, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackDirective, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextDirective, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniTourComponent, UniWrapDirective, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, clampDecimal, clearAnchorName, compareDecimal, createAnnouncer, createListboxNavigation, createPressRepeat, dayOfWeek, daysInMonth, decimalScale, discreteOverlayTransition, evaluateExpression, focusableElements, formatDate, formatMonthHeading, formatNumber, formatTime, fromScaled, getFileExtension, inclusiveDayCount, isCanonicalDecimal, isDivider, isToggleOpen, isValidDate, isoDate, joinDateTime, listboxPopupAttr, listboxPopupStyles, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeNumberParts, localeWeekStart, losesPrecision, monthOf, motionSafe, newAnchorName, newListboxAnchor, normalizeDecimal, parseDateText, parseNumber, parseTimeText, promoteListboxPopup, rawNumberText, resolveElement, resolveFocusTarget, resolveNumberFormat, restoreOverlayFocus, roundDecimal, setAnchorName, settleNumber, shiftDecimal, speakNumber, splitDateTime, spotlightStyles, stepDecimal, supportsAnchoredPopup, timeSlots, toAsciiDigits, toDecimal, toNumber, toScaled, todayIso, transformOriginFor, uniqueId, useTimer, visuallyHidden, weekdayNames };
14745
+ export { BodyRenderDirective, ConfirmationDialogComponent, DRAWER_PANEL, DragAndDropDirective, FOCUSABLE_SELECTOR, ListboxNavigation, LocalStorageService, NotificationService, NotificationsComponent, RippleDirective, TRANSFORM_ORIGINS, ThemeService, UNI_FORMS, UNI_LAYOUT, UNI_THEMES, UniAlertComponent, UniAppBarComponent, UniAvatarComponent, UniAvatarGroupComponent, UniBackgroundComponent, UniBadgeComponent, UniBaseDatasource, UniBoxDirective, UniBreadcrumbComponent, UniButtonComponent, UniButtonGroupComponent, UniCalendarComponent, UniCalloutComponent, UniCardComponent, UniCardContentComponent, UniCardHeaderComponent, UniCenterDirective, UniCheckboxComponent, UniComboboxComponent, UniDataSearchComponent, UniDataTableComponent, UniDateInputComponent, UniDateTimeInputComponent, UniDebounceInputComponent, UniDialogButtonsComponent, UniDialogComponent, UniDialogHeaderComponent, UniDividerComponent, UniDrawerButtonsComponent, UniDrawerComponent, UniDrawerHeaderComponent, UniDropdownComponent, UniExpandAreaComponent, UniExpandComponent, UniExpandToggleComponent, UniFileDropZoneComponent, UniGridAreaDirective, UniGridDirective, UniIconButtonComponent, UniIconComponent, UniInputBoxComponent, UniInputComponent, UniJsonViewComponent, UniMenuComponent, UniMultiSelectComponent, UniMultiSelectDropdownComponent, UniNotificationBadgeComponent, UniNumberInputComponent, UniNumberRangeInputComponent, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, UniQuantityStepperComponent, UniRadioComponent, UniRecordDatasource, UniRowDirective, UniScrollAreaComponent, UniSearchInputComponent, UniSelectComponent, UniServerSideDatasource, UniSkeletonComponent, UniSliderComponent, UniSnackbarComponent, UniSortHeaderComponent, UniStackDirective, UniStatComponent, UniSymbolComponent, UniTabComponent, UniTabsComponent, UniTagComponent, UniTagInputComponent, UniTextDirective, UniTextareaComponent, UniThemeBuilderComponent, UniThemeSwitchComponent, UniTimeInputComponent, UniToggleComponent, UniTooltipComponent, UniTourComponent, UniWrapDirective, acceptableFile, addDays, addMonths, anchorArrowStyles, anchorStyles, buildMonthGrid, clampDecimal, clearAnchorName, compareDecimal, createAnnouncer, createListboxNavigation, createPressRepeat, dayOfWeek, daysInMonth, decimalScale, discreteOverlayTransition, evaluateExpression, focusableElements, formatDate, formatMonthHeading, formatNumber, formatTime, fromScaled, getFileExtension, inclusiveDayCount, isCanonicalDecimal, isDivider, isToggleOpen, isValidDate, isoDate, joinDateTime, listboxPopupAttr, listboxPopupStyles, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeNumberParts, localeWeekStart, losesPrecision, monthOf, motionSafe, newAnchorName, newListboxAnchor, normalizeDecimal, parseDateText, parseNumber, parseTimeText, promoteListboxPopup, rawNumberText, resolveElement, resolveFocusTarget, resolveNumberFormat, restoreOverlayFocus, roundDecimal, setAnchorName, settleNumber, shiftDecimal, speakNumber, splitDateTime, spotlightStyles, stepDecimal, supportsAnchoredPopup, timeSlots, toAsciiDigits, toDecimal, toNumber, toScaled, todayIso, transformOriginFor, uniqueId, useTimer, visuallyHidden, weekdayNames };
14267
14746
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map