@uni-design-system/uni-angular 10.0.0 → 10.1.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 {
@@ -2533,7 +2556,7 @@ class UniCheckboxComponent extends BaseComponent {
2533
2556
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2534
2557
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
2535
2558
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
2536
- /** Synced from required() validators by the Signal Forms [field] directive. */
2559
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
2537
2560
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
2538
2561
  /**
2539
2562
  * Id(s) of external element(s) describing this control — typically your
@@ -5311,7 +5334,7 @@ class UniInputComponent {
5311
5334
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5312
5335
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5313
5336
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5314
- /** Synced from required() validators by the Signal Forms [field] directive. */
5337
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
5315
5338
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
5316
5339
  /**
5317
5340
  * Id(s) of external element(s) describing this control — typically your
@@ -5327,7 +5350,7 @@ class UniInputComponent {
5327
5350
  */
5328
5351
  type = input('text', ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
5329
5352
  // --- CONSTRAINTS ---
5330
- // These are Signal Forms' own optional control inputs, so the `[field]`
5353
+ // These are Signal Forms' own optional control inputs, so the `[formField]`
5331
5354
  // directive syncs them from the field's validators the same way it syncs
5332
5355
  // `required` — and they are reflected onto the native element so the browser
5333
5356
  // can do its part (number steppers, length limits, on-screen keyboards).
@@ -5548,7 +5571,7 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
5548
5571
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5549
5572
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5550
5573
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5551
- /** Synced from required() validators by the Signal Forms [field] directive. */
5574
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
5552
5575
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
5553
5576
  /**
5554
5577
  * Id(s) of external element(s) describing this control — typically your
@@ -5779,7 +5802,7 @@ class UniRadioComponent extends BaseComponent {
5779
5802
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5780
5803
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5781
5804
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5782
- /** Synced from required() validators by the Signal Forms [field] directive. */
5805
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
5783
5806
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
5784
5807
  /**
5785
5808
  * Id(s) of external element(s) describing this control — typically your
@@ -6090,7 +6113,7 @@ class UniSelectComponent {
6090
6113
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
6091
6114
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
6092
6115
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
6093
- /** Synced from required() validators by the Signal Forms [field] directive. */
6116
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
6094
6117
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
6095
6118
  /**
6096
6119
  * Id(s) of external element(s) describing this control — typically your
@@ -6537,6 +6560,9 @@ class UniNumberInputComponent extends BaseComponent {
6537
6560
  onRelease: () => this.announceValue(),
6538
6561
  disabled: () => this.disabled() || this.readOnly() || this.atMax(),
6539
6562
  repeat: () => this.repeat(),
6563
+ // A native spinner leaves focus in its field; without this the arrow keys
6564
+ // go dead the moment you click a stepper.
6565
+ focus: () => this.inputRef().nativeElement.focus(),
6540
6566
  timing: this.repeatTiming,
6541
6567
  });
6542
6568
  decrement = createPressRepeat({
@@ -6544,6 +6570,9 @@ class UniNumberInputComponent extends BaseComponent {
6544
6570
  onRelease: () => this.announceValue(),
6545
6571
  disabled: () => this.disabled() || this.readOnly() || this.atMin(),
6546
6572
  repeat: () => this.repeat(),
6573
+ // A native spinner leaves focus in its field; without this the arrow keys
6574
+ // go dead the moment you click a stepper.
6575
+ focus: () => this.inputRef().nativeElement.focus(),
6547
6576
  timing: this.repeatTiming,
6548
6577
  });
6549
6578
  // --- Input events ---------------------------------------------------------
@@ -7947,11 +7976,13 @@ class UniQuantityStepperComponent extends BaseComponent {
7947
7976
  increment = createPressRepeat({
7948
7977
  onStep: () => this.applyStep(1, false),
7949
7978
  onRelease: () => this.announceValue(),
7979
+ focus: (button) => this.focusField(button),
7950
7980
  disabled: () => this.disabled() || this.atMax(),
7951
7981
  });
7952
7982
  decrement = createPressRepeat({
7953
7983
  onStep: () => this.applyStep(-1, false),
7954
7984
  onRelease: () => this.announceValue(),
7985
+ focus: (button) => this.focusField(button),
7955
7986
  disabled: () => this.disabled() || this.atMin(),
7956
7987
  });
7957
7988
  // --- Keyboard -------------------------------------------------------------
@@ -7985,8 +8016,17 @@ class UniQuantityStepperComponent extends BaseComponent {
7985
8016
  this.increment.cancel();
7986
8017
  this.decrement.cancel();
7987
8018
  }
7988
- focusField() {
7989
- this.inputRef()?.nativeElement.focus();
8019
+ /**
8020
+ * Focus the field a stepper press should land in. With `editable=false` there
8021
+ * is no field and the buttons are the tab stops, so the pressed button takes
8022
+ * it instead.
8023
+ */
8024
+ focusField(fallback) {
8025
+ const input = this.inputRef()?.nativeElement;
8026
+ if (input)
8027
+ input.focus();
8028
+ else
8029
+ fallback?.focus();
7990
8030
  }
7991
8031
  // --- Styling --------------------------------------------------------------
7992
8032
  className = computed(() => css({ display: 'inline-block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
@@ -8000,7 +8040,6 @@ class UniQuantityStepperComponent extends BaseComponent {
8000
8040
  */
8001
8041
  fieldChrome = this.theme.getComponentOptions('input');
8002
8042
  rootClass = computed(() => {
8003
- const options = this.componentOptions();
8004
8043
  const colors = this.theme.colors();
8005
8044
  const chrome = this.fieldChrome();
8006
8045
  return css({
@@ -8011,9 +8050,9 @@ class UniQuantityStepperComponent extends BaseComponent {
8011
8050
  // field beside it rather than 32 plus its border.
8012
8051
  boxSizing: 'border-box',
8013
8052
  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'),
8053
+ ...this.theme.backgroundColor(this.disabled() ? chrome.disabledColor : this.containerColor()),
8054
+ ...this.theme.border(this.containerBorder()),
8055
+ ...this.theme.radius(this.containerRadius()),
8017
8056
  ...(this.showError() ? { borderColor: colors['warn'] } : {}),
8018
8057
  ...(this.disabled() ? { cursor: 'not-allowed' } : {}),
8019
8058
  // The middle input clears its own outline (removeInputPlatformStyling),
@@ -8061,8 +8100,30 @@ class UniQuantityStepperComponent extends BaseComponent {
8061
8100
  ...this.theme.focusRing(),
8062
8101
  });
8063
8102
  }, ...(ngDevMode ? [{ debugName: "buttonClass" }] : /* istanbul ignore next */ []));
8103
+ /**
8104
+ * Container chrome, defaulting to the shared `input` entry rather than to
8105
+ * hardcoded tokens. It is not a field, but it sits beside them in carts and
8106
+ * table rows, so a theme that restyles `input` must carry it along — the
8107
+ * options below stay as per-component overrides for a deliberately different
8108
+ * look.
8109
+ */
8110
+ containerColor = computed(() => this.componentOptions().color ?? this.fieldChrome().color, ...(ngDevMode ? [{ debugName: "containerColor" }] : /* istanbul ignore next */ []));
8111
+ containerBorder = computed(() => this.componentOptions().border ?? this.fieldChrome().border, ...(ngDevMode ? [{ debugName: "containerBorder" }] : /* istanbul ignore next */ []));
8112
+ containerRadius = computed(() => this.componentOptions().borderRadius ?? this.fieldChrome().borderRadius, ...(ngDevMode ? [{ debugName: "containerRadius" }] : /* istanbul ignore next */ []));
8064
8113
  /** 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 */ []));
8114
+ dividerBorder = computed(() => this.containerBorder(), ...(ngDevMode ? [{ debugName: "dividerBorder" }] : /* istanbul ignore next */ []));
8115
+ /**
8116
+ * Characters the value cell asks the browser to size itself for.
8117
+ *
8118
+ * Load-bearing: a bare `<input>` defaults to `size="20"`, and `flex-basis:
8119
+ * auto` resolves to that intrinsic width — so the control claimed ~230px
8120
+ * instead of the ~100px its buttons and `valueWidth` need, and stole track
8121
+ * width from anything beside it in a grid (`1fr` is `minmax(auto, 1fr)`, and
8122
+ * the `auto` floor includes this). Tracking the content keeps the cell honest
8123
+ * while still letting it grow with the digits, which a fixed `width` would
8124
+ * not. `valueWidth` remains the floor, via `min-width`.
8125
+ */
8126
+ valueSize = computed(() => Math.max(this.displayText().length, 1), ...(ngDevMode ? [{ debugName: "valueSize" }] : /* istanbul ignore next */ []));
8066
8127
  valueBase() {
8067
8128
  const options = this.componentOptions();
8068
8129
  const colors = this.theme.colors();
@@ -8095,11 +8156,11 @@ class UniQuantityStepperComponent extends BaseComponent {
8095
8156
  readoutClass = computed(() => css([this.valueBase(), { display: 'grid', placeItems: 'center' }]), ...(ngDevMode ? [{ debugName: "readoutClass" }] : /* istanbul ignore next */ []));
8096
8157
  glyphSize = computed(() => Math.max(12, Math.round(this.height() / 2)), ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
8097
8158
  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 });
8159
+ 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
8160
  }
8100
8161
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniQuantityStepperComponent, decorators: [{
8101
8162
  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" }]
8163
+ 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
8164
  }], 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
8165
 
8105
8166
  /**
@@ -8667,7 +8728,7 @@ class UniTextareaComponent {
8667
8728
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
8668
8729
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
8669
8730
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
8670
- /** Synced from required() validators by the Signal Forms [field] directive. */
8731
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
8671
8732
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
8672
8733
  /**
8673
8734
  * Id(s) of external element(s) describing this control — typically your
@@ -8679,7 +8740,7 @@ class UniTextareaComponent {
8679
8740
  placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
8680
8741
  /** Visible text rows. Defaults to the theme's `textarea` options. */
8681
8742
  rows = input(undefined, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
8682
- // Signal Forms' own optional control inputs: the `[field]` directive syncs
8743
+ // Signal Forms' own optional control inputs: the `[formField]` directive syncs
8683
8744
  // them from the field's validators, as it does `required`.
8684
8745
  readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
8685
8746
  name = input('', ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
@@ -8714,6 +8775,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
8714
8775
  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
8776
  }], 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
8777
 
8778
+ /**
8779
+ * Everything a switch needs, from the three numbers a theme actually states.
8780
+ *
8781
+ * `travel` is why this is derived rather than written down: the knob starts at
8782
+ * `inset` and must end the same distance from the far edge, so it moves
8783
+ * `width - inset - knob - inset`, which reduces to `width - height`. The old
8784
+ * code hardcoded a translate of one track height, which was correct only while
8785
+ * the width was locked at 2x and the knob at 0.8x.
8786
+ */
8787
+ function geometry(width, height, inset) {
8788
+ return { width, height, inset, knob: height - inset * 2, travel: width - height, radius: height / 2 };
8789
+ }
8717
8790
  class UniToggleComponent extends BaseComponent {
8718
8791
  // --- REQUIRED SIGNALS (populated by FormCheckboxControl) ---
8719
8792
  checked = model(false, ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
@@ -8721,7 +8794,7 @@ class UniToggleComponent extends BaseComponent {
8721
8794
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
8722
8795
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
8723
8796
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
8724
- /** Synced from required() validators by the Signal Forms [field] directive. */
8797
+ /** Synced from required() validators by the Signal Forms [formField] directive. */
8725
8798
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
8726
8799
  /**
8727
8800
  * Id(s) of external element(s) describing this control — typically your
@@ -8730,6 +8803,16 @@ class UniToggleComponent extends BaseComponent {
8730
8803
  ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
8731
8804
  // --- CONFIGURATION ---
8732
8805
  label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : /* istanbul ignore next */ []));
8806
+ /**
8807
+ * Checked-state track color token, overriding the theme's
8808
+ * `toggle.behavior.checkedColor`.
8809
+ *
8810
+ * This exists alongside the theme option because `variant` — where this color
8811
+ * used to live exclusively — defaults to `'primary'`, so the component cannot
8812
+ * tell "set to primary" from "not set". Without an input, a theme-level
8813
+ * `checkedColor` would silently make per-instance `variant` inert.
8814
+ */
8815
+ checkedColor = input(...(ngDevMode ? [undefined, { debugName: "checkedColor" }] : /* istanbul ignore next */ []));
8733
8816
  // Only show errors if the user has actually interacted with the field
8734
8817
  showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
8735
8818
  markAsTouched() {
@@ -8739,18 +8822,44 @@ class UniToggleComponent extends BaseComponent {
8739
8822
  this.checked.set(event.target.checked);
8740
8823
  this.markAsTouched();
8741
8824
  }
8825
+ /**
8826
+ * Track and knob geometry for the active `size`, read out of the theme's
8827
+ * `sizes` block as data — `width`, `height` and the knob's inset `padding`.
8828
+ *
8829
+ * Read rather than spread: `padding` must not reach the track as real CSS or
8830
+ * it would double up with the knob's own `top`/`left` offsets. `uni-calendar`
8831
+ * treats its size block the same way.
8832
+ */
8742
8833
  metrics = computed(() => {
8743
- const toggleSize = this.componentOptions().size || 20;
8744
- const sliderSize = toggleSize * 0.8;
8834
+ // The legacy single-number token wins when a theme still sets it: that
8835
+ // theme opted into the old derived-ratio geometry before `sizes` existed,
8836
+ // and it applies to every instance regardless of the `size` input.
8837
+ const legacy = this.componentOptions().size;
8838
+ if (legacy != null) {
8839
+ const height = Number(legacy);
8840
+ return geometry(height * 2, height, (height - height * 0.8) / 2);
8841
+ }
8842
+ const size = this.style();
8843
+ const height = Number(size['height'] ?? 20);
8844
+ const width = Number(size['width'] ?? height * 2);
8845
+ const inset = Number(size['padding'] ?? (height - height * 0.8) / 2);
8846
+ return geometry(width, height, inset);
8847
+ }, ...(ngDevMode ? [{ debugName: "metrics" }] : /* istanbul ignore next */ []));
8848
+ /** The resolved checked/accent color: input, then theme option, then variant. */
8849
+ accent = computed(() => this.checkedColor() ?? this.componentOptions().checkedColor ?? this.variant(), ...(ngDevMode ? [{ debugName: "accent" }] : /* istanbul ignore next */ []));
8850
+ /** Knob slide and track color change, as a motion token — never `all`. */
8851
+ transitions = computed(() => {
8852
+ const motion = this.theme.motion(this.componentOptions().motion ?? 'control');
8853
+ const speed = motion.duration / 1000;
8745
8854
  return {
8746
- toggleSize,
8747
- toggleWidth: toggleSize * 2,
8748
- sliderSize,
8749
- sliderOffset: (toggleSize - sliderSize) / 2,
8855
+ // Scoped, never `all`: the focus ring must apply instantly rather than
8856
+ // interpolating its outline color from a stale value.
8857
+ track: `background-color ${speed}s ${motion.easing}, border-color ${speed}s ${motion.easing}`,
8858
+ knob: `transform ${speed}s ${motion.easing}, background-color ${speed}s ${motion.easing}`,
8750
8859
  };
8751
- }, ...(ngDevMode ? [{ debugName: "metrics" }] : /* istanbul ignore next */ []));
8860
+ }, ...(ngDevMode ? [{ debugName: "transitions" }] : /* istanbul ignore next */ []));
8752
8861
  toggleLabel = computed(() => {
8753
- const { toggleSize, toggleWidth, sliderSize, sliderOffset } = this.metrics();
8862
+ const { height, width, knob, inset, radius } = this.metrics();
8754
8863
  return css({
8755
8864
  userSelect: 'none',
8756
8865
  cursor: this.disabled() ? 'not-allowed' : 'pointer',
@@ -8760,26 +8869,24 @@ class UniToggleComponent extends BaseComponent {
8760
8869
  gap: 8,
8761
8870
  opacity: this.disabled() ? 0.6 : 1,
8762
8871
  '& .toggle-switch': {
8763
- width: toggleWidth,
8764
- height: toggleSize,
8872
+ width,
8873
+ height,
8765
8874
  backgroundColor: this.disabled()
8766
8875
  ? this.getThemeColor('disabled')
8767
8876
  : this.getThemeColor(this.componentOptions().trackColor ?? 'surface-variant'),
8768
- borderRadius: toggleSize / 2,
8877
+ borderRadius: radius,
8769
8878
  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',
8879
+ transition: this.transitions().track,
8773
8880
  },
8774
8881
  '& .toggle-slider': {
8775
- width: sliderSize,
8776
- height: sliderSize,
8882
+ width: knob,
8883
+ height: knob,
8777
8884
  backgroundColor: this.getThemeColor(this.componentOptions().knobColor ?? 'surface'),
8778
8885
  borderRadius: '50%',
8779
8886
  position: 'absolute',
8780
- top: sliderOffset,
8781
- left: sliderOffset,
8782
- transition: 'transform 0.3s ease, background-color 0.3s ease',
8887
+ top: inset,
8888
+ left: inset,
8889
+ transition: this.transitions().knob,
8783
8890
  ...this.theme.boxShadow('raised'),
8784
8891
  },
8785
8892
  // Hover darkens whatever the token resolves to — the button convention.
@@ -8791,7 +8898,8 @@ class UniToggleComponent extends BaseComponent {
8791
8898
  });
8792
8899
  }, ...(ngDevMode ? [{ debugName: "toggleLabel" }] : /* istanbul ignore next */ []));
8793
8900
  toggleInput = computed(() => {
8794
- const { toggleSize } = this.metrics();
8901
+ const { travel } = this.metrics();
8902
+ const accent = this.getThemeColor(this.accent());
8795
8903
  return css({
8796
8904
  position: 'absolute',
8797
8905
  zIndex: -1,
@@ -8799,18 +8907,20 @@ class UniToggleComponent extends BaseComponent {
8799
8907
  height: 0,
8800
8908
  opacity: 0,
8801
8909
  '&:checked + .toggle-switch': {
8802
- backgroundColor: this.getThemeColor(this.variant()),
8803
- borderColor: this.getThemeColor(this.variant()),
8910
+ backgroundColor: accent,
8911
+ borderColor: accent,
8804
8912
  },
8805
8913
  '&:checked + .toggle-switch .toggle-slider': {
8806
- transform: `translateX(${toggleSize}px)`,
8914
+ transform: `translateX(${travel}px)`,
8807
8915
  },
8808
8916
  '&:disabled + .toggle-switch': {
8809
8917
  cursor: 'not-allowed',
8810
8918
  },
8811
- // The shared, themable focus indicator, keyed off the hidden input.
8919
+ // The shared, themable focus indicator, keyed off the hidden input. It
8920
+ // wears the checked color rather than the variant, so a themed on-state
8921
+ // is not paired with a ring in some other role's color.
8812
8922
  '&:focus + .toggle-switch': {
8813
- ...this.theme.focusRingStyle(this.getThemeColor(this.variant())),
8923
+ ...this.theme.focusRingStyle(accent),
8814
8924
  },
8815
8925
  });
8816
8926
  }, ...(ngDevMode ? [{ debugName: "toggleInput" }] : /* istanbul ignore next */ []));
@@ -8819,12 +8929,12 @@ class UniToggleComponent extends BaseComponent {
8819
8929
  return colors[token] ? colors[token] : colors['primary'];
8820
8930
  }
8821
8931
  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 });
8932
+ 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
8933
  }
8824
8934
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniToggleComponent, decorators: [{
8825
8935
  type: Component,
8826
8936
  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 }] }] } });
8937
+ }], 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
8938
 
8829
8939
  /**
8830
8940
  * Every layout and typography attribute directive, for
@@ -10461,8 +10571,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
10461
10571
  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
10572
  }], ctorParameters: () => [] });
10463
10573
 
10574
+ const DRAWER_PANEL = new InjectionToken('uni-drawer-panel');
10575
+
10576
+ /**
10577
+ * The drawer's pinned footer action row — the save bar of an editor panel.
10578
+ *
10579
+ * Sits outside the scrolling body, so the actions stay reachable however long
10580
+ * the form is. Mirrors `[dialog-buttons]`; the difference is posture, which
10581
+ * lives in the `drawerButtons` theme options rather than here.
10582
+ */
10583
+ class UniDrawerButtonsComponent extends BaseComponent {
10584
+ drawer = inject(DRAWER_PANEL, { optional: true });
10585
+ confirmButtonText = input(...(ngDevMode ? [undefined, { debugName: "confirmButtonText" }] : /* istanbul ignore next */ []));
10586
+ confirmButtonVariant = input(...(ngDevMode ? [undefined, { debugName: "confirmButtonVariant" }] : /* istanbul ignore next */ []));
10587
+ cancelButtonText = input(...(ngDevMode ? [undefined, { debugName: "cancelButtonText" }] : /* istanbul ignore next */ []));
10588
+ cancelButtonVariant = input(...(ngDevMode ? [undefined, { debugName: "cancelButtonVariant" }] : /* istanbul ignore next */ []));
10589
+ disableConfirm = input(...(ngDevMode ? [undefined, { debugName: "disableConfirm" }] : /* istanbul ignore next */ []));
10590
+ padding = input(...(ngDevMode ? [undefined, { debugName: "padding" }] : /* istanbul ignore next */ []));
10591
+ justifyContent = input(...(ngDevMode ? [undefined, { debugName: "justifyContent" }] : /* istanbul ignore next */ []));
10592
+ confirmed = output();
10593
+ // Inputs win over theme options; the trailing literal is the fallback.
10594
+ confirmVariant = computed(() => this.confirmButtonVariant() ?? this.componentOptions().confirmButtonVariant ?? 'primary', ...(ngDevMode ? [{ debugName: "confirmVariant" }] : /* istanbul ignore next */ []));
10595
+ cancelVariant = computed(() => this.cancelButtonVariant() ?? this.componentOptions().cancelButtonVariant ?? 'quaternary', ...(ngDevMode ? [{ debugName: "cancelVariant" }] : /* istanbul ignore next */ []));
10596
+ paddingValue = computed(() => this.padding() ?? this.componentOptions().padding ?? 'md', ...(ngDevMode ? [{ debugName: "paddingValue" }] : /* istanbul ignore next */ []));
10597
+ justifyContentValue = computed(() => this.justifyContent() ?? this.componentOptions().justifyContent ?? 'flex-end', ...(ngDevMode ? [{ debugName: "justifyContentValue" }] : /* istanbul ignore next */ []));
10598
+ gapValue = computed(() => this.componentOptions().gap ?? 'sm', ...(ngDevMode ? [{ debugName: "gapValue" }] : /* istanbul ignore next */ []));
10599
+ buttonSize = computed(() => this.componentOptions().buttonSize ?? 'md', ...(ngDevMode ? [{ debugName: "buttonSize" }] : /* istanbul ignore next */ []));
10600
+ /** A pinned row, sized by its content rather than by the body beside it. */
10601
+ hostClass = computed(() => css({
10602
+ flex: 'none',
10603
+ ...this.theme.borderTop(this.componentOptions().divider),
10604
+ }), ...(ngDevMode ? [{ debugName: "hostClass" }] : /* istanbul ignore next */ []));
10605
+ className = computed(() => css([
10606
+ this.componentTheme().fixed,
10607
+ this.componentOptions().stretch && {
10608
+ width: '100%',
10609
+ minWidth: '100%',
10610
+ '& > button': { flex: '1 1 50%', maxWidth: '50%' },
10611
+ },
10612
+ ]), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
10613
+ /**
10614
+ * Cancel routes through the drawer's own close decision, so a panel with
10615
+ * unsaved changes can veto it exactly as it vetoes Escape.
10616
+ */
10617
+ closeDrawer() {
10618
+ this.drawer?.requestClose('close-button');
10619
+ }
10620
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerButtonsComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
10621
+ 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 });
10622
+ }
10623
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerButtonsComponent, decorators: [{
10624
+ type: Component,
10625
+ 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" }]
10626
+ }], 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"] }] } });
10627
+
10628
+ /**
10629
+ * The drawer's pinned header row: a title, and optionally a close button.
10630
+ *
10631
+ * Sits outside the scrolling body, so it stays put while the form beneath it
10632
+ * moves. Reached either by projecting it — `<div uni-drawer-header>` — or
10633
+ * implicitly, by giving `uni-drawer` a `headline`, in which case the drawer
10634
+ * renders one of these itself.
10635
+ */
10636
+ class UniDrawerHeaderComponent extends BaseComponent {
10637
+ drawer = inject(DRAWER_PANEL, { optional: true });
10638
+ /** Title text. Falls back to the drawer's `headline`; projected content wins over both. */
10639
+ headline = input(...(ngDevMode ? [undefined, { debugName: "headline" }] : /* istanbul ignore next */ []));
10640
+ /** Attached to the title so the drawer is labelled by it. */
10641
+ titleId = this.drawer?.titleId ?? null;
10642
+ title = computed(() => this.headline() ?? this.drawer?.headline() ?? '', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
10643
+ showClose = computed(() => this.drawer?.defaultCloseButton() ?? true, ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
10644
+ constructor() {
10645
+ super();
10646
+ // Tells the drawer it is labelled by this row rather than by `ariaLabel`.
10647
+ this.drawer?.hasHeader.set(true);
10648
+ }
10649
+ /** Never a bare close: the drawer decides, so a veto is honoured here too. */
10650
+ closeDrawer() {
10651
+ this.drawer?.requestClose('close-button');
10652
+ }
10653
+ className = computed(() => css({
10654
+ // A pinned row: it is a flex child of the panel and must not be sized
10655
+ // by the scrolling body beside it.
10656
+ flex: 'none',
10657
+ ...this.theme.borderBottom(this.componentOptions().divider),
10658
+ }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
10659
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10660
+ 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 });
10661
+ }
10662
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerHeaderComponent, decorators: [{
10663
+ type: Component,
10664
+ 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" }]
10665
+ }], ctorParameters: () => [], propDecorators: { headline: [{ type: i0.Input, args: [{ isSignal: true, alias: "headline", required: false }] }] } });
10666
+
10464
10667
  /**
10465
- * Navigation drawer with two modes sharing one content slot:
10668
+ * Drawer with two modes sharing one three-row layout:
10466
10669
  *
10467
10670
  * - `side` — an in-flow `<aside>` that pushes content (dashboard sidenav);
10468
10671
  * opening/closing animates its width, and the divider border primitive
@@ -10471,17 +10674,78 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
10471
10674
  * scrim backdrop come from the platform (same machinery as `uni-dialog`),
10472
10675
  * sliding in from its edge.
10473
10676
  *
10474
- * Surface, width, divider, elevation, padding and backdrop all resolve from
10475
- * `drawer` theme tokens.
10677
+ * **The panel is never the scroll container.** It is a flex column of three
10678
+ * rows — an optional `[uni-drawer-header]`, the projected body, an optional
10679
+ * `[uni-drawer-buttons]` — and only the body scrolls. The panel itself is
10680
+ * `overflow: clip` on both axes. That is what lets a header and a save bar pin
10681
+ * while a long form scrolls between them, and it is why the theme's `padding`
10682
+ * option lands on the body row rather than the panel: padding on a scrolling
10683
+ * box scrolls away with its content.
10684
+ *
10685
+ * Surface, width, divider, elevation, padding, backdrop, scrim and background
10686
+ * all resolve from `drawer` theme tokens.
10476
10687
  */
10477
10688
  class UniDrawerComponent extends BaseComponent {
10478
10689
  /** Two-way bindable open state: [(open)]. */
10479
10690
  open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
10480
10691
  mode = input('side', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
10481
10692
  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');
10693
+ /**
10694
+ * Accessible name for the overlay mode. Only consulted when the drawer has
10695
+ * no header to be labelled by.
10696
+ *
10697
+ * There is deliberately no default. A drawer used as an editor panel that
10698
+ * inherited the literal "Navigation" would announce itself as something it
10699
+ * is not, and a wrong accessible name is worse than a missing one — the
10700
+ * missing one is at least caught by any audit.
10701
+ */
10702
+ ariaLabel = input(...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
10703
+ /**
10704
+ * Title for the drawer's header row. Shorthand for projecting a
10705
+ * `[uni-drawer-header]`; project one instead when the header needs more
10706
+ * than a title (a record counter, prev/next navigation).
10707
+ */
10708
+ headline = input(...(ngDevMode ? [undefined, { debugName: "headline" }] : /* istanbul ignore next */ []));
10709
+ /** Whether the header row renders a close button. */
10710
+ defaultCloseButton = input(true, ...(ngDevMode ? [{ debugName: "defaultCloseButton" }] : /* istanbul ignore next */ []));
10711
+ /** Panel width in px, overriding the theme's `drawer.behavior.width`. */
10712
+ width = input(...(ngDevMode ? [undefined, { debugName: "width" }] : /* istanbul ignore next */ []));
10713
+ /**
10714
+ * Whether the overlay dims the page behind it, overriding the theme's
10715
+ * `drawer.behavior.scrim`. False leaves the backdrop transparent so the page
10716
+ * stays legible while the panel is open — an editor panel beside a board the
10717
+ * user is still reading.
10718
+ *
10719
+ * This does not make the drawer non-modal: focus is still trapped and the
10720
+ * page behind is still inert. It is a visibility choice, not a modality one.
10721
+ */
10722
+ scrim = input(...(ngDevMode ? [undefined, { debugName: "scrim" }] : /* istanbul ignore next */ []));
10723
+ /**
10724
+ * CSS selector for the element to focus when the overlay opens. The native
10725
+ * default is the first focusable element, which in an editor panel is
10726
+ * usually the close button rather than the first field.
10727
+ */
10728
+ initialFocus = input(...(ngDevMode ? [undefined, { debugName: "initialFocus" }] : /* istanbul ignore next */ []));
10729
+ /**
10730
+ * The drawer is *asking* to close — Escape, the backdrop, or a close/cancel
10731
+ * button. Pair with `disableAutoClose` to hold the panel open while an async
10732
+ * confirmation runs.
10733
+ */
10734
+ closeRequest = output();
10735
+ /**
10736
+ * When true the drawer never closes itself; it only emits `closeRequest` and
10737
+ * waits for the consumer to set `open`. Off by default, so a drawer that
10738
+ * ignores `closeRequest` behaves exactly as it always has.
10739
+ */
10740
+ disableAutoClose = input(false, ...(ngDevMode ? [{ debugName: "disableAutoClose" }] : /* istanbul ignore next */ []));
10741
+ /** Set by a projected `[uni-drawer-header]` so it can pin flush to the top. */
10742
+ hasHeader = signal(false, ...(ngDevMode ? [{ debugName: "hasHeader" }] : /* istanbul ignore next */ []));
10743
+ /** Id referenced by aria-labelledby; the header row attaches it to its title. */
10744
+ titleId = uniqueId('uni-drawer-title');
10745
+ labelledBy = computed(() => (this.hasHeader() ? this.titleId : null), ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
10746
+ headerTemplate = viewChild.required('header');
10747
+ bodyTemplate = viewChild.required('body');
10748
+ footerTemplate = viewChild.required('footer');
10485
10749
  overlay = viewChild('overlay', ...(ngDevMode ? [{ debugName: "overlay" }] : /* istanbul ignore next */ []));
10486
10750
  constructor() {
10487
10751
  super();
@@ -10493,6 +10757,9 @@ class UniDrawerComponent extends BaseComponent {
10493
10757
  if (!dialog.open) {
10494
10758
  dialog.removeAttribute('closing');
10495
10759
  dialog.showModal();
10760
+ const selector = this.initialFocus();
10761
+ if (selector)
10762
+ dialog.querySelector(selector)?.focus();
10496
10763
  }
10497
10764
  }
10498
10765
  else if (dialog.open) {
@@ -10501,14 +10768,24 @@ class UniDrawerComponent extends BaseComponent {
10501
10768
  }
10502
10769
  });
10503
10770
  }
10771
+ /**
10772
+ * The one place a close is decided, so every route in — Escape, the
10773
+ * backdrop, the header's close button, the footer's cancel — behaves
10774
+ * identically and is equally vetoable.
10775
+ */
10776
+ requestClose(reason) {
10777
+ this.closeRequest.emit({ reason });
10778
+ if (!this.disableAutoClose())
10779
+ this.open.set(false);
10780
+ }
10504
10781
  onBackdropClick(event) {
10505
10782
  if (event.target.nodeName === 'DIALOG')
10506
- this.open.set(false);
10783
+ this.requestClose('backdrop');
10507
10784
  }
10508
10785
  /** Route Escape through the animated close, keeping `open` in sync. */
10509
10786
  onCancel(event) {
10510
10787
  event.preventDefault();
10511
- this.open.set(false);
10788
+ this.requestClose('escape');
10512
10789
  }
10513
10790
  onAnimationEnd(event) {
10514
10791
  const dialog = this.overlay()?.nativeElement;
@@ -10522,53 +10799,129 @@ class UniDrawerComponent extends BaseComponent {
10522
10799
  edge = computed(() => (this.position() === 'start' ? '-100%' : '100%'), ...(ngDevMode ? [{ debugName: "edge" }] : /* istanbul ignore next */ []));
10523
10800
  slideIn = computed(() => keyframes({ from: { transform: `translateX(${this.edge()})` }, to: { transform: 'translateX(0)' } }), ...(ngDevMode ? [{ debugName: "slideIn" }] : /* istanbul ignore next */ []));
10524
10801
  slideOut = computed(() => keyframes({ from: { transform: 'translateX(0)' }, to: { transform: `translateX(${this.edge()})` } }), ...(ngDevMode ? [{ debugName: "slideOut" }] : /* istanbul ignore next */ []));
10802
+ /** Input wins over the theme option; the literal is the last-resort default. */
10803
+ panelWidth = computed(() => this.width() ?? this.componentOptions().width ?? 280, ...(ngDevMode ? [{ debugName: "panelWidth" }] : /* istanbul ignore next */ []));
10804
+ showScrim = computed(() => this.scrim() ?? this.componentOptions().scrim ?? true, ...(ngDevMode ? [{ debugName: "showScrim" }] : /* istanbul ignore next */ []));
10805
+ /**
10806
+ * The panel's surface. `solid` is the plain color pair; `glass` and
10807
+ * `gradient` derive from it, so a theme swaps treatment without restating
10808
+ * the color.
10809
+ */
10810
+ surface = computed(() => {
10811
+ const options = this.componentOptions();
10812
+ const pair = this.theme.colorPair(options.color);
10813
+ const base = pair?.backgroundColor;
10814
+ const treatment = options.background ?? 'solid';
10815
+ if (!base || treatment === 'solid')
10816
+ return pair;
10817
+ if (treatment === 'glass') {
10818
+ return {
10819
+ ...pair,
10820
+ backgroundColor: `color-mix(in srgb, ${base} 72%, transparent)`,
10821
+ backdropFilter: 'blur(12px) saturate(1.4)',
10822
+ };
10823
+ }
10824
+ // A vertical tint toward the content color: always visible, and it never
10825
+ // lets the page show through the way a fade to transparent would.
10826
+ return {
10827
+ ...pair,
10828
+ backgroundImage: `linear-gradient(to bottom, ${base} 0%, color-mix(in srgb, ${base} 92%, ${pair?.color ?? 'transparent'} 8%) 100%)`,
10829
+ };
10830
+ }, ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
10831
+ /** The shared flex column: three rows, and never a scroll container itself. */
10832
+ shell = {
10833
+ boxSizing: 'border-box',
10834
+ display: 'flex',
10835
+ flexDirection: 'column',
10836
+ // Both axes, explicitly, and never the shorthand. Setting one axis alone
10837
+ // computes the other to `auto` — which is precisely how a panel becomes
10838
+ // an accidental scroll container.
10839
+ overflowX: 'clip',
10840
+ overflowY: 'clip',
10841
+ };
10525
10842
  sideClass = computed(() => {
10526
10843
  const options = this.componentOptions();
10527
- const width = options.width ?? 280;
10844
+ const width = this.panelWidth();
10528
10845
  const start = this.position() === 'start';
10529
10846
  return css({
10530
- display: 'block',
10531
- boxSizing: 'border-box',
10847
+ ...this.shell,
10532
10848
  height: '100%',
10533
10849
  flex: 'none',
10534
- overflowX: 'hidden',
10535
- overflowY: 'auto',
10536
10850
  transition: 'width 0.25s ease, visibility 0.25s',
10537
- ...this.theme.colorPair(options.color),
10851
+ ...this.surface(),
10538
10852
  ...(start
10539
10853
  ? this.theme.borderRight(options.divider)
10540
10854
  : this.theme.borderLeft(options.divider)),
10541
10855
  ...(this.open()
10542
- ? { width, visibility: 'visible', ...this.theme.padding(options.padding) }
10543
- : { width: 0, visibility: 'hidden', padding: 0, border: 'none' }),
10856
+ ? { width, visibility: 'visible' }
10857
+ : { width: 0, visibility: 'hidden', border: 'none' }),
10544
10858
  });
10545
10859
  }, ...(ngDevMode ? [{ debugName: "sideClass" }] : /* istanbul ignore next */ []));
10546
10860
  overClass = computed(() => {
10547
10861
  const options = this.componentOptions();
10548
10862
  const start = this.position() === 'start';
10549
10863
  return css({
10550
- boxSizing: 'border-box',
10551
- width: options.width ?? 280,
10864
+ ...this.shell,
10865
+ width: this.panelWidth(),
10552
10866
  maxWidth: '90vw',
10553
10867
  height: '100dvh',
10554
10868
  maxHeight: '100dvh',
10555
10869
  border: 'none',
10870
+ padding: 0,
10556
10871
  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),
10872
+ ...this.surface(),
10560
10873
  ...this.theme.boxShadow(options.elevation),
10561
- '&::backdrop': { ...options.backdrop },
10874
+ // The UA stylesheet hides a closed dialog with `display: none`, which the
10875
+ // shell's `display: flex` would otherwise beat on specificity — leaving
10876
+ // the panel sitting in normal flow behind the page whenever it is shut.
10877
+ // The closing animation still runs: `open` is only removed after it ends.
10878
+ '&:not([open])': { display: 'none' },
10879
+ // `scrim: false` keeps the modality — focus trap, inert page — but stops
10880
+ // the drawer dimming what it covers.
10881
+ '&::backdrop': this.showScrim() ? { ...options.backdrop } : { background: 'transparent' },
10562
10882
  '&[open]': { animation: `${this.slideIn()} 250ms ease-out` },
10563
10883
  '&[closing]': { animation: `${this.slideOut()} 250ms ease-in` },
10564
10884
  });
10565
10885
  }, ...(ngDevMode ? [{ debugName: "overClass" }] : /* istanbul ignore next */ []));
10886
+ /** The only scrolling row, and the only padded one. */
10887
+ bodyClass = computed(() => css({
10888
+ flex: '1 1 auto',
10889
+ minHeight: 0,
10890
+ // Defence in depth: a positioned body is the containing block for any
10891
+ // stray absolute descendant, so nothing can re-home into an ancestor
10892
+ // and inflate its scrollHeight. Only safe because the shell above is
10893
+ // `overflow: clip` — on its own this would move the phantom overflow
10894
+ // into this scroller instead of out of the panel.
10895
+ position: 'relative',
10896
+ overflowX: 'hidden',
10897
+ overflowY: 'auto',
10898
+ overscrollBehavior: 'contain',
10899
+ ...this.theme.padding(this.componentOptions().padding),
10900
+ }), ...(ngDevMode ? [{ debugName: "bodyClass" }] : /* istanbul ignore next */ []));
10566
10901
  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>
10902
+ 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: [
10903
+ { provide: COMPONENT_NAME, useValue: 'drawer' },
10904
+ { provide: DRAWER_PANEL, useExisting: forwardRef(() => UniDrawerComponent) },
10905
+ ], 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: `
10906
+ <!-- One <ng-content> per slot, each parked in a template so both modes can
10907
+ render the same projected nodes. The catch-all is declared last so the
10908
+ two selective slots claim their content first. -->
10909
+ <ng-template #header>
10910
+ @if (headline()) {
10911
+ <div uni-drawer-header></div>
10912
+ }
10913
+ <ng-content select="[uni-drawer-header]" />
10914
+ </ng-template>
10915
+ <ng-template #footer>
10916
+ <ng-content select="[uni-drawer-buttons], [drawer-buttons]" />
10917
+ </ng-template>
10918
+ <ng-template #body><ng-content /></ng-template>
10919
+
10569
10920
  @if (mode() === 'side') {
10570
10921
  <aside [class]="sideClass()" [attr.aria-hidden]="open() ? null : 'true'">
10571
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
10922
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
10923
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
10924
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10572
10925
  </aside>
10573
10926
  } @else {
10574
10927
  <!-- Click handles the ::backdrop only (target check); keyboard closing
@@ -10578,28 +10931,49 @@ class UniDrawerComponent extends BaseComponent {
10578
10931
  <dialog
10579
10932
  #overlay
10580
10933
  [class]="overClass()"
10581
- [attr.aria-label]="ariaLabel()"
10934
+ [attr.aria-labelledby]="labelledBy()"
10935
+ [attr.aria-label]="labelledBy() ? null : ariaLabel()"
10582
10936
  (click)="onBackdropClick($event)"
10583
10937
  (cancel)="onCancel($event)"
10584
10938
  (animationend)="onAnimationEnd($event)"
10585
10939
  >
10586
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
10940
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
10941
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
10942
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10587
10943
  </dialog>
10588
10944
  }
10589
- `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10945
+ `, 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
10946
  }
10591
10947
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDrawerComponent, decorators: [{
10592
10948
  type: Component,
10593
10949
  args: [{
10594
10950
  changeDetection: ChangeDetectionStrategy.OnPush,
10595
10951
  selector: 'uni-drawer',
10596
- imports: [NgTemplateOutlet],
10597
- providers: [{ provide: COMPONENT_NAME, useValue: 'drawer' }],
10952
+ imports: [NgTemplateOutlet, UniDrawerHeaderComponent],
10953
+ providers: [
10954
+ { provide: COMPONENT_NAME, useValue: 'drawer' },
10955
+ { provide: DRAWER_PANEL, useExisting: forwardRef(() => UniDrawerComponent) },
10956
+ ],
10598
10957
  template: `
10599
- <ng-template #content><ng-content /></ng-template>
10958
+ <!-- One <ng-content> per slot, each parked in a template so both modes can
10959
+ render the same projected nodes. The catch-all is declared last so the
10960
+ two selective slots claim their content first. -->
10961
+ <ng-template #header>
10962
+ @if (headline()) {
10963
+ <div uni-drawer-header></div>
10964
+ }
10965
+ <ng-content select="[uni-drawer-header]" />
10966
+ </ng-template>
10967
+ <ng-template #footer>
10968
+ <ng-content select="[uni-drawer-buttons], [drawer-buttons]" />
10969
+ </ng-template>
10970
+ <ng-template #body><ng-content /></ng-template>
10971
+
10600
10972
  @if (mode() === 'side') {
10601
10973
  <aside [class]="sideClass()" [attr.aria-hidden]="open() ? null : 'true'">
10602
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
10974
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
10975
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
10976
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10603
10977
  </aside>
10604
10978
  } @else {
10605
10979
  <!-- Click handles the ::backdrop only (target check); keyboard closing
@@ -10609,17 +10983,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
10609
10983
  <dialog
10610
10984
  #overlay
10611
10985
  [class]="overClass()"
10612
- [attr.aria-label]="ariaLabel()"
10986
+ [attr.aria-labelledby]="labelledBy()"
10987
+ [attr.aria-label]="labelledBy() ? null : ariaLabel()"
10613
10988
  (click)="onBackdropClick($event)"
10614
10989
  (cancel)="onCancel($event)"
10615
10990
  (animationend)="onAnimationEnd($event)"
10616
10991
  >
10617
- <ng-container [ngTemplateOutlet]="contentTemplate()" />
10992
+ <ng-container [ngTemplateOutlet]="headerTemplate()" />
10993
+ <div [class]="bodyClass()"><ng-container [ngTemplateOutlet]="bodyTemplate()" /></div>
10994
+ <ng-container [ngTemplateOutlet]="footerTemplate()" />
10618
10995
  </dialog>
10619
10996
  }
10620
10997
  `,
10621
10998
  }]
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 }] }] } });
10999
+ }], 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
11000
 
10624
11001
  class UniExpandComponent extends BaseComponent {
10625
11002
  collapsed = model(true, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
@@ -14263,5 +14640,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
14263
14640
  * Generated bundle index. Do not edit.
14264
14641
  */
14265
14642
 
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 };
14643
+ 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
14644
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map