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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, linkedSignal, resource, Injectable, inject, DestroyRef, InjectionToken, input, ChangeDetectionStrategy, Component, ElementRef, Directive, model, afterRenderEffect, Renderer2, output, viewChild, ViewChild, viewChildren, contentChildren, effect, untracked, 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, 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';
@@ -367,6 +367,685 @@ const splitDateTime = (value) => {
367
367
  /** One combined value only when both parts are present. */
368
368
  const joinDateTime = (date, time) => date && time ? `${date}T${time}` : undefined;
369
369
 
370
+ /**
371
+ * Canonical numeric value shapes shared by `uni-number-input`,
372
+ * `uni-quantity-stepper`, `uni-number-range-input` and `uni-slider`.
373
+ *
374
+ * The components' internal source of truth is a **canonical decimal string** —
375
+ * optional sign, digits, an optional `.`, no grouping and no affix:
376
+ * `'-1234.56'`. The bound `number` is its projection, emitted on commit.
377
+ *
378
+ * Nothing numeric passes through a float, because floats give wrong answers to
379
+ * questions people ask of money: `0.1 + 0.2` is `0.30000000000000004`, and
380
+ * `(1.15).toFixed(1)` is `'1.1'` — 1.15 is really 1.1499999999999999, so the
381
+ * platform rounds a tie that isn't there. See `decimal.helper.ts`.
382
+ */
383
+
384
+ /** Canonical decimal, permitting a leading `+` and a bare `.5` / `5.` form. */
385
+ const CANONICAL = /^[+-]?(\d+(\.\d*)?|\.\d+)$/;
386
+ /** `1.5e-7`, `1e21` — what `String(number)` produces outside 1e-7…1e21. */
387
+ const EXPONENTIAL = /^([+-]?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/;
388
+ /** True when `text` is already a canonical decimal (leading/trailing space allowed). */
389
+ const isCanonicalDecimal = (text) => CANONICAL.test(text.trim());
390
+ /** Fraction-digit count. `'1.250'` → 3, `'12'` → 0, `'5.'` → 0. */
391
+ const decimalScale = (value) => {
392
+ const point = value.indexOf('.');
393
+ return point < 0 ? 0 : value.length - point - 1;
394
+ };
395
+ /**
396
+ * Canonical decimal → integer scaled by `10^scale`. Fraction digits beyond
397
+ * `scale` are truncated, so callers that must not lose them pass a `scale` at
398
+ * least `decimalScale(value)`.
399
+ */
400
+ const toScaled = (value, scale) => {
401
+ let text = value.trim();
402
+ const negative = text.startsWith('-');
403
+ if (negative || text.startsWith('+'))
404
+ text = text.slice(1);
405
+ const [integer, fraction = ''] = text.split('.');
406
+ const padded = (fraction + '0'.repeat(scale)).slice(0, scale);
407
+ const digits = (integer + padded).replace(/^0+(?=\d)/, '');
408
+ const scaled = BigInt(digits || '0');
409
+ return negative ? -scaled : scaled;
410
+ };
411
+ /** Scaled integer → canonical decimal, with trailing fraction zeros trimmed. */
412
+ const fromScaled = (scaled, scale) => {
413
+ const negative = scaled < 0n;
414
+ let digits = (negative ? -scaled : scaled).toString();
415
+ if (scale > 0) {
416
+ digits = digits.padStart(scale + 1, '0');
417
+ digits = `${digits.slice(0, -scale)}.${digits.slice(-scale)}`;
418
+ // `0+$` stops at the point, so '10.00' loses only its fraction zeros.
419
+ digits = digits.replace(/0+$/, '').replace(/\.$/, '');
420
+ }
421
+ if (digits === '' || digits === '0')
422
+ return '0';
423
+ return (negative ? '-' : '') + digits;
424
+ };
425
+ /**
426
+ * Strip a leading `+`, leading zeros and trailing fraction zeros: `'+01.50'`
427
+ * → `'1.5'`. Requires a canonical decimal; guard with `isCanonicalDecimal`.
428
+ */
429
+ const normalizeDecimal = (value) => {
430
+ const scale = decimalScale(value);
431
+ return fromScaled(toScaled(value, scale), scale);
432
+ };
433
+ /**
434
+ * Any numeric input → canonical decimal, expanding the exponential notation
435
+ * `String(number)` produces outside 1e-7…1e21. A `step` of `1e-7` would
436
+ * otherwise reach the arithmetic as the literal text `'1e-7'`.
437
+ *
438
+ * Throws on text that is not numeric at all — every caller here passes either
439
+ * a `number` input or text already cleared by the parser.
440
+ */
441
+ const toDecimal = (value) => {
442
+ const text = String(value).trim();
443
+ if (CANONICAL.test(text))
444
+ return normalizeDecimal(text);
445
+ const match = EXPONENTIAL.exec(text);
446
+ if (!match)
447
+ throw new RangeError(`Not a decimal number: ${JSON.stringify(text)}`);
448
+ const [, sign, integer, fraction = '', exponent] = match;
449
+ const digits = integer + fraction;
450
+ const point = integer.length + Number(exponent);
451
+ let expanded;
452
+ if (point <= 0)
453
+ expanded = `0.${'0'.repeat(-point)}${digits}`;
454
+ else if (point >= digits.length)
455
+ expanded = digits + '0'.repeat(point - digits.length);
456
+ else
457
+ expanded = `${digits.slice(0, point)}.${digits.slice(point)}`;
458
+ return normalizeDecimal((sign === '-' ? '-' : '') + expanded);
459
+ };
460
+ /** `-1` when `a < b`, `1` when `a > b`, `0` when equal. `'1.50'` equals `'1.5'`. */
461
+ const compareDecimal = (a, b) => {
462
+ const scale = Math.max(decimalScale(a), decimalScale(b));
463
+ const left = toScaled(a, scale);
464
+ const right = toScaled(b, scale);
465
+ return left < right ? -1 : left > right ? 1 : 0;
466
+ };
467
+ /**
468
+ * Round to `fractionDigits`, breaking ties per `mode`. Exact where
469
+ * `Number.prototype.toFixed` is not — see the file header.
470
+ */
471
+ const roundDecimal = (value, fractionDigits, mode = 'half-up') => {
472
+ const digits = Math.max(0, Math.trunc(fractionDigits));
473
+ const scale = decimalScale(value);
474
+ if (scale <= digits)
475
+ return normalizeDecimal(value);
476
+ const scaled = toScaled(value, scale);
477
+ const divisor = 10n ** BigInt(scale - digits);
478
+ let quotient = scaled / divisor; // BigInt division truncates toward zero
479
+ const remainder = scaled % divisor;
480
+ if (remainder === 0n)
481
+ return fromScaled(quotient, digits);
482
+ const negative = scaled < 0n;
483
+ const twiceRemainder = (remainder < 0n ? -remainder : remainder) * 2n;
484
+ const away = () => {
485
+ quotient += negative ? -1n : 1n;
486
+ };
487
+ switch (mode) {
488
+ case 'trunc':
489
+ break;
490
+ case 'ceil':
491
+ if (!negative)
492
+ quotient += 1n;
493
+ break;
494
+ case 'floor':
495
+ if (negative)
496
+ quotient -= 1n;
497
+ break;
498
+ case 'half-even':
499
+ if (twiceRemainder > divisor || (twiceRemainder === divisor && quotient % 2n !== 0n))
500
+ away();
501
+ break;
502
+ default: // half-up — a tie goes away from zero
503
+ if (twiceRemainder >= divisor)
504
+ away();
505
+ }
506
+ return fromScaled(quotient, digits);
507
+ };
508
+ /**
509
+ * Multiply by `10^places`, exactly. Used for the percent preset's
510
+ * fraction ⇄ display shift (`0.15` ⇄ `15`) and for deriving a default
511
+ * large step of `step × 10` without touching a float.
512
+ */
513
+ const shiftDecimal = (value, places) => {
514
+ const currentScale = decimalScale(value);
515
+ let scaled = toScaled(value, currentScale);
516
+ let scale = currentScale - places;
517
+ if (scale < 0) {
518
+ scaled *= 10n ** BigInt(-scale);
519
+ scale = 0;
520
+ }
521
+ return fromScaled(scaled, scale);
522
+ };
523
+ /**
524
+ * Hold a value inside its fences, reporting which one it hit so the caller can
525
+ * announce it. Clamping belongs on commit, never per keystroke: a `min=10`
526
+ * field that clamps live can never be typed into, because the `1` becomes `10`
527
+ * before the `5` arrives.
528
+ */
529
+ const clampDecimal = (value, min, max) => {
530
+ if (min != null && compareDecimal(value, toDecimal(min)) < 0) {
531
+ return { value: toDecimal(min), hit: 'min' };
532
+ }
533
+ if (max != null && compareDecimal(value, toDecimal(max)) > 0) {
534
+ return { value: toDecimal(max), hit: 'max' };
535
+ }
536
+ return { value, hit: null };
537
+ };
538
+ /**
539
+ * One step from `current`, in `direction` (`1` up, `-1` down).
540
+ *
541
+ * Steps land on the grid `origin + n · step`, where `origin` is `min` by
542
+ * default. A value that is *off* the grid snaps to the nearest grid point **in
543
+ * the direction of travel** rather than jumping past it: with `min=5, step=10`
544
+ * the grid is 5, 15, 25, and stepping up from 7 gives 15, not 17.
545
+ *
546
+ * Fences stop the value; they never wrap unless `wrap` is set and both bounds
547
+ * are defined. Returns `current` unchanged when `step` is zero.
548
+ */
549
+ const stepDecimal = (current, direction, config = {}) => {
550
+ const step = toDecimal(config.step ?? 1);
551
+ const origin = config.stepOrigin === 'zero' || config.min == null ? '0' : toDecimal(config.min);
552
+ // One shared scale keeps every term an exact integer.
553
+ const scale = Math.max(decimalScale(current), decimalScale(step), decimalScale(origin), config.min == null ? 0 : decimalScale(toDecimal(config.min)), config.max == null ? 0 : decimalScale(toDecimal(config.max)));
554
+ const stepBy = toScaled(step, scale);
555
+ if (stepBy === 0n)
556
+ return current;
557
+ const value = toScaled(current, scale);
558
+ const anchor = toScaled(origin, scale);
559
+ let offset = (value - anchor) % stepBy;
560
+ if (offset < 0n)
561
+ offset += stepBy;
562
+ let next;
563
+ if (offset === 0n)
564
+ next = value + (direction > 0 ? stepBy : -stepBy);
565
+ else
566
+ next = direction > 0 ? value + (stepBy - offset) : value - offset;
567
+ const min = config.min == null ? null : toScaled(toDecimal(config.min), scale);
568
+ const max = config.max == null ? null : toScaled(toDecimal(config.max), scale);
569
+ if (config.wrap && min != null && max != null) {
570
+ // The cycle includes one step past `max` so 23 → 0 rather than 23 → 23.
571
+ const span = max - min + stepBy;
572
+ let position = (next - min) % span;
573
+ if (position < 0n)
574
+ position += span;
575
+ next = min + position;
576
+ }
577
+ else {
578
+ if (max != null && next > max)
579
+ next = max;
580
+ if (min != null && next < min)
581
+ next = min;
582
+ }
583
+ return fromScaled(next, scale);
584
+ };
585
+
586
+ /**
587
+ * Locale-aware number parsing and formatting, on top of the exact arithmetic
588
+ * in `decimal.helper.ts`. `Intl.NumberFormat` supplies both directions —
589
+ * separators, currency placement and digit systems all come from the locale
590
+ * and none of them is hardcoded per language. No number library.
591
+ *
592
+ * The reason this exists rather than `<input type="number">`: per the HTML
593
+ * value sanitization algorithm, a number input whose text is not a valid
594
+ * floating-point number reports `value === ''`. Type `12,50` as most of Europe
595
+ * does, or paste `1,234.56` from a spreadsheet, and the app reads an empty
596
+ * field with no way to tell that from a blank one.
597
+ */
598
+ /** Preset defaults. `null` decimals means "ask `Intl` about the currency". */
599
+ const PRESETS$1 = {
600
+ decimal: { decimals: [0, 3], grouping: 'min2', inputMode: 'decimal' },
601
+ integer: { decimals: [0, 0], grouping: 'min2', inputMode: 'numeric' },
602
+ currency: { decimals: null, grouping: 'always', inputMode: 'decimal' },
603
+ percent: { decimals: [0, 2], grouping: 'min2', inputMode: 'decimal' },
604
+ };
605
+ /**
606
+ * First code point of each localized digit run we map back to ASCII:
607
+ * Arabic-Indic, Extended Arabic-Indic (Persian/Urdu), Devanagari, Bengali,
608
+ * Thai. Anything outside these still parses in its ASCII form.
609
+ */
610
+ const DIGIT_ZEROS = [0x0660, 0x06f0, 0x0966, 0x09e6, 0x0e50];
611
+ /** Arabic decimal separator and thousands separator. */
612
+ const ARABIC_DECIMAL = '٫';
613
+ const ARABIC_GROUP = '٬';
614
+ /**
615
+ * Locale separators plus, when a currency is given, its symbol, side and
616
+ * fraction digits. Memoized: constructing an `Intl.NumberFormat` is expensive
617
+ * and a field re-resolves this on every keystroke.
618
+ */
619
+ const localeNumberParts = memoize((locale, currency) => {
620
+ const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
621
+ const group = parts.find((part) => part.type === 'group')?.value ?? ',';
622
+ const decimal = parts.find((part) => part.type === 'decimal')?.value ?? '.';
623
+ if (!currency) {
624
+ return {
625
+ group,
626
+ decimal,
627
+ currencySymbol: '',
628
+ currencyLeading: true,
629
+ currencyDecimals: 2,
630
+ };
631
+ }
632
+ const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
633
+ const currencyParts = formatter.formatToParts(1);
634
+ const symbolIndex = currencyParts.findIndex((part) => part.type === 'currency');
635
+ const integerIndex = currencyParts.findIndex((part) => part.type === 'integer');
636
+ return {
637
+ group,
638
+ decimal,
639
+ currencySymbol: currencyParts[symbolIndex]?.value ?? '',
640
+ currencyLeading: symbolIndex < integerIndex,
641
+ currencyDecimals: formatter.resolvedOptions().maximumFractionDigits ?? 2,
642
+ };
643
+ });
644
+ /**
645
+ * Map localized digits and Arabic separators to ASCII, so `١٢٣٤٫٥` parses in
646
+ * `ar` and `१२३४.५` in `hi`.
647
+ */
648
+ const toAsciiDigits = (text) => {
649
+ let out = '';
650
+ for (const char of text) {
651
+ const code = char.codePointAt(0) ?? 0;
652
+ const zero = DIGIT_ZEROS.find((start) => code >= start && code <= start + 9);
653
+ if (zero != null)
654
+ out += String(code - zero);
655
+ else if (char === ARABIC_DECIMAL)
656
+ out += '.';
657
+ else if (char === ARABIC_GROUP)
658
+ continue;
659
+ else
660
+ out += char;
661
+ }
662
+ return out;
663
+ };
664
+ /**
665
+ * Evaluate `+ − × ÷ ( )` over decimal literals — shunting-yard, roughly thirty
666
+ * lines, and **never `eval`**. Returns a canonical decimal, or `null` when the
667
+ * text is not a well-formed expression.
668
+ *
669
+ * Floats are acceptable here in a way they are not elsewhere: this is a
670
+ * convenience path for spreadsheet muscle memory (`12*3`, `100/4+5`), and the
671
+ * result is settled to ten decimals before re-entering exact arithmetic.
672
+ * Division is the only operation that can produce a non-terminating decimal,
673
+ * and no exact representation would help there either.
674
+ */
675
+ const evaluateExpression = (text) => {
676
+ const source = text.replace(/×/g, '*').replace(/÷/g, '/').replace(/−/g, '-');
677
+ if (!/^[\d.\s+\-*/()]+$/.test(source))
678
+ return null;
679
+ // A bare number is not an expression — it belongs on the ordinary path.
680
+ if (!/[\d)]\s*[+\-*/]\s*[\d(.]/.test(source) && !source.includes('('))
681
+ return null;
682
+ const tokens = source.match(/\d+\.?\d*|\.\d+|[+\-*/()]/g);
683
+ if (!tokens)
684
+ return null;
685
+ const precedence = { '+': 1, '-': 1, '*': 2, '/': 2 };
686
+ const output = [];
687
+ const operators = [];
688
+ let previous = null;
689
+ for (const token of tokens) {
690
+ if (/^[\d.]/.test(token)) {
691
+ output.push(Number(token));
692
+ }
693
+ else if (token === '(') {
694
+ operators.push(token);
695
+ }
696
+ else if (token === ')') {
697
+ while (operators.length && operators[operators.length - 1] !== '(') {
698
+ output.push(operators.pop());
699
+ }
700
+ if (!operators.length)
701
+ return null;
702
+ operators.pop();
703
+ }
704
+ else {
705
+ // Unary minus: push an implicit 0 so `-3` and `(2+-3)` both work.
706
+ if (token === '-' && (previous === null || previous in precedence || previous === '(')) {
707
+ output.push(0);
708
+ }
709
+ while (operators.length &&
710
+ precedence[operators[operators.length - 1]] >= precedence[token]) {
711
+ output.push(operators.pop());
712
+ }
713
+ operators.push(token);
714
+ }
715
+ previous = token;
716
+ }
717
+ while (operators.length) {
718
+ const operator = operators.pop();
719
+ if (operator === '(')
720
+ return null;
721
+ output.push(operator);
722
+ }
723
+ const stack = [];
724
+ for (const token of output) {
725
+ if (typeof token === 'number') {
726
+ stack.push(token);
727
+ continue;
728
+ }
729
+ const right = stack.pop();
730
+ const left = stack.pop();
731
+ if (left === undefined || right === undefined)
732
+ return null;
733
+ stack.push(token === '+'
734
+ ? left + right
735
+ : token === '-'
736
+ ? left - right
737
+ : token === '*'
738
+ ? left * right
739
+ : left / right);
740
+ }
741
+ if (stack.length !== 1 || !Number.isFinite(stack[0]))
742
+ return null;
743
+ return normalizeDecimal(Number(stack[0].toFixed(10)).toString());
744
+ };
745
+ /** Fill in every preset, locale and currency default. */
746
+ const resolveNumberFormat = (config = {}) => {
747
+ const presetName = config.currency ? 'currency' : (config.preset ?? 'decimal');
748
+ const preset = PRESETS$1[presetName];
749
+ const locale = config.locale || 'en-US';
750
+ const parts = localeNumberParts(locale, config.currency);
751
+ const [minimumFractionDigits, maximumFractionDigits] = Array.isArray(config.decimals)
752
+ ? config.decimals
753
+ : config.decimals != null
754
+ ? [config.decimals, config.decimals]
755
+ : (preset.decimals ?? [parts.currencyDecimals, parts.currencyDecimals]);
756
+ const isPercent = presetName === 'percent';
757
+ const isInteger = presetName === 'integer';
758
+ // Adornments live outside the editable text, so the caret never walks over
759
+ // them and `prefix`/`suffix` can be any string without becoming parseable.
760
+ const prefix = config.prefix || (config.currency && parts.currencyLeading ? parts.currencySymbol : '');
761
+ const suffix = config.suffix ||
762
+ (config.currency && !parts.currencyLeading ? parts.currencySymbol : '') ||
763
+ (isPercent ? '%' : '');
764
+ return {
765
+ locale,
766
+ parts,
767
+ prefix,
768
+ suffix,
769
+ minimumFractionDigits,
770
+ maximumFractionDigits,
771
+ grouping: config.grouping !== undefined ? config.grouping : preset.grouping,
772
+ compact: config.numberFormat?.notation === 'compact',
773
+ isInteger,
774
+ shift: config.valueIsFraction ? 2 : 0,
775
+ roundingMode: config.roundingMode ?? 'half-up',
776
+ inputMode: isInteger && (config.min == null || config.min < 0) ? 'decimal' : preset.inputMode,
777
+ unitAnnouncement: config.unitAnnouncement,
778
+ };
779
+ };
780
+ /**
781
+ * Read a user's text into a canonical decimal in **model units**.
782
+ *
783
+ * Accepted, in order: canonical/ASCII (always, whatever the locale — it is
784
+ * what agents and APIs write), locale-grouped, affixed, localized digits,
785
+ * compact (`1.5k`), accounting negatives (`(1,234.56)` → `-1234.56`), and
786
+ * expressions when `allowExpressions` is set.
787
+ */
788
+ const parseNumber = (raw, format, options = {}) => {
789
+ const original = toAsciiDigits(String(raw)).trim();
790
+ if (original === '')
791
+ return { status: 'empty' };
792
+ let text = original;
793
+ const { prefix, suffix, parts } = format;
794
+ // People paste from spreadsheets: strip this field's own affixes, the
795
+ // currency symbol and code, and any stray percent sign.
796
+ if (prefix)
797
+ text = text.split(prefix).join('');
798
+ if (suffix)
799
+ text = text.split(suffix).join('');
800
+ if (options.currency) {
801
+ text = text.split(parts.currencySymbol).join('');
802
+ text = text.replace(new RegExp(options.currency, 'i'), '');
803
+ }
804
+ text = text.replace(/%/g, '');
805
+ // `\s` covers NBSP and the narrow/thin spaces a French copy-paste carries.
806
+ text = text.replace(/\s/g, '');
807
+ if (text === '')
808
+ return { status: 'error', reason: 'unparseable' };
809
+ // Finance types parentheses for a negative; refusing them is a papercut.
810
+ let negative = false;
811
+ const accounting = /^\((.+)\)$/.exec(text);
812
+ if (accounting) {
813
+ negative = true;
814
+ text = accounting[1];
815
+ }
816
+ let magnitude = 0;
817
+ if (format.compact) {
818
+ const compact = /^(.+?)([kKmMbB])$/.exec(text);
819
+ if (compact) {
820
+ text = compact[1];
821
+ magnitude = { k: 3, m: 6, b: 9 }[compact[2].toLowerCase()];
822
+ }
823
+ }
824
+ let canonical = null;
825
+ if (isCanonicalDecimal(text)) {
826
+ canonical = text;
827
+ }
828
+ else {
829
+ const degrouped = text.split(parts.group).join('').split(parts.decimal).join('.');
830
+ if (isCanonicalDecimal(degrouped)) {
831
+ canonical = degrouped;
832
+ }
833
+ else if (options.allowExpressions) {
834
+ // Evaluate the ORIGINAL text: parentheses here are grouping, not the
835
+ // accounting negative stripped above.
836
+ const evaluated = evaluateExpression(original);
837
+ if (evaluated != null) {
838
+ return { status: 'ok', value: shiftDecimal(evaluated, -format.shift), viaExpression: true };
839
+ }
840
+ }
841
+ }
842
+ if (canonical == null)
843
+ return { status: 'error', reason: 'unparseable' };
844
+ canonical = normalizeDecimal(canonical);
845
+ if (magnitude)
846
+ canonical = shiftDecimal(canonical, magnitude);
847
+ if (negative && !canonical.startsWith('-') && canonical !== '0')
848
+ canonical = `-${canonical}`;
849
+ if (format.isInteger && decimalScale(canonical) > 0) {
850
+ return { status: 'error', reason: 'not-integer' };
851
+ }
852
+ return { status: 'ok', value: shiftDecimal(canonical, -format.shift), viaExpression: false };
853
+ };
854
+ /** Insert the locale's group separator every three integer digits. */
855
+ const applyGrouping = (integer, separator) => integer.replace(/\B(?=(\d{3})+(?!\d))/g, () => separator);
856
+ /**
857
+ * Canonical decimal (model units) → the display number, without affixes.
858
+ *
859
+ * `min2` grouping — the default — starts at five integer digits, so a year
860
+ * renders `2026` rather than `2,026` while a price still renders `10,000`.
861
+ */
862
+ const formatNumber = (canonical, format) => {
863
+ const display = shiftDecimal(canonical, format.shift);
864
+ if (format.compact) {
865
+ return new Intl.NumberFormat(format.locale, {
866
+ notation: 'compact',
867
+ maximumFractionDigits: 1,
868
+ }).format(Number(display));
869
+ }
870
+ const rounded = roundDecimal(display, format.maximumFractionDigits, format.roundingMode);
871
+ const negative = rounded.startsWith('-');
872
+ const [integerPart, fractionPart = ''] = (negative ? rounded.slice(1) : rounded).split('.');
873
+ const fraction = fractionPart.padEnd(format.minimumFractionDigits, '0');
874
+ const grouped = format.grouping === false
875
+ ? integerPart
876
+ : format.grouping === 'min2'
877
+ ? integerPart.length > 4
878
+ ? applyGrouping(integerPart, format.parts.group)
879
+ : integerPart
880
+ : integerPart.length > 3
881
+ ? applyGrouping(integerPart, format.parts.group)
882
+ : integerPart;
883
+ return ((negative ? '-' : '') + grouped + (fraction ? format.parts.decimal + fraction : ''));
884
+ };
885
+ /**
886
+ * The plain text the field shows while focused: the display number with no
887
+ * grouping and no affixes, so the caret never has to walk over a separator
888
+ * that appears and vanishes mid-word.
889
+ */
890
+ const rawNumberText = (canonical, format) => roundDecimal(shiftDecimal(canonical, format.shift), format.maximumFractionDigits, format.roundingMode);
891
+ /** Round a committed value to the field's precision, in model units. */
892
+ const settleNumber = (canonical, format) => shiftDecimal(rawNumberText(canonical, format), -format.shift);
893
+ /**
894
+ * The `aria-valuetext` string: the formatted number with its affixes spoken.
895
+ * `aria-valuenow` alone announces "1234.56", which is the one thing about a
896
+ * money field that is not the point. An empty field says "Empty" per APG.
897
+ */
898
+ const speakNumber = (canonical, format, emptyText = 'Empty') => {
899
+ if (canonical == null)
900
+ return emptyText;
901
+ const number = formatNumber(canonical, format);
902
+ const unit = format.unitAnnouncement || format.suffix;
903
+ const spoken = unit ? (unit === '%' ? ' percent' : ` ${unit}`) : '';
904
+ return format.prefix + number + spoken;
905
+ };
906
+ /** Model-units canonical decimal → the bound `number`. */
907
+ const toNumber = (canonical) => Number(canonical);
908
+ /**
909
+ * True when a value cannot survive the trip through `number` — the reason the
910
+ * components also expose an exact `valueAsString` model, and the trigger for
911
+ * the dev-mode warning. Silent precision loss is the whole point of that
912
+ * second model, so it is worth saying out loud once.
913
+ *
914
+ * A `number` can only be checked for magnitude, since it has already lost
915
+ * whatever it was going to lose. A canonical string can be checked properly:
916
+ * `'9007199254740993'` comes back as `'9007199254740992'`.
917
+ */
918
+ const losesPrecision = (value) => {
919
+ if (typeof value === 'number') {
920
+ return !Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER;
921
+ }
922
+ const canonical = toDecimal(value);
923
+ const projected = Number(canonical);
924
+ return !Number.isFinite(projected) || String(projected) !== canonical;
925
+ };
926
+
927
+ /**
928
+ * Hold-to-repeat for stepper buttons: press once to step once, hold to keep
929
+ * stepping, faster the longer you hold. Getting a quantity from 1 to 200 is
930
+ * otherwise 199 clicks.
931
+ *
932
+ * Like the other cdk helpers this owns **no DOM and attaches no listeners to
933
+ * an element** — the component's template hands it the events, which keeps the
934
+ * ARIA and the markup where they belong:
935
+ *
936
+ * ```html
937
+ * <button
938
+ * type="button"
939
+ * tabindex="-1"
940
+ * [disabled]="atMax()"
941
+ * (pointerdown)="increment.press($event)"
942
+ * (pointerup)="increment.release()"
943
+ * (pointercancel)="increment.cancel()"
944
+ * (lostpointercapture)="increment.release()"
945
+ * >
946
+ * ```
947
+ *
948
+ * It does register one `window` blur listener, because a hold that survives
949
+ * the window losing focus is a value that keeps climbing while the user is
950
+ * somewhere else. That listener is torn down with the injection context, so
951
+ * `createPressRepeat` must be called from one — a field initializer, as with
952
+ * `useTimer()`.
953
+ */
954
+ /** Milliseconds spent interpolating from `intervalMs` down to `fastIntervalMs`. */
955
+ const RAMP_WINDOW_MS = 500;
956
+ const DEFAULTS = {
957
+ /** Held this long before repeating starts, so a normal click steps once. */
958
+ delayMs: 500,
959
+ /** Repeat period once it starts — 10 steps a second. */
960
+ intervalMs: 100,
961
+ /** Repeat period at full speed — 40 steps a second. */
962
+ fastIntervalMs: 25,
963
+ /** Held this long before the acceleration begins. */
964
+ rampMs: 2000,
965
+ };
966
+ function createPressRepeat(config) {
967
+ const destroyRef = inject(DestroyRef);
968
+ const holding = signal(false, ...(ngDevMode ? [{ debugName: "holding" }] : /* istanbul ignore next */ []));
969
+ let timer = null;
970
+ let startedAt = 0;
971
+ let repeated = false;
972
+ const timing = () => ({ ...DEFAULTS, ...config.timing?.() });
973
+ /** Linear ramp: flat until `rampMs`, then down to `fastIntervalMs`. */
974
+ const intervalFor = (elapsed) => {
975
+ const { intervalMs, fastIntervalMs, rampMs } = timing();
976
+ if (elapsed <= rampMs)
977
+ return intervalMs;
978
+ const progress = Math.min(1, (elapsed - rampMs) / RAMP_WINDOW_MS);
979
+ return intervalMs + (fastIntervalMs - intervalMs) * progress;
980
+ };
981
+ const stopTimer = () => {
982
+ if (timer != null)
983
+ clearTimeout(timer);
984
+ timer = null;
985
+ };
986
+ const tick = () => {
987
+ repeated = true;
988
+ config.onStep(true);
989
+ // Re-armed rather than set on an interval, so the period can shorten
990
+ // between ticks as the hold accelerates.
991
+ timer = setTimeout(tick, intervalFor(Date.now() - startedAt));
992
+ };
993
+ const end = (notify) => {
994
+ stopTimer();
995
+ if (!holding())
996
+ return;
997
+ holding.set(false);
998
+ const didRepeat = repeated;
999
+ repeated = false;
1000
+ startedAt = 0;
1001
+ if (notify)
1002
+ config.onRelease?.(didRepeat);
1003
+ };
1004
+ const onWindowBlur = () => end(false);
1005
+ window.addEventListener('blur', onWindowBlur);
1006
+ destroyRef.onDestroy(() => {
1007
+ stopTimer();
1008
+ window.removeEventListener('blur', onWindowBlur);
1009
+ });
1010
+ return {
1011
+ holding: holding.asReadonly(),
1012
+ press(event) {
1013
+ if (config.disabled?.())
1014
+ return;
1015
+ if (holding())
1016
+ return;
1017
+ if (event) {
1018
+ // Keeps the pointer stream on the button even when the finger slides
1019
+ // off it, so `pointerup` still arrives and the run still ends.
1020
+ event.preventDefault();
1021
+ const target = event.currentTarget;
1022
+ // jsdom and older engines lack the method entirely.
1023
+ if (target instanceof Element && typeof target.setPointerCapture === 'function') {
1024
+ try {
1025
+ target.setPointerCapture(event.pointerId);
1026
+ }
1027
+ catch {
1028
+ // A synthetic or already-released pointer id; the run is still fine.
1029
+ }
1030
+ }
1031
+ }
1032
+ holding.set(true);
1033
+ repeated = false;
1034
+ startedAt = Date.now();
1035
+ config.onStep(false);
1036
+ if (config.repeat?.() === false)
1037
+ return;
1038
+ timer = setTimeout(tick, timing().delayMs);
1039
+ },
1040
+ release() {
1041
+ end(true);
1042
+ },
1043
+ cancel() {
1044
+ end(false);
1045
+ },
1046
+ };
1047
+ }
1048
+
370
1049
  /**
371
1050
  * The keyboard and ARIA bookkeeping shared by every combobox-style popup:
372
1051
  * open state, the active option index, and the `aria-activedescendant` id
@@ -2777,6 +3456,20 @@ class UniInputBoxComponent extends BaseComponent {
2777
3456
  width = input(undefined, ...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
2778
3457
  fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth" }] : /* istanbul ignore next */ []));
2779
3458
  grow = input(undefined, ...(ngDevMode ? [{ debugName: "grow" }] : /* istanbul ignore next */ []));
3459
+ /**
3460
+ * Stop applying the themed leading inset to the inner control, for fields
3461
+ * that place it themselves.
3462
+ *
3463
+ * The inset normally rides the `<input>`, which is right while the text is
3464
+ * the field's leading edge. It is wrong the moment an adornment sits in
3465
+ * front: a currency prefix would hug the border while the number it belongs
3466
+ * to is indented past it. A field with adornments takes the inset over and
3467
+ * puts it on whichever element is actually first.
3468
+ */
3469
+ managedInset = input(false, ...(ngDevMode ? [{ debugName: "managedInset" }] : /* istanbul ignore next */ []));
3470
+ /** Auto-height fields (tag input, textarea) still keep the themed height as
3471
+ a floor, so a single-line field lines up with every other input. */
3472
+ minHeight = computed(() => this.height() === 'auto' ? this.componentOptions().height : undefined, ...(ngDevMode ? [{ debugName: "minHeight" }] : /* istanbul ignore next */ []));
2780
3473
  color = computed(() => this.error() ? this.componentOptions().errorColor : this.componentOptions().color, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
2781
3474
  border = computed(() => this.error() ? this.componentOptions().errorBorder : this.componentOptions().border, ...(ngDevMode ? [{ debugName: "border" }] : /* istanbul ignore next */ []));
2782
3475
  shadow = computed(() => this.error() ? this.componentOptions().errorShadow : this.componentOptions().shadow, ...(ngDevMode ? [{ debugName: "shadow" }] : /* istanbul ignore next */ []));
@@ -2790,7 +3483,9 @@ class UniInputBoxComponent extends BaseComponent {
2790
3483
  '& input, select, textarea': {
2791
3484
  ...removeInputPlatformStyling,
2792
3485
  height: '100%',
2793
- ...this.theme.paddingLeft(this.componentOptions().paddingLeft),
3486
+ ...(this.managedInset()
3487
+ ? undefined
3488
+ : this.theme.paddingLeft(this.componentOptions().paddingLeft)),
2794
3489
  ...this.theme.color(this.componentOptions().textColor),
2795
3490
  ...this.theme.typeface(this.componentOptions().typeface),
2796
3491
  },
@@ -2824,12 +3519,12 @@ class UniInputBoxComponent extends BaseComponent {
2824
3519
  },
2825
3520
  ]), ...(ngDevMode ? [{ debugName: "inputBoxClass" }] : /* istanbul ignore next */ []));
2826
3521
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2827
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [containerColor]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [width]=\"width()\"\n [fullWidth]=\"fullWidth()\"\n [grow]=\"grow()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3522
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputBoxComponent, isStandalone: true, selector: "uni-input-box", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null }, managedInset: { classPropertyName: "managedInset", publicName: "managedInset", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], usesInheritance: true, ngImport: i0, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [minHeight]=\"minHeight()\"\n [containerColor]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [width]=\"width()\"\n [fullWidth]=\"fullWidth()\"\n [grow]=\"grow()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2828
3523
  }
2829
3524
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputBoxComponent, decorators: [{
2830
3525
  type: Component,
2831
- args: [{ selector: 'uni-input-box', imports: [UniRowDirective], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [containerColor]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [width]=\"width()\"\n [fullWidth]=\"fullWidth()\"\n [grow]=\"grow()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
2832
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], 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 }] }] } });
3526
+ args: [{ selector: 'uni-input-box', imports: [UniRowDirective], providers: [{ provide: COMPONENT_NAME, useValue: 'input' }], host: { '[class]': 'className' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n row-layout\n alignItems=\"center\"\n [height]=\"height() ?? componentOptions().height\"\n [minHeight]=\"minHeight()\"\n [containerColor]=\"color()\"\n [border]=\"border()\"\n [borderRadius]=\"componentOptions().borderRadius\"\n [shadow]=\"shadow()\"\n [minWidth]=\"minWidth()\"\n [width]=\"width()\"\n [fullWidth]=\"fullWidth()\"\n [grow]=\"grow()\"\n [class]=\"inputBoxClass()\"\n position=\"relative\"\n>\n <ng-content></ng-content>\n</div>\n" }]
3527
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], 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 }] }], managedInset: [{ type: i0.Input, args: [{ isSignal: true, alias: "managedInset", required: false }] }] } });
2833
3528
 
2834
3529
  /**
2835
3530
  * Form-bound, closed-set, single-select autocomplete: `FormValueControl<T | null>`
@@ -3240,7 +3935,7 @@ class UniComboboxComponent extends BaseComponent {
3240
3935
  ]);
3241
3936
  }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
3242
3937
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniComboboxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3243
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniComboboxComponent, isStandalone: true, selector: "uni-combobox, Combobox", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, filterLocally: { classPropertyName: "filterLocally", publicName: "filterLocally", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", selected: "selected", cleared: "cleared", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'combobox' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n resolves only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\">\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [value]=\"displayValue()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"popupOpen()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput()\"\n (click)=\"onFieldClick()\"\n />\n @if (clearable() && value() !== null && !disabled()) {\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [iconName]=\"componentOptions().clearIcon ?? 'close'\"\n (click)=\"clear()\"\n >\n Clear {{ label() }}\n </button>\n }\n <!-- Pointer-only: keyboard has ArrowDown, the input announces expanded. -->\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [class]=\"toggleClass()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"onToggleMousedown($event)\"\n >\n <uni-icon [name]=\"componentOptions().toggleIcon ?? 'chevronDown'\" size=\"20\" />\n </button>\n </div>\n </uni-input-box>\n\n @if (popupOpen()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @if (filteredIndices().length === 0) {\n <!-- Non-interactive: not an option, not navigable, no id. -->\n <li class=\"empty\">{{ emptyText() }}</li>\n } @else {\n @for (optIndex of filteredIndices(); track optIndex; let i = $index) {\n <!-- aria-selected marks the *committed* option; the active one is\n carried by aria-activedescendant \u2014 they are different facts. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"optIndex === committedIndex()\"\n [attr.aria-disabled]=\"options()[optIndex].disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(optIndex)\"\n >\n <span class=\"check\" aria-hidden=\"true\">\n @if (optIndex === committedIndex()) {\n <uni-icon [name]=\"componentOptions().selectedIcon ?? 'check'\" size=\"18\" />\n }\n </span>\n <span class=\"text\">\n <span class=\"option-label\">{{ options()[optIndex].label }}</span>\n @if (options()[optIndex].description) {\n <span class=\"desc\">{{ options()[optIndex].description }}</span>\n }\n </span>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Commits, clears, refusals and result counts are otherwise silent. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3938
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniComboboxComponent, isStandalone: true, selector: "uni-combobox, Combobox", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null }, filterLocally: { classPropertyName: "filterLocally", publicName: "filterLocally", isSignal: true, isRequired: false, transformFunction: null }, filterWith: { classPropertyName: "filterWith", publicName: "filterWith", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", selected: "selected", cleared: "cleared", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'combobox' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n resolves only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\">\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [value]=\"displayValue()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"popupOpen()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onKeydown($event)\"\n (input)=\"onInput()\"\n (click)=\"onFieldClick()\"\n />\n @if (clearable() && value() !== null && !disabled()) {\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n [iconName]=\"componentOptions().clearIcon ?? 'close'\"\n (click)=\"clear()\"\n >\n Clear {{ label() }}\n </button>\n }\n <!-- Pointer-only: keyboard has ArrowDown, the input announces expanded. -->\n <button\n type=\"button\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [class]=\"toggleClass()\"\n [disabled]=\"disabled()\"\n (mousedown)=\"onToggleMousedown($event)\"\n >\n <uni-icon [name]=\"componentOptions().toggleIcon ?? 'chevronDown'\" size=\"20\" />\n </button>\n </div>\n </uni-input-box>\n\n @if (popupOpen()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @if (filteredIndices().length === 0) {\n <!-- Non-interactive: not an option, not navigable, no id. -->\n <li class=\"empty\">{{ emptyText() }}</li>\n } @else {\n @for (optIndex of filteredIndices(); track optIndex; let i = $index) {\n <!-- aria-selected marks the *committed* option; the active one is\n carried by aria-activedescendant \u2014 they are different facts. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"optIndex === committedIndex()\"\n [attr.aria-disabled]=\"options()[optIndex].disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(optIndex)\"\n >\n <span class=\"check\" aria-hidden=\"true\">\n @if (optIndex === committedIndex()) {\n <uni-icon [name]=\"componentOptions().selectedIcon ?? 'check'\" size=\"18\" />\n }\n </span>\n <span class=\"text\">\n <span class=\"option-label\">{{ options()[optIndex].label }}</span>\n @if (options()[optIndex].description) {\n <span class=\"desc\">{{ options()[optIndex].description }}</span>\n }\n </span>\n </li>\n }\n }\n </ul>\n }\n\n <!-- Commits, clears, refusals and result counts are otherwise silent. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3244
3939
  }
3245
3940
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniComboboxComponent, decorators: [{
3246
3941
  type: Component,
@@ -4145,7 +4840,7 @@ class UniDateInputComponent extends BaseComponent {
4145
4840
  ]);
4146
4841
  }, ...(ngDevMode ? [{ debugName: "embeddedClass" }] : /* istanbul ignore next */ []));
4147
4842
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4148
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDateInputComponent, isStandalone: true, selector: "uni-date-input, DateInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", opened: "opened", closed: "closed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "toggleRef", first: true, predicate: ["toggle"], descendants: true, read: ElementRef, isSignal: true }, { propertyName: "popupRef", first: true, predicate: ["popupDialog"], descendants: true, isSignal: true }, { propertyName: "dropdown", first: true, predicate: UniDropdownComponent, descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: UniCalendarComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so commits happen only when focus\n leaves the whole field, not while moving into the popup. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <!-- The dropdown wires aria-haspopup/aria-expanded/aria-controls onto\n this button; its name tracks the value (\"Change date, \u2026\"). -->\n <button\n #toggle\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'calendar_month'\"\n [disable]=\"disabled()\"\n >\n {{ toggleLabel() }}\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (toggleElement()) {\n <uni-dropdown\n [trigger]=\"toggleElement()!\"\n ariaHasPopup=\"dialog\"\n placement=\"bottom-end\"\n [containerColor]=\"componentOptions().popupColor ?? 'primary-surface'\"\n [shadow]=\"componentOptions().popupShadow ?? 'menu'\"\n [borderRadius]=\"componentOptions().popupBorderRadius ?? 'xs'\"\n paddingVertical=\"xs\"\n paddingHorizontal=\"xs\"\n (dropdownShowing)=\"onPopupShowing()\"\n (dropdownHiding)=\"onPopupHiding()\"\n >\n <!-- A focus-holding dialog per the APG date-picker pattern: Tab cycles\n inside, Escape closes and returns focus to the field. -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div #popupDialog role=\"dialog\" aria-label=\"Choose date\" (keydown)=\"onPopupKeydown($event)\">\n <uni-calendar\n mode=\"single\"\n [value]=\"value()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n (selected)=\"onCalendarPick($event)\"\n />\n </div>\n </uni-dropdown>\n }\n\n <!-- Commits, steps and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniCalendarComponent, selector: "uni-calendar, Calendar", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "mode", "month", "minDate", "maxDate", "disabledDates", "markers", "locale", "weekStart", "ariaLabel", "size"], outputs: ["valueChange", "touchedChange", "monthChange", "selected"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "containerColor"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4843
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniDateInputComponent, isStandalone: true, selector: "uni-date-input, DateInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", opened: "opened", closed: "closed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "toggleRef", first: true, predicate: ["toggle"], descendants: true, read: ElementRef, isSignal: true }, { propertyName: "popupRef", first: true, predicate: ["popupDialog"], descendants: true, isSignal: true }, { propertyName: "dropdown", first: true, predicate: UniDropdownComponent, descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: UniCalendarComponent, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so commits happen only when focus\n leaves the whole field, not while moving into the popup. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <!-- The dropdown wires aria-haspopup/aria-expanded/aria-controls onto\n this button; its name tracks the value (\"Change date, \u2026\"). -->\n <button\n #toggle\n icon-button\n type=\"button\"\n size=\"sm\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'calendar_month'\"\n [disable]=\"disabled()\"\n >\n {{ toggleLabel() }}\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (toggleElement()) {\n <uni-dropdown\n [trigger]=\"toggleElement()!\"\n ariaHasPopup=\"dialog\"\n placement=\"bottom-end\"\n [containerColor]=\"componentOptions().popupColor ?? 'primary-surface'\"\n [shadow]=\"componentOptions().popupShadow ?? 'menu'\"\n [borderRadius]=\"componentOptions().popupBorderRadius ?? 'xs'\"\n paddingVertical=\"xs\"\n paddingHorizontal=\"xs\"\n (dropdownShowing)=\"onPopupShowing()\"\n (dropdownHiding)=\"onPopupHiding()\"\n >\n <!-- A focus-holding dialog per the APG date-picker pattern: Tab cycles\n inside, Escape closes and returns focus to the field. -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div #popupDialog role=\"dialog\" aria-label=\"Choose date\" (keydown)=\"onPopupKeydown($event)\">\n <uni-calendar\n mode=\"single\"\n [value]=\"value()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n (selected)=\"onCalendarPick($event)\"\n />\n </div>\n </uni-dropdown>\n }\n\n <!-- Commits, steps and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniCalendarComponent, selector: "uni-calendar, Calendar", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "mode", "month", "minDate", "maxDate", "disabledDates", "markers", "locale", "weekStart", "ariaLabel", "size"], outputs: ["valueChange", "touchedChange", "monthChange", "selected"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "containerColor"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4149
4844
  }
4150
4845
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateInputComponent, decorators: [{
4151
4846
  type: Component,
@@ -4469,7 +5164,7 @@ class UniTimeInputComponent extends BaseComponent {
4469
5164
  }));
4470
5165
  }, ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
4471
5166
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4472
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTimeInputComponent, isStandalone: true, selector: "uni-time-input, TimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, minTime: { classPropertyName: "minTime", publicName: "minTime", isSignal: true, isRequired: false, transformFunction: null }, maxTime: { classPropertyName: "maxTime", publicName: "maxTime", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5167
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTimeInputComponent, isStandalone: true, selector: "uni-time-input, TimeInput", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, minTime: { classPropertyName: "minTime", publicName: "minTime", isSignal: true, isRequired: false, transformFunction: null }, maxTime: { classPropertyName: "maxTime", publicName: "maxTime", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'timeInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes and the draft\n commits only when focus leaves the whole field. -->\n<div (focusout)=\"onFocusOut($event)\">\n <ng-template #parts>\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n aria-autocomplete=\"list\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"resolvedPlaceholder()\"\n [value]=\"displayText()\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"ariaDescribedBy() ?? null\"\n [attr.aria-invalid]=\"showError() || draftInvalid() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput()\"\n />\n <span [class]=\"toggleWrapClass()\">\n <button\n icon-button\n type=\"button\"\n size=\"sm\"\n aria-haspopup=\"listbox\"\n [symbolName]=\"componentOptions().toggleSymbol ?? 'schedule'\"\n [attr.aria-expanded]=\"list.open()\"\n [disable]=\"disabled()\"\n (click)=\"onToggle()\"\n >\n Choose time\n </button>\n </span>\n </ng-template>\n\n @if (embedded()) {\n <!-- A composer (uni-date-time-input) owns the field chrome. -->\n <div [class]=\"embeddedClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n } @else {\n <uni-input-box [error]=\"showError() || draftInvalid()\" [disabled]=\"disabled()\">\n <div [class]=\"rowClass()\"><ng-container [ngTemplateOutlet]=\"parts\" /></div>\n </uni-input-box>\n }\n\n @if (list.open()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @for (time of options(); track time; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"time === value()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectOption(time)\"\n >\n {{ optionLabels()[i] }}\n </li>\n }\n </ul>\n }\n\n <!-- Commits and rejections are otherwise a silent reformat. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconButtonComponent, selector: "button[uni-icon-button], button[icon-button]", inputs: ["ariaLabel", "iconName", "symbolName", "variant", "size", "disable", "loading", "opticalSize"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4473
5168
  }
4474
5169
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTimeInputComponent, decorators: [{
4475
5170
  type: Component,
@@ -4602,7 +5297,7 @@ class UniDateTimeInputComponent extends BaseComponent {
4602
5297
  });
4603
5298
  }, ...(ngDevMode ? [{ debugName: "dividerClass" }] : /* istanbul ignore next */ []));
4604
5299
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
4605
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDateTimeInputComponent, isStandalone: true, selector: "uni-date-time-input, DateTimeInput", 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 }, minDateTime: { classPropertyName: "minDateTime", publicName: "minDateTime", isSignal: true, isRequired: false, transformFunction: null }, maxDateTime: { classPropertyName: "maxDateTime", publicName: "maxDateTime", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, slotsFor: { classPropertyName: "slotsFor", publicName: "slotsFor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { listeners: { "focusout": "onHostFocusOut($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateTimeInput' }], usesInheritance: true, ngImport: i0, template: "<!-- One field chrome for both parts; the parts render embedded (no box of\n their own), so error/disabled/focus states stay consistent with every\n other field. Tab order: date, then time \u2014 two honest tab stops. -->\n<uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div role=\"group\" [attr.aria-label]=\"label()\" [class]=\"groupClass()\">\n <div [class]=\"datePartClass()\">\n <uni-date-input\n [embedded]=\"true\"\n label=\"Date\"\n [value]=\"dateValue()\"\n [minDate]=\"dateMin()\"\n [maxDate]=\"dateMax()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n [disabled]=\"disabled()\"\n [ariaDescribedBy]=\"ariaDescribedBy()\"\n (valueChange)=\"onDatePartChange($event)\"\n />\n </div>\n <div [class]=\"dividerClass()\"></div>\n <div [class]=\"timePartClass()\">\n <uni-time-input\n [embedded]=\"true\"\n label=\"Time\"\n [value]=\"timeValue()\"\n [minTime]=\"timeMin()\"\n [maxTime]=\"timeMax()\"\n [slots]=\"effectiveSlots()\"\n [minuteStep]=\"minuteStep()\"\n [hour12]=\"hour12()\"\n [locale]=\"locale()\"\n [disabled]=\"timeDisabled()\"\n (valueChange)=\"onTimePartChange($event)\"\n />\n </div>\n </div>\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniDateInputComponent, selector: "uni-date-input, DateInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "displayFormat", "locale", "commitOnBlur", "parse", "embedded", "minDate", "maxDate", "disabledDates", "markers", "weekStart"], outputs: ["valueChange", "touchedChange", "opened", "closed", "rejected"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }, { kind: "component", type: UniTimeInputComponent, selector: "uni-time-input, TimeInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "minuteStep", "minTime", "maxTime", "slots", "hour12", "locale", "commitOnBlur", "embedded"], outputs: ["valueChange", "touchedChange", "rejected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5300
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniDateTimeInputComponent, isStandalone: true, selector: "uni-date-time-input, DateTimeInput", 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 }, minDateTime: { classPropertyName: "minDateTime", publicName: "minDateTime", isSignal: true, isRequired: false, transformFunction: null }, maxDateTime: { classPropertyName: "maxDateTime", publicName: "maxDateTime", isSignal: true, isRequired: false, transformFunction: null }, disabledDates: { classPropertyName: "disabledDates", publicName: "disabledDates", isSignal: true, isRequired: false, transformFunction: null }, markers: { classPropertyName: "markers", publicName: "markers", isSignal: true, isRequired: false, transformFunction: null }, slots: { classPropertyName: "slots", publicName: "slots", isSignal: true, isRequired: false, transformFunction: null }, minuteStep: { classPropertyName: "minuteStep", publicName: "minuteStep", isSignal: true, isRequired: false, transformFunction: null }, hour12: { classPropertyName: "hour12", publicName: "hour12", isSignal: true, isRequired: false, transformFunction: null }, weekStart: { classPropertyName: "weekStart", publicName: "weekStart", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, slotsFor: { classPropertyName: "slotsFor", publicName: "slotsFor", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { listeners: { "focusout": "onHostFocusOut($event)" }, properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'dateTimeInput' }], usesInheritance: true, ngImport: i0, template: "<!-- One field chrome for both parts; the parts render embedded (no box of\n their own), so error/disabled/focus states stay consistent with every\n other field. Tab order: date, then time \u2014 two honest tab stops. -->\n<uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div role=\"group\" [attr.aria-label]=\"label()\" [class]=\"groupClass()\">\n <div [class]=\"datePartClass()\">\n <uni-date-input\n [embedded]=\"true\"\n label=\"Date\"\n [value]=\"dateValue()\"\n [minDate]=\"dateMin()\"\n [maxDate]=\"dateMax()\"\n [disabledDates]=\"disabledDates()\"\n [markers]=\"markers()\"\n [weekStart]=\"weekStart()\"\n [locale]=\"locale()\"\n [disabled]=\"disabled()\"\n [ariaDescribedBy]=\"ariaDescribedBy()\"\n (valueChange)=\"onDatePartChange($event)\"\n />\n </div>\n <div [class]=\"dividerClass()\"></div>\n <div [class]=\"timePartClass()\">\n <uni-time-input\n [embedded]=\"true\"\n label=\"Time\"\n [value]=\"timeValue()\"\n [minTime]=\"timeMin()\"\n [maxTime]=\"timeMax()\"\n [slots]=\"effectiveSlots()\"\n [minuteStep]=\"minuteStep()\"\n [hour12]=\"hour12()\"\n [locale]=\"locale()\"\n [disabled]=\"timeDisabled()\"\n (valueChange)=\"onTimePartChange($event)\"\n />\n </div>\n </div>\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniDateInputComponent, selector: "uni-date-input, DateInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "displayFormat", "locale", "commitOnBlur", "parse", "embedded", "minDate", "maxDate", "disabledDates", "markers", "weekStart"], outputs: ["valueChange", "touchedChange", "opened", "closed", "rejected"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }, { kind: "component", type: UniTimeInputComponent, selector: "uni-time-input, TimeInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "placeholder", "minuteStep", "minTime", "maxTime", "slots", "hour12", "locale", "commitOnBlur", "embedded"], outputs: ["valueChange", "touchedChange", "rejected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4606
5301
  }
4607
5302
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDateTimeInputComponent, decorators: [{
4608
5303
  type: Component,
@@ -4679,7 +5374,7 @@ class UniInputComponent {
4679
5374
  }
4680
5375
  inputClass = css({});
4681
5376
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4682
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputComponent, isStandalone: true, selector: "uni-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, pattern: { classPropertyName: "pattern", publicName: "pattern", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, inputMode: { classPropertyName: "inputMode", publicName: "inputMode", isSignal: true, isRequired: false, transformFunction: null }, list: { classPropertyName: "list", publicName: "list", isSignal: true, isRequired: false, transformFunction: null }, spellcheck: { classPropertyName: "spellcheck", publicName: "spellcheck", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [width]=\"width()\" [fullWidth]=\"fullWidth()\" [grow]=\"grow()\">\n <ng-content select=\"pre-input, [pre-input]\" />\n <input\n [type]=\"type()\"\n [value]=\"value()\"\n (input)=\"handleInput($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (blur)=\"markAsTouched()\"\n [class]=\"inputClass\"\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.inputmode]=\"inputMode() || null\"\n [attr.list]=\"list() || null\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.step]=\"step() ?? null\"\n [attr.minlength]=\"minLength() ?? null\"\n [attr.maxlength]=\"maxLength() ?? null\"\n [attr.pattern]=\"nativePattern()\"\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 />\n <ng-content select=\"post-input, [post-input]\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5377
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniInputComponent, isStandalone: true, selector: "uni-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, pattern: { classPropertyName: "pattern", publicName: "pattern", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, inputMode: { classPropertyName: "inputMode", publicName: "inputMode", isSignal: true, isRequired: false, transformFunction: null }, list: { classPropertyName: "list", publicName: "list", isSignal: true, isRequired: false, transformFunction: null }, spellcheck: { classPropertyName: "spellcheck", publicName: "spellcheck", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [width]=\"width()\" [fullWidth]=\"fullWidth()\" [grow]=\"grow()\">\n <ng-content select=\"pre-input, [pre-input]\" />\n <input\n [type]=\"type()\"\n [value]=\"value()\"\n (input)=\"handleInput($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n (blur)=\"markAsTouched()\"\n [class]=\"inputClass\"\n [attr.name]=\"name() || null\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.inputmode]=\"inputMode() || null\"\n [attr.list]=\"list() || null\"\n [attr.min]=\"min() ?? null\"\n [attr.max]=\"max() ?? null\"\n [attr.step]=\"step() ?? null\"\n [attr.minlength]=\"minLength() ?? null\"\n [attr.maxlength]=\"maxLength() ?? null\"\n [attr.pattern]=\"nativePattern()\"\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 />\n <ng-content select=\"post-input, [post-input]\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4683
5378
  }
4684
5379
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniInputComponent, decorators: [{
4685
5380
  type: Component,
@@ -5004,7 +5699,7 @@ class UniMultiSelectDropdownComponent extends BaseComponent {
5004
5699
  });
5005
5700
  }
5006
5701
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
5007
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectDropdownComponent, isStandalone: true, selector: "uni-multi-select-dropdown", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], viewQueries: [{ propertyName: "optionRefs", predicate: ["optionRow"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n @if (label(); as fieldLabel) {\n <span [class]=\"srOnly\">{{ fieldLabel }},</span>\n }\n <span [class]=\"srOnly\">{{ selectionSummary() }}</span>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div (keydown)=\"onPanelKeydown($event)\">\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div\n stack-layout\n gap=\"xs\"\n padding=\"xs\"\n role=\"group\"\n [attr.aria-label]=\"label() ? label() + ' options' : 'Options'\"\n >\n @for (option of filteredOptions(); track option.value; let i = $index) {\n <div #optionRow (focusin)=\"onOptionFocus(i)\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled() || !!option.disabled\"\n >\n </uni-checkbox>\n </div>\n }\n @if (filteredOptions().length === 0) {\n <span uni-text=\"label\" role=\"status\">No options match.</span>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n </div>\n</uni-dropdown>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "containerColor"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "directive", type: UniStackDirective, selector: "[uni-stack-layout], [stack-layout]", inputs: ["display", "flexDirection", "minHeight"] }, { kind: "component", type: UniDividerComponent, selector: "uni-divider", inputs: ["orientation", "border"] }, { kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5702
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniMultiSelectDropdownComponent, isStandalone: true, selector: "uni-multi-select-dropdown", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, host: { properties: { "class": "className" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'multiSelectDropdown' }], viewQueries: [{ propertyName: "optionRefs", predicate: ["optionRow"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<button\n [class]=\"triggerClass\"\n #trigger\n [style.cursor]=\"disabled() ? 'default' : 'pointer'\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (click)=\"!disabled() && touched.set(true)\"\n>\n @if (label(); as fieldLabel) {\n <span [class]=\"srOnly\">{{ fieldLabel }},</span>\n }\n <span [class]=\"srOnly\">{{ selectionSummary() }}</span>\n <uni-input-box [error]=\"showError()\" [disabled]=\"disabled()\">\n <div\n row-layout\n alignItems=\"center\"\n justifyContent=\"space-between\"\n [fullWidth]=\"true\"\n [minWidth]=\"0\"\n paddingLeft=\"sm\"\n >\n <span uni-text\n display=\"block\"\n [typeface]=\"componentOptions().textRole\"\n [color]=\"textColor()\"\n [ellipsis]=\"true\"\n [style.flex-grow]=\"1\"\n >{{ selectedLabelsText() }}</span>\n <uni-symbol name=\"keyboard_arrow_down\" [style.flex-shrink]=\"0\"></uni-symbol>\n </div>\n </uni-input-box>\n</button>\n<uni-dropdown\n #dropdown\n [trigger]=\"trigger\"\n ariaHasPopup=\"dialog\"\n (dropdownShowing)=\"$event && searchInput.focus()\"\n (dropdownHiding)=\"$event && trigger.focus()\"\n>\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <div (keydown)=\"onPanelKeydown($event)\">\n <div box-layout gap=\"xs\" padding=\"xs\">\n <input\n #searchInput\n type=\"text\"\n [class]=\"searchInputClass()\"\n [value]=\"query()\"\n (input)=\"handleQueryInput($event)\"\n placeholder=\"Search...\"\n aria-label=\"Filter options\"\n (click)=\"$event.stopPropagation()\"\n [disabled]=\"disabled()\"\n />\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div\n stack-layout\n gap=\"xs\"\n padding=\"xs\"\n role=\"group\"\n [attr.aria-label]=\"label() ? label() + ' options' : 'Options'\"\n >\n @for (option of filteredOptions(); track option.value; let i = $index) {\n <div #optionRow (focusin)=\"onOptionFocus(i)\">\n <uni-checkbox\n [label]=\"option.label\"\n [checked]=\"isOptionSelected(option)()\"\n (checkedChange)=\"toggleOption(option, $event)\"\n variant=\"primary\"\n [disabled]=\"disabled() || !!option.disabled\"\n >\n </uni-checkbox>\n </div>\n }\n @if (filteredOptions().length === 0) {\n <span uni-text=\"label\" role=\"status\">No options match.</span>\n }\n </div>\n\n <uni-divider [border]=\"componentOptions().dividerBorder\" />\n\n <div row-layout gap=\"xs\" justifyContent=\"space-around\" padding=\"xs\">\n <button text-button (click)=\"dropdown.hideDropdown()\" size=\"sm\" variant=\"ghost\">Done</button>\n </div>\n </div>\n</uni-dropdown>\n", dependencies: [{ kind: "component", type: UniCheckboxComponent, selector: "uni-checkbox", inputs: ["checked", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "label", "indeterminate"], outputs: ["checkedChange", "touchedChange", "indeterminateChange"] }, { kind: "directive", type: UniBoxDirective, selector: "[uni-box-layout], [box-layout]", inputs: ["containerColor", "backgroundColor", "borderRadius", "borderRadiusLeft", "borderRadiusRight", "borderRadiusTop", "borderRadiusBottom", "padding", "paddingHorizontal", "paddingVertical", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "border", "borderTop", "borderBottom", "borderLeft", "borderRight", "dashBorder", "alignSelf", "alignItems", "alignContent", "justifyContent", "grow", "flex", "shrink", "basis", "marginInline", "display", "position", "inset", "height", "minHeight", "maxHeight", "width", "minWidth", "maxWidth", "ignoreDir", "gridArea", "gridColumn", "gridRow", "overflow", "shadow", "gap", "fullWidth", "fullHeight", "flexDirection", "textAlign", "wrapItems", "zIndex"] }, { kind: "component", type: UniDropdownComponent, selector: "uni-dropdown", inputs: ["trigger", "placement", "offset", "ariaHasPopup", "paddingVertical", "paddingHorizontal", "border", "borderRadius", "shadow", "containerColor"], outputs: ["dropdownShowing", "dropdownHiding"] }, { kind: "directive", type: UniStackDirective, selector: "[uni-stack-layout], [stack-layout]", inputs: ["display", "flexDirection", "minHeight"] }, { kind: "component", type: UniDividerComponent, selector: "uni-divider", inputs: ["orientation", "border"] }, { kind: "component", type: UniButtonComponent, selector: "button[uni-text-button], button[text-button]", inputs: ["disable", "loading", "fullWidth", "symbolLeft", "symbolRight"] }, { kind: "directive", type: UniTextDirective, selector: "[uni-text]", inputs: ["uni-text", "typeface", "color", "display", "align", "nowrap", "maxWidth", "ellipsis"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }, { kind: "directive", type: UniRowDirective, selector: "[uni-row-layout], [row-layout]", inputs: ["display", "flexDirection", "minWidth"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5008
5703
  }
5009
5704
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniMultiSelectDropdownComponent, decorators: [{
5010
5705
  type: Component,
@@ -5274,7 +5969,7 @@ class UniDebounceInputComponent {
5274
5969
  minWidth: 0,
5275
5970
  });
5276
5971
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDebounceInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5277
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.12", type: UniDebounceInputComponent, isStandalone: true, selector: "uni-debounce-input", inputs: { inputName: { classPropertyName: "inputName", publicName: "inputName", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, inputMode: { classPropertyName: "inputMode", publicName: "inputMode", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaExpanded: { classPropertyName: "ariaExpanded", publicName: "ariaExpanded", isSignal: true, isRequired: false, transformFunction: null }, ariaControls: { classPropertyName: "ariaControls", publicName: "ariaControls", isSignal: true, isRequired: false, transformFunction: null }, ariaActivedescendant: { classPropertyName: "ariaActivedescendant", publicName: "ariaActivedescendant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "inputElement", first: true, predicate: ["field"], descendants: true, isSignal: true }], ngImport: i0, template: "<uni-input-box>\n <ng-content select=\"[pre-input]\" />\n <input\n #field\n [class]=\"inputClass\"\n [type]=\"type()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.inputmode]=\"inputMode() || null\"\n [name]=\"inputName()\"\n [id]=\"inputId()\"\n [value]=\"value() ?? ''\"\n (input)=\"handleInput($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"label() || null\"\n [attr.role]=\"role() || null\"\n [attr.aria-expanded]=\"ariaExpanded() ?? null\"\n [attr.aria-controls]=\"ariaControls() || null\"\n [attr.aria-activedescendant]=\"ariaActivedescendant() || null\"\n [attr.aria-autocomplete]=\"role() === 'combobox' ? 'list' : null\"\n />\n <ng-content select=\"[post-input]\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5972
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.12", type: UniDebounceInputComponent, isStandalone: true, selector: "uni-debounce-input", inputs: { inputName: { classPropertyName: "inputName", publicName: "inputName", isSignal: true, isRequired: false, transformFunction: null }, inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, inputMode: { classPropertyName: "inputMode", publicName: "inputMode", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, ariaExpanded: { classPropertyName: "ariaExpanded", publicName: "ariaExpanded", isSignal: true, isRequired: false, transformFunction: null }, ariaControls: { classPropertyName: "ariaControls", publicName: "ariaControls", isSignal: true, isRequired: false, transformFunction: null }, ariaActivedescendant: { classPropertyName: "ariaActivedescendant", publicName: "ariaActivedescendant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, viewQueries: [{ propertyName: "inputElement", first: true, predicate: ["field"], descendants: true, isSignal: true }], ngImport: i0, template: "<uni-input-box>\n <ng-content select=\"[pre-input]\" />\n <input\n #field\n [class]=\"inputClass\"\n [type]=\"type()\"\n [attr.autocomplete]=\"autocomplete() || null\"\n [attr.inputmode]=\"inputMode() || null\"\n [name]=\"inputName()\"\n [id]=\"inputId()\"\n [value]=\"value() ?? ''\"\n (input)=\"handleInput($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"label() || null\"\n [attr.role]=\"role() || null\"\n [attr.aria-expanded]=\"ariaExpanded() ?? null\"\n [attr.aria-controls]=\"ariaControls() || null\"\n [attr.aria-activedescendant]=\"ariaActivedescendant() || null\"\n [attr.aria-autocomplete]=\"role() === 'combobox' ? 'list' : null\"\n />\n <ng-content select=\"[post-input]\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5278
5973
  }
5279
5974
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniDebounceInputComponent, decorators: [{
5280
5975
  type: Component,
@@ -5461,7 +6156,7 @@ class UniSelectComponent {
5461
6156
  pointerEvents: 'none' /* Crucial for clicking through */,
5462
6157
  });
5463
6158
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5464
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\" [width]=\"width()\" [fullWidth]=\"fullWidth()\" [grow]=\"grow()\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option\n [value]=\"i\"\n [selected]=\"currentSelectedIndex() === i.toString()\"\n [disabled]=\"opt.disabled ?? false\"\n >\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6159
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSelectComponent, isStandalone: true, selector: "uni-select", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, template: "<uni-input-box [error]=\"showError()\" [class]=\"selectClass\" [width]=\"width()\" [fullWidth]=\"fullWidth()\" [grow]=\"grow()\">\n <select\n [disabled]=\"disabled()\"\n [value]=\"currentSelectedIndex()\"\n (change)=\"handleSelectChange($event)\"\n (blur)=\"touched.set(true)\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n >\n @if (placeholder()) {\n <option [value]=\"UNSELECTED\" disabled selected>\n {{ placeholder() }}\n </option>\n }\n\n @for (opt of options(); track opt.label; let i = $index) {\n <!-- [selected] evaluates during the option's own render, so a selection\n pointing at an option added in the same change-detection pass still\n applies (the select-level [value] write lands before new options\n exist and is ignored by the browser). -->\n <option\n [value]=\"i\"\n [selected]=\"currentSelectedIndex() === i.toString()\"\n [disabled]=\"opt.disabled ?? false\"\n >\n {{ opt.label }}\n </option>\n }\n </select>\n <uni-symbol name=\"keyboard_arrow_down\" [class]=\"arrowClass\" />\n</uni-input-box>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }, { kind: "component", type: UniSymbolComponent, selector: "uni-symbol", inputs: ["name", "fill", "weight", "grade", "opticalSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5465
6160
  }
5466
6161
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSelectComponent, decorators: [{
5467
6162
  type: Component,
@@ -5469,138 +6164,1943 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
5469
6164
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], 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 }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
5470
6165
 
5471
6166
  /**
5472
- * Range slider on a native `<input type="range">` keyboard interaction and
5473
- * the ARIA slider contract come from the platform. Fill, track, thumb and
5474
- * radii resolve from `slider` theme tokens; the fill percentage rides a CSS
5475
- * custom property so dragging never regenerates styles.
6167
+ * Numeric field with locale-aware parsing, `Intl` formatting on commit,
6168
+ * prefix/suffix adornments and steppers that hold to repeat.
6169
+ *
6170
+ * Not `<input type="number">`, and the first reason is a data-loss bug: per the
6171
+ * HTML value sanitization algorithm, a number input whose text is not a valid
6172
+ * floating-point number reports `value === ''`. Type `12,50` as most of Europe
6173
+ * does, or paste `1,234.56` from a spreadsheet, and the app reads an empty
6174
+ * field. This is `type="text"` with `role="spinbutton"`, which is the only way
6175
+ * to keep the user's malformed text on screen and tell them about it.
6176
+ *
6177
+ * Chrome comes from `uni-input-box`, so error, disabled and focus states match
6178
+ * every other field. All arithmetic runs on the cdk's exact decimal helpers.
5476
6179
  */
5477
- class UniSliderComponent extends BaseComponent {
5478
- // --- REQUIRED SIGNALS (populated by FormValueControl) ---
5479
- value = model(0, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
6180
+ class UniNumberInputComponent extends BaseComponent {
6181
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
6182
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
5480
6183
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
5481
6184
  touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
5482
6185
  invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
5483
6186
  dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
5484
- /** Synced from required() validators by the Signal Forms [field] directive. */
5485
6187
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
6188
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
5486
6189
  /**
5487
- * Id(s) of external element(s) describing this control typically your
5488
- * app-rendered value or error text exposed as aria-describedby.
6190
+ * Exact binding, as a canonical decimal string. Bind this instead of `value`
6191
+ * where a cent in the fifth decimal place matters; both stay in sync, so it
6192
+ * is a one-word change from the ordinary case.
5489
6193
  */
5490
- ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
5491
- // --- CONFIGURATION ---
6194
+ valueAsString = model(null, ...(ngDevMode ? [{ debugName: "valueAsString" }] : /* istanbul ignore next */ []));
6195
+ // --- Configuration -------------------------------------------------------
6196
+ /** Accessible name, e.g. "Unit price". */
5492
6197
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
5493
- // `min`/`max` are part of the FormValueControl contract (synced from
5494
- // min()/max() validators), so their type must admit undefined.
5495
- min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
5496
- max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
6198
+ placeholder = input(...(ngDevMode ? [undefined, { debugName: "placeholder" }] : /* istanbul ignore next */ []));
6199
+ preset = input('decimal', ...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
6200
+ /** ISO 4217 code, e.g. `'USD'`. Implies `preset="currency"`. */
6201
+ currency = input(...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
6202
+ /** BCP 47 tag. Defaults to the document language, then the browser's. */
6203
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
6204
+ /** Static adornment before the number, e.g. `'$'`. Never parseable input. */
6205
+ prefix = input(...(ngDevMode ? [undefined, { debugName: "prefix" }] : /* istanbul ignore next */ []));
6206
+ /** Static adornment after the number, e.g. `'kg'`, `'/mo'`. */
6207
+ suffix = input(...(ngDevMode ? [undefined, { debugName: "suffix" }] : /* istanbul ignore next */ []));
6208
+ decimals = input(...(ngDevMode ? [undefined, { debugName: "decimals" }] : /* istanbul ignore next */ []));
6209
+ grouping = input(...(ngDevMode ? [undefined, { debugName: "grouping" }] : /* istanbul ignore next */ []));
6210
+ /** Escape hatch, merged over the preset. */
6211
+ numberFormat = input(...(ngDevMode ? [undefined, { debugName: "numberFormat" }] : /* istanbul ignore next */ []));
6212
+ roundingMode = input('half-up', ...(ngDevMode ? [{ debugName: "roundingMode" }] : /* istanbul ignore next */ []));
6213
+ align = input(...(ngDevMode ? [undefined, { debugName: "align" }] : /* istanbul ignore next */ []));
6214
+ /** The model is a fraction: `0.15` displays as `15%`. */
6215
+ valueIsFraction = input(false, ...(ngDevMode ? [{ debugName: "valueIsFraction" }] : /* istanbul ignore next */ []));
6216
+ /** Spoken long form of an abbreviated suffix, e.g. `'kilograms'` for `kg`. */
6217
+ unitAnnouncement = input(...(ngDevMode ? [undefined, { debugName: "unitAnnouncement" }] : /* istanbul ignore next */ []));
6218
+ readOnly = input(false, ...(ngDevMode ? [{ debugName: "readOnly" }] : /* istanbul ignore next */ []));
6219
+ /** Renders without its own input-box chrome, for composers like uni-slider. */
6220
+ embedded = input(false, ...(ngDevMode ? [{ debugName: "embedded" }] : /* istanbul ignore next */ []));
6221
+ // --- Range and stepping --------------------------------------------------
6222
+ min = input(...(ngDevMode ? [undefined, { debugName: "min" }] : /* istanbul ignore next */ []));
6223
+ max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
5497
6224
  step = input(1, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
5498
- resolvedMin = computed(() => this.min() ?? 0, ...(ngDevMode ? [{ debugName: "resolvedMin" }] : /* istanbul ignore next */ []));
5499
- resolvedMax = computed(() => this.max() ?? 100, ...(ngDevMode ? [{ debugName: "resolvedMax" }] : /* istanbul ignore next */ []));
5500
- markAsTouched() {
5501
- this.touched.set(true);
6225
+ /** `PageUp`/`PageDown` and `Shift+Arrow`. Default: `step × 10`. */
6226
+ largeStep = input(...(ngDevMode ? [undefined, { debugName: "largeStep" }] : /* istanbul ignore next */ []));
6227
+ /** `Alt+Arrow`, Figma's fine-nudge convention. Unset disables it. */
6228
+ smallStep = input(...(ngDevMode ? [undefined, { debugName: "smallStep" }] : /* istanbul ignore next */ []));
6229
+ stepOrigin = input('min', ...(ngDevMode ? [{ debugName: "stepOrigin" }] : /* istanbul ignore next */ []));
6230
+ /** Cyclic fields only — 23 → 0 hours, 359 → 0 degrees. */
6231
+ wrap = input(false, ...(ngDevMode ? [{ debugName: "wrap" }] : /* istanbul ignore next */ []));
6232
+ /** `false` refuses an out-of-range commit instead of clamping it. */
6233
+ clampOnCommit = input(true, ...(ngDevMode ? [{ debugName: "clampOnCommit" }] : /* istanbul ignore next */ []));
6234
+ /** What ↑ commits on an empty field. Default: `min ?? 0`. */
6235
+ emptyStepValue = input(...(ngDevMode ? [undefined, { debugName: "emptyStepValue" }] : /* istanbul ignore next */ []));
6236
+ // --- Entry behaviour -----------------------------------------------------
6237
+ commitOnBlur = input(true, ...(ngDevMode ? [{ debugName: "commitOnBlur" }] : /* istanbul ignore next */ []));
6238
+ selectOnFocus = input(false, ...(ngDevMode ? [{ debugName: "selectOnFocus" }] : /* istanbul ignore next */ []));
6239
+ /** `12*3` → 36. Off by default: a parser in a form field is a real cost. */
6240
+ allowExpressions = input(false, ...(ngDevMode ? [{ debugName: "allowExpressions" }] : /* istanbul ignore next */ []));
6241
+ /** Scroll-to-step. Off by default — see `onWheel`. */
6242
+ wheel = input(false, ...(ngDevMode ? [{ debugName: "wheel" }] : /* istanbul ignore next */ []));
6243
+ repeat = input(true, ...(ngDevMode ? [{ debugName: "repeat" }] : /* istanbul ignore next */ []));
6244
+ /** Custom parser, replacing the built-in locale parsing. */
6245
+ parse = input(...(ngDevMode ? [undefined, { debugName: "parse" }] : /* istanbul ignore next */ []));
6246
+ /** Overrides the themed layout for this instance. */
6247
+ stepperLayout = input(...(ngDevMode ? [undefined, { debugName: "stepperLayout" }] : /* istanbul ignore next */ []));
6248
+ // --- Events --------------------------------------------------------------
6249
+ stepped = output();
6250
+ /** A commit was refused; the raw text stays in the field. */
6251
+ rejected = output();
6252
+ inputRef = viewChild.required('field');
6253
+ srOnly = css(visuallyHidden);
6254
+ /** Clamps, fences, rejections and expression results are otherwise silent. */
6255
+ announcer = createAnnouncer();
6256
+ hintId = uniqueId('uni-number-input-hint');
6257
+ /** Uncommitted text. `null` means "show the committed value". */
6258
+ draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
6259
+ /** A commit that failed — styles the field until the text is edited. */
6260
+ draftInvalid = signal(false, ...(ngDevMode ? [{ debugName: "draftInvalid" }] : /* istanbul ignore next */ []));
6261
+ focused = signal(false, ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
6262
+ /**
6263
+ * The canonical decimal behind both models — the field's source of truth.
6264
+ *
6265
+ * Two models that each accept writes need a rule for which one won, and
6266
+ * "whichever the app touched last" is the only one that does not surprise
6267
+ * somebody. A `linkedSignal` over both gives us that: the model whose value
6268
+ * differs from the previous source is the one that changed.
6269
+ *
6270
+ * The subtlety is the echo. Committing writes both models, and the `value`
6271
+ * projection of a 17-digit exact string is lossy — so on the next pass
6272
+ * `value` looks changed, and naively adopting it would clobber the very
6273
+ * precision `valueAsString` exists to keep. A changed `value` that already
6274
+ * matches `Number(exact)` is our own projection coming back, not a write.
6275
+ */
6276
+ canonical = linkedSignal({ ...(ngDevMode ? { debugName: "canonical" } : /* istanbul ignore next */ {}), source: () => ({ value: this.value(), exact: this.valueAsString() }),
6277
+ computation: (source, previous) => {
6278
+ const prior = previous?.source;
6279
+ if (prior && source.exact !== prior.exact)
6280
+ return source.exact;
6281
+ if (prior && source.value !== prior.value) {
6282
+ if (source.exact != null && Number(source.exact) === source.value)
6283
+ return source.exact;
6284
+ return source.value == null ? null : toDecimal(source.value);
6285
+ }
6286
+ if (source.exact != null)
6287
+ return source.exact;
6288
+ return source.value == null ? null : toDecimal(source.value);
6289
+ } });
6290
+ constructor() {
6291
+ super();
6292
+ // A hybrid device can gain or lose a coarse pointer mid-session.
6293
+ if (typeof matchMedia === 'function') {
6294
+ const query = matchMedia('(pointer: coarse)');
6295
+ const onChange = () => this.coarsePointer.set(query.matches);
6296
+ query.addEventListener('change', onChange);
6297
+ inject(DestroyRef).onDestroy(() => query.removeEventListener('change', onChange));
6298
+ }
6299
+ // Keep both models reflecting the source of truth, so an app that binds
6300
+ // only one of them still reads a consistent value from the other.
6301
+ effect(() => {
6302
+ const canonical = this.canonical();
6303
+ untracked(() => {
6304
+ if (this.valueAsString() !== canonical)
6305
+ this.valueAsString.set(canonical);
6306
+ const projected = canonical == null ? null : Number(canonical);
6307
+ if (this.value() !== projected)
6308
+ this.value.set(projected);
6309
+ });
6310
+ });
6311
+ // Silent precision loss is the whole reason `valueAsString` exists, so it
6312
+ // is worth saying out loud — once, in dev, per offending value.
6313
+ if (isDevMode()) {
6314
+ let warned = null;
6315
+ effect(() => {
6316
+ const exact = this.valueAsString();
6317
+ const value = this.value();
6318
+ const subject = exact ?? value;
6319
+ if (subject == null)
6320
+ return;
6321
+ const key = String(subject);
6322
+ if (key === warned || !losesPrecision(subject))
6323
+ return;
6324
+ warned = key;
6325
+ console.warn(`[uni-number-input] "${this.label()}": ${key} cannot round-trip through a JavaScript number. ` +
6326
+ 'Bind [(valueAsString)] instead of [(value)] to keep it exact.');
6327
+ });
6328
+ }
5502
6329
  }
5503
- handleInput(event) {
5504
- this.value.set(Number(event.target.value));
6330
+ // --- Format resolution ----------------------------------------------------
6331
+ resolvedLocale = computed(() => this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'), ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : /* istanbul ignore next */ []));
6332
+ format = computed(() => resolveNumberFormat({
6333
+ preset: this.preset(),
6334
+ currency: this.currency(),
6335
+ locale: this.resolvedLocale(),
6336
+ decimals: this.decimals(),
6337
+ grouping: this.grouping(),
6338
+ prefix: this.prefix(),
6339
+ suffix: this.suffix(),
6340
+ roundingMode: this.roundingMode(),
6341
+ valueIsFraction: this.valueIsFraction(),
6342
+ numberFormat: this.numberFormat(),
6343
+ min: this.min(),
6344
+ unitAnnouncement: this.unitAnnouncement(),
6345
+ }), ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
6346
+ /**
6347
+ * Two stacked arrows cannot both be 24px tall inside a 32px field, so on a
6348
+ * coarse pointer the stacked layout becomes `split`, where each button is a
6349
+ * full-height square and clears the WCAG 2.2 SC 2.5.8 floor. Two 12px
6350
+ * targets under a fingertip is a coin toss.
6351
+ */
6352
+ coarsePointer = signal(typeof matchMedia === 'function' ? matchMedia('(pointer: coarse)').matches : false, ...(ngDevMode ? [{ debugName: "coarsePointer" }] : /* istanbul ignore next */ []));
6353
+ layout = computed(() => {
6354
+ const requested = this.stepperLayout() ?? this.componentOptions().stepperLayout ?? 'stacked';
6355
+ return requested === 'stacked' && this.coarsePointer() ? 'split' : requested;
6356
+ }, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
6357
+ showSteppers = computed(() => this.layout() !== 'none' && !this.readOnly(), ...(ngDevMode ? [{ debugName: "showSteppers" }] : /* istanbul ignore next */ []));
6358
+ showError = computed(() => (this.invalid() && (this.touched() || this.dirty())) || this.draftInvalid(), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
6359
+ /** Raw while focused, formatted once committed — no caret arithmetic ever. */
6360
+ displayText = computed(() => {
6361
+ const draft = this.draft();
6362
+ if (draft != null)
6363
+ return draft;
6364
+ const canonical = this.canonical();
6365
+ if (canonical == null)
6366
+ return '';
6367
+ return this.focused()
6368
+ ? rawNumberText(canonical, this.format())
6369
+ : formatNumber(canonical, this.format());
6370
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
6371
+ valueTextForAria = computed(() => speakNumber(this.canonical(), this.format()), ...(ngDevMode ? [{ debugName: "valueTextForAria" }] : /* istanbul ignore next */ []));
6372
+ /**
6373
+ * `aria-valuenow` is omitted entirely on an empty field, per APG — a
6374
+ * spinbutton reporting 0 for "nothing yet" is a wrong answer, not a missing
6375
+ * one. `aria-valuetext` carries the localized "Empty" instead.
6376
+ */
6377
+ canonicalForAria = computed(() => this.canonical() ?? null, ...(ngDevMode ? [{ debugName: "canonicalForAria" }] : /* istanbul ignore next */ []));
6378
+ describedBy = computed(() => [this.ariaDescribedBy(), this.hintId].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
6379
+ // --- Fences ---------------------------------------------------------------
6380
+ atFence(which) {
6381
+ const bound = which === 'min' ? this.min() : this.max();
6382
+ const canonical = this.canonical();
6383
+ if (bound == null || canonical == null)
6384
+ return false;
6385
+ const clamped = clampDecimal(canonical, this.min(), this.max());
6386
+ if (clamped.hit === which)
6387
+ return true;
6388
+ return which === 'min'
6389
+ ? Number(canonical) <= bound
6390
+ : Number(canonical) >= bound;
5505
6391
  }
5506
- fillPercent = computed(() => {
5507
- const min = this.resolvedMin();
5508
- const range = this.resolvedMax() - min;
5509
- if (range <= 0)
5510
- return '0%';
5511
- const ratio = (this.value() - min) / range;
5512
- return `${Math.min(100, Math.max(0, ratio * 100))}%`;
5513
- }, ...(ngDevMode ? [{ debugName: "fillPercent" }] : /* istanbul ignore next */ []));
5514
- inputClass = computed(() => {
6392
+ atMin = computed(() => this.atFence('min') && !this.wrap(), ...(ngDevMode ? [{ debugName: "atMin" }] : /* istanbul ignore next */ []));
6393
+ atMax = computed(() => this.atFence('max') && !this.wrap(), ...(ngDevMode ? [{ debugName: "atMax" }] : /* istanbul ignore next */ []));
6394
+ // --- Committing -----------------------------------------------------------
6395
+ /** Set the source of truth; the constructor's effect pushes it to both models. */
6396
+ write(canonical) {
6397
+ this.canonical.set(canonical);
6398
+ }
6399
+ /**
6400
+ * Turn the draft into a value. Out-of-range either clamps (announced) or is
6401
+ * refused, per `clampOnCommit`; unreadable text stays in the field, flagged.
6402
+ */
6403
+ commitDraft() {
6404
+ const draft = this.draft();
6405
+ if (draft == null)
6406
+ return;
6407
+ const custom = this.parse();
6408
+ if (custom) {
6409
+ const parsed = custom(draft, this.resolvedLocale());
6410
+ if (parsed == null)
6411
+ return this.reject(draft, 'unparseable');
6412
+ return this.acceptValue(parsed, false);
6413
+ }
6414
+ const result = parseNumber(draft, this.format(), {
6415
+ allowExpressions: this.allowExpressions(),
6416
+ currency: this.currency(),
6417
+ });
6418
+ if (result.status === 'empty') {
6419
+ this.draft.set(null);
6420
+ this.draftInvalid.set(false);
6421
+ this.write(null);
6422
+ return;
6423
+ }
6424
+ if (result.status === 'error')
6425
+ return this.reject(draft, result.reason);
6426
+ this.acceptValue(result.value, result.viaExpression);
6427
+ }
6428
+ acceptValue(parsed, viaExpression) {
6429
+ const settled = settleNumber(parsed, this.format());
6430
+ const clamped = clampDecimal(settled, this.min(), this.max());
6431
+ if (clamped.hit && !this.clampOnCommit()) {
6432
+ return this.reject(this.draft() ?? settled, clamped.hit);
6433
+ }
6434
+ this.draft.set(null);
6435
+ this.draftInvalid.set(false);
6436
+ this.write(clamped.value);
6437
+ if (clamped.hit) {
6438
+ const bound = clamped.hit === 'min' ? this.min() : this.max();
6439
+ this.announcer.announce(`${clamped.hit === 'min' ? 'Minimum' : 'Maximum'} is ${bound}. Value set to ${bound}.`);
6440
+ }
6441
+ else if (viaExpression) {
6442
+ this.announcer.announce(`${formatNumber(clamped.value, this.format())}.`);
6443
+ }
6444
+ }
6445
+ reject(raw, reason) {
6446
+ this.draftInvalid.set(true);
6447
+ this.rejected.emit({ raw, reason });
6448
+ this.announcer.announce(this.rejectionMessage(raw, reason));
6449
+ }
6450
+ rejectionMessage(raw, reason) {
6451
+ switch (reason) {
6452
+ case 'min':
6453
+ return `${raw} is below the minimum of ${this.min()}.`;
6454
+ case 'max':
6455
+ return `${raw} is above the maximum of ${this.max()}.`;
6456
+ case 'not-integer':
6457
+ return `${raw} must be a whole number.`;
6458
+ default:
6459
+ return `${raw} is not a number.`;
6460
+ }
6461
+ }
6462
+ // --- Stepping -------------------------------------------------------------
6463
+ stepSize(magnitude) {
6464
+ if (magnitude === 'normal')
6465
+ return this.step();
6466
+ if (magnitude === 'large')
6467
+ return this.largeStep() ?? this.step() * 10;
6468
+ return this.smallStep() ?? null;
6469
+ }
6470
+ /**
6471
+ * Apply one step. An empty field commits `emptyStepValue ?? min ?? 0`, so ↑
6472
+ * on a blank quantity gives 1 rather than NaN.
6473
+ */
6474
+ applyStep(direction, magnitude = 'normal', announce = true) {
6475
+ if (this.disabled() || this.readOnly())
6476
+ return;
6477
+ const size = this.stepSize(magnitude);
6478
+ if (size == null)
6479
+ return;
6480
+ // Type-then-step should step from what is on screen, not what was committed.
6481
+ if (this.draft() != null)
6482
+ this.commitDraft();
6483
+ if (this.draftInvalid())
6484
+ return;
6485
+ const from = this.value();
6486
+ const current = this.canonical();
6487
+ if (current == null) {
6488
+ const seed = toDecimal(this.emptyStepValue() ?? this.min() ?? 0);
6489
+ this.write(seed);
6490
+ this.stepped.emit({ from, to: Number(seed), by: 0 });
6491
+ if (announce)
6492
+ this.announceValue();
6493
+ return;
6494
+ }
6495
+ const next = stepDecimal(current, direction, {
6496
+ step: size,
6497
+ min: this.min(),
6498
+ max: this.max(),
6499
+ stepOrigin: this.stepOrigin(),
6500
+ wrap: this.wrap(),
6501
+ });
6502
+ if (next === current) {
6503
+ if (announce)
6504
+ this.announceFence(direction);
6505
+ return;
6506
+ }
6507
+ this.write(next);
6508
+ this.stepped.emit({ from, to: Number(next), by: Number(next) - (from ?? 0) });
6509
+ if (announce)
6510
+ this.announceValue();
6511
+ }
6512
+ announceValue() {
6513
+ this.announcer.announce(`${speakNumber(this.canonical(), this.format())}.`);
6514
+ }
6515
+ announceFence(direction) {
6516
+ const bound = direction > 0 ? this.max() : this.min();
6517
+ if (bound == null)
6518
+ return;
6519
+ this.announcer.announce(`${direction > 0 ? 'Maximum' : 'Minimum'}, ${bound}.`);
6520
+ }
6521
+ // --- Hold to repeat -------------------------------------------------------
6522
+ repeatTiming = () => {
5515
6523
  const options = this.componentOptions();
5516
- const fill = this.theme.colors()[options.color ?? 'primary'];
5517
- const track = this.theme.colors()[options.trackColor ?? 'surface-variant'];
5518
- const radius = this.theme.radii()[options.borderRadius ?? 'max'];
5519
- const trackHeight = options.trackHeight ?? 4;
5520
- const thumbSize = options.thumbSize ?? 16;
5521
- // Webkit paints the fill via a gradient stopped at the custom property;
5522
- // Firefox has a real ::-moz-range-progress.
5523
- const trackFill = `linear-gradient(to right, ${fill} var(--uni-slider-fill), ${track} var(--uni-slider-fill))`;
5524
- const thumb = {
5525
- appearance: 'none',
5526
- width: thumbSize,
5527
- height: thumbSize,
5528
- borderRadius: radius,
5529
- border: 'none',
5530
- background: fill,
5531
- cursor: 'pointer',
6524
+ return {
6525
+ delayMs: options.repeatDelayMs,
6526
+ intervalMs: options.repeatIntervalMs,
6527
+ fastIntervalMs: options.repeatFastIntervalMs,
6528
+ rampMs: options.repeatRampMs,
5532
6529
  };
5533
- return css({
5534
- appearance: 'none',
5535
- width: '100%',
5536
- height: thumbSize,
5537
- margin: 0,
5538
- background: 'transparent',
5539
- cursor: 'pointer',
5540
- '&::-webkit-slider-runnable-track': {
5541
- height: trackHeight,
5542
- borderRadius: radius,
5543
- background: trackFill,
5544
- },
5545
- '&::-webkit-slider-thumb': {
5546
- ...thumb,
5547
- marginTop: (trackHeight - thumbSize) / 2,
5548
- },
5549
- '&::-moz-range-track': { height: trackHeight, borderRadius: radius, background: track },
5550
- '&::-moz-range-progress': { height: trackHeight, borderRadius: radius, background: fill },
5551
- '&::-moz-range-thumb': thumb,
5552
- // The shared, themable focus indicator, in the track's fill color.
5553
- '&:focus-visible': { ...this.theme.focusRingStyle(fill) },
5554
- '&:disabled': {
5555
- cursor: 'not-allowed',
5556
- opacity: 0.5,
5557
- '&::-webkit-slider-thumb': { cursor: 'not-allowed' },
5558
- '&::-moz-range-thumb': { cursor: 'not-allowed' },
6530
+ };
6531
+ /**
6532
+ * The live region announces on release only — a screen reader narrating two
6533
+ * hundred intermediate values is a denial of service.
6534
+ */
6535
+ increment = createPressRepeat({
6536
+ onStep: () => this.applyStep(1, 'normal', false),
6537
+ onRelease: () => this.announceValue(),
6538
+ disabled: () => this.disabled() || this.readOnly() || this.atMax(),
6539
+ repeat: () => this.repeat(),
6540
+ timing: this.repeatTiming,
6541
+ });
6542
+ decrement = createPressRepeat({
6543
+ onStep: () => this.applyStep(-1, 'normal', false),
6544
+ onRelease: () => this.announceValue(),
6545
+ disabled: () => this.disabled() || this.readOnly() || this.atMin(),
6546
+ repeat: () => this.repeat(),
6547
+ timing: this.repeatTiming,
6548
+ });
6549
+ // --- Input events ---------------------------------------------------------
6550
+ onInput(text) {
6551
+ this.draft.set(text);
6552
+ // The flag describes a *committed* failure; editing clears it.
6553
+ this.draftInvalid.set(false);
6554
+ }
6555
+ onFocus() {
6556
+ this.focused.set(true);
6557
+ if (this.selectOnFocus()) {
6558
+ queueMicrotask(() => this.inputRef().nativeElement.select());
6559
+ }
6560
+ }
6561
+ onBlur() {
6562
+ this.focused.set(false);
6563
+ this.touched.set(true);
6564
+ if (this.commitOnBlur())
6565
+ this.commitDraft();
6566
+ this.increment.cancel();
6567
+ this.decrement.cancel();
6568
+ }
6569
+ onKeydown(event) {
6570
+ if (this.readOnly())
6571
+ return;
6572
+ const magnitude = event.shiftKey ? 'large' : event.altKey ? 'small' : 'normal';
6573
+ switch (event.key) {
6574
+ case 'ArrowUp':
6575
+ event.preventDefault();
6576
+ this.applyStep(1, magnitude);
6577
+ break;
6578
+ case 'ArrowDown':
6579
+ event.preventDefault();
6580
+ this.applyStep(-1, magnitude);
6581
+ break;
6582
+ case 'PageUp':
6583
+ event.preventDefault();
6584
+ this.applyStep(1, 'large');
6585
+ break;
6586
+ case 'PageDown':
6587
+ event.preventDefault();
6588
+ this.applyStep(-1, 'large');
6589
+ break;
6590
+ case 'Home': {
6591
+ // No-op when unbounded: nothing sensible lives at an open fence.
6592
+ const min = this.min();
6593
+ if (min == null)
6594
+ return;
6595
+ event.preventDefault();
6596
+ this.draft.set(null);
6597
+ this.write(toDecimal(min));
6598
+ this.announceValue();
6599
+ break;
6600
+ }
6601
+ case 'End': {
6602
+ const max = this.max();
6603
+ if (max == null)
6604
+ return;
6605
+ event.preventDefault();
6606
+ this.draft.set(null);
6607
+ this.write(toDecimal(max));
6608
+ this.announceValue();
6609
+ break;
6610
+ }
6611
+ case 'Enter':
6612
+ // Never submit the form while an uncommitted draft is in the field.
6613
+ if (this.draft() != null)
6614
+ event.preventDefault();
6615
+ this.commitDraft();
6616
+ break;
6617
+ case 'Escape':
6618
+ event.preventDefault();
6619
+ this.draft.set(null);
6620
+ this.draftInvalid.set(false);
6621
+ this.increment.cancel();
6622
+ this.decrement.cancel();
6623
+ break;
6624
+ case 'Tab':
6625
+ // Never trap: commit what is typed, then let focus move on.
6626
+ this.commitDraft();
6627
+ break;
6628
+ }
6629
+ }
6630
+ /**
6631
+ * Scroll-to-step, off by default. A *focused* `<input type="number">` changes
6632
+ * value on the wheel, which silently corrupts forms people are merely
6633
+ * scrolling past. When enabled this needs focus **and** hover, and it only
6634
+ * calls `preventDefault` when the value actually moved, so a page does not
6635
+ * get scroll-trapped on a field sitting at its max.
6636
+ */
6637
+ onWheel(event) {
6638
+ if (!this.wheel() || !this.focused() || this.disabled() || this.readOnly())
6639
+ return;
6640
+ const before = this.canonical();
6641
+ this.applyStep(event.deltaY < 0 ? 1 : -1);
6642
+ if (this.canonical() !== before)
6643
+ event.preventDefault();
6644
+ }
6645
+ // --- Styling --------------------------------------------------------------
6646
+ className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
6647
+ fieldRowClass = computed(() => {
6648
+ const options = this.componentOptions();
6649
+ return css([
6650
+ {
6651
+ display: 'flex',
6652
+ alignItems: 'center',
6653
+ width: '100%',
6654
+ // Full field height, so a stretched stepper column is the height of the
6655
+ // field rather than of the text inside it.
6656
+ height: '100%',
6657
+ },
6658
+ this.theme.gap(options.affixGap ?? 'xs'),
6659
+ // Trailing inset, matching the leading one, so a suffix — or the text
6660
+ // itself — does not sit against the border. It rides the row because
6661
+ // `removeInputPlatformStyling` zeroes padding on `& input` at a higher
6662
+ // specificity than this class. Skipped when a stepper holds the trailing
6663
+ // edge: a button is meant to reach the border.
6664
+ this.showSteppers() ? undefined : this.theme.paddingRight(this.trailingInset()),
6665
+ ]);
6666
+ }, ...(ngDevMode ? [{ debugName: "fieldRowClass" }] : /* istanbul ignore next */ []));
6667
+ /** The shared field inset, reused on the trailing side so the two match. */
6668
+ trailingInset = computed(() => this.fieldChrome().paddingLeft, ...(ngDevMode ? [{ debugName: "trailingInset" }] : /* istanbul ignore next */ []));
6669
+ inputClass = computed(() => {
6670
+ const options = this.componentOptions();
6671
+ return css({
6672
+ flex: 1,
6673
+ minWidth: 0,
6674
+ border: 0,
6675
+ outline: 'none',
6676
+ background: 'transparent',
6677
+ color: 'inherit',
6678
+ font: 'inherit',
6679
+ padding: 0,
6680
+ textAlign: this.align() ?? options.align ?? 'start',
6681
+ ...(options.tabularNumerals === false ? {} : { fontVariantNumeric: 'tabular-nums' }),
6682
+ // Colour alone cannot carry "this is not a number" (WCAG 1.4.1), and the
6683
+ // same underline already means the same thing on an invalid uni-tag.
6684
+ ...(this.draftInvalid()
6685
+ ? {
6686
+ textDecoration: 'underline dashed',
6687
+ textUnderlineOffset: 3,
6688
+ textDecorationColor: this.theme.colors()['warn'],
6689
+ }
6690
+ : {}),
6691
+ });
6692
+ }, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
6693
+ /**
6694
+ * The shared field chrome, read from the `input` theme entry — the same entry
6695
+ * `uni-input-box` resolves. Not a duplicate token: the inset has to be the
6696
+ * one every other field uses, or a money field stops lining up with the text
6697
+ * field above it.
6698
+ */
6699
+ fieldChrome = this.theme.getComponentOptions('input');
6700
+ /**
6701
+ * The leading inset for a prefix adornment. When there is a prefix the field
6702
+ * tells the box to stop insetting the `<input>` (`managedInset`) and puts the
6703
+ * inset here instead, so the `$` sits at the field's leading edge with the
6704
+ * number right after it. With no prefix the box keeps doing it — the text is
6705
+ * the leading edge then, and the box's rule outranks this class anyway.
6706
+ * `embedded` fields have no chrome, so they get no inset either.
6707
+ */
6708
+ leadingInset = computed(() => this.embedded() ? undefined : this.theme.paddingLeft(this.fieldChrome().paddingLeft), ...(ngDevMode ? [{ debugName: "leadingInset" }] : /* istanbul ignore next */ []));
6709
+ affixBase() {
6710
+ const options = this.componentOptions();
6711
+ return {
6712
+ flex: 'none',
6713
+ userSelect: 'none',
6714
+ ...this.theme.color(options.affixColor ?? 'on-primary-surface-variant'),
6715
+ };
6716
+ }
6717
+ /** Carries the field's leading inset, being the first thing in the row. */
6718
+ prefixClass = computed(() => css([this.affixBase(), this.leadingInset()]), ...(ngDevMode ? [{ debugName: "prefixClass" }] : /* istanbul ignore next */ []));
6719
+ suffixClass = computed(() => css([this.affixBase()]), ...(ngDevMode ? [{ debugName: "suffixClass" }] : /* istanbul ignore next */ []));
6720
+ /** Shared chrome for every stepper button, in any layout. */
6721
+ stepperButton() {
6722
+ const options = this.componentOptions();
6723
+ const target = options.minTouchTarget ?? 24;
6724
+ return {
6725
+ display: 'grid',
6726
+ placeItems: 'center',
6727
+ flex: 'none',
6728
+ minWidth: target,
6729
+ minHeight: target,
6730
+ padding: 0,
6731
+ border: 0,
6732
+ background: 'transparent',
6733
+ color: 'inherit',
6734
+ cursor: 'pointer',
6735
+ touchAction: 'none',
6736
+ '&:disabled': { opacity: 0.4, cursor: 'not-allowed' },
6737
+ ...this.theme.focusRing(),
6738
+ };
6739
+ }
6740
+ /** Split and trailing layouts: one square button per direction. */
6741
+ stepperClass = computed(() => css({
6742
+ ...this.stepperButton(),
6743
+ width: this.componentOptions().stepperWidth ?? 32,
6744
+ alignSelf: 'stretch',
6745
+ }), ...(ngDevMode ? [{ debugName: "stepperClass" }] : /* istanbul ignore next */ []));
6746
+ /** Stacked layout: two half-height arrows sharing one column. */
6747
+ stackedColumnClass = computed(() => css({
6748
+ display: 'flex',
6749
+ flexDirection: 'column',
6750
+ flex: 'none',
6751
+ alignSelf: 'stretch',
6752
+ justifyContent: 'center',
6753
+ width: this.componentOptions().stepperWidth ?? 32,
6754
+ }), ...(ngDevMode ? [{ debugName: "stackedColumnClass" }] : /* istanbul ignore next */ []));
6755
+ stackedButtonClass = computed(() => css({
6756
+ ...this.stepperButton(),
6757
+ width: '100%',
6758
+ // The two arrows split the field height between them. They cannot each
6759
+ // reach `minTouchTarget` — 2 × 24 does not fit a 32px field — which is
6760
+ // why a coarse pointer gets the `split` layout instead; see `layout`.
6761
+ minHeight: 0,
6762
+ flex: 1,
6763
+ }), ...(ngDevMode ? [{ debugName: "stackedButtonClass" }] : /* istanbul ignore next */ []));
6764
+ glyphSize = computed(() => (this.layout() === 'stacked' ? 12 : 18), ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
6765
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniNumberInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6766
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniNumberInputComponent, isStandalone: true, selector: "uni-number-input, NumberInput", 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 }, valueAsString: { classPropertyName: "valueAsString", publicName: "valueAsString", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, decimals: { classPropertyName: "decimals", publicName: "decimals", isSignal: true, isRequired: false, transformFunction: null }, grouping: { classPropertyName: "grouping", publicName: "grouping", isSignal: true, isRequired: false, transformFunction: null }, numberFormat: { classPropertyName: "numberFormat", publicName: "numberFormat", isSignal: true, isRequired: false, transformFunction: null }, roundingMode: { classPropertyName: "roundingMode", publicName: "roundingMode", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, valueIsFraction: { classPropertyName: "valueIsFraction", publicName: "valueIsFraction", isSignal: true, isRequired: false, transformFunction: null }, unitAnnouncement: { classPropertyName: "unitAnnouncement", publicName: "unitAnnouncement", isSignal: true, isRequired: false, transformFunction: null }, readOnly: { classPropertyName: "readOnly", publicName: "readOnly", isSignal: true, isRequired: false, transformFunction: null }, embedded: { classPropertyName: "embedded", publicName: "embedded", isSignal: true, isRequired: false, 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 }, largeStep: { classPropertyName: "largeStep", publicName: "largeStep", isSignal: true, isRequired: false, transformFunction: null }, smallStep: { classPropertyName: "smallStep", publicName: "smallStep", isSignal: true, isRequired: false, transformFunction: null }, stepOrigin: { classPropertyName: "stepOrigin", publicName: "stepOrigin", isSignal: true, isRequired: false, transformFunction: null }, wrap: { classPropertyName: "wrap", publicName: "wrap", isSignal: true, isRequired: false, transformFunction: null }, clampOnCommit: { classPropertyName: "clampOnCommit", publicName: "clampOnCommit", isSignal: true, isRequired: false, transformFunction: null }, emptyStepValue: { classPropertyName: "emptyStepValue", publicName: "emptyStepValue", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, selectOnFocus: { classPropertyName: "selectOnFocus", publicName: "selectOnFocus", isSignal: true, isRequired: false, transformFunction: null }, allowExpressions: { classPropertyName: "allowExpressions", publicName: "allowExpressions", isSignal: true, isRequired: false, transformFunction: null }, wheel: { classPropertyName: "wheel", publicName: "wheel", isSignal: true, isRequired: false, transformFunction: null }, repeat: { classPropertyName: "repeat", publicName: "repeat", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, stepperLayout: { classPropertyName: "stepperLayout", publicName: "stepperLayout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", valueAsString: "valueAsStringChange", stepped: "stepped", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'numberInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Stepper buttons, shared by every layout. Real <button>s so they are\n announced and activated as buttons, but tabindex=\"-1\": they are pointer\n affordances, and the keyboard route is the arrow keys. The same reasoning\n governs uni-combobox's chevron and uni-tag-input's per-chip remove. -->\n<ng-template #decrementButton let-styleClass=\"styleClass\" let-icon=\"icon\">\n <button\n type=\"button\"\n tabindex=\"-1\"\n [class]=\"styleClass\"\n [disabled]=\"disabled() || atMin()\"\n [attr.aria-label]=\"'Decrease ' + label()\"\n (pointerdown)=\"decrement.press($event)\"\n (pointerup)=\"decrement.release()\"\n (pointercancel)=\"decrement.cancel()\"\n (lostpointercapture)=\"decrement.release()\"\n >\n <uni-icon [name]=\"icon\" [size]=\"glyphSize()\" />\n </button>\n</ng-template>\n\n<ng-template #incrementButton let-styleClass=\"styleClass\" let-icon=\"icon\">\n <button\n type=\"button\"\n tabindex=\"-1\"\n [class]=\"styleClass\"\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]=\"icon\" [size]=\"glyphSize()\" />\n </button>\n</ng-template>\n\n<ng-template #fieldContent>\n <div [class]=\"fieldRowClass()\">\n @if (showSteppers() && layout() === 'split') {\n <ng-container\n [ngTemplateOutlet]=\"decrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().decrementIcon ?? 'minus',\n }\"\n ></ng-container>\n }\n\n <!-- Affixes are adornments, not text: they live outside the <input>, so\n caret math, select-all and paste never step over them, and they are\n aria-hidden because aria-valuetext already speaks them. -->\n @if (format().prefix) {\n <span [class]=\"prefixClass()\" aria-hidden=\"true\">{{ format().prefix }}</span>\n }\n\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readOnly()\"\n [attr.inputmode]=\"format().inputMode\"\n [attr.placeholder]=\"placeholder() ?? null\"\n [attr.aria-label]=\"label()\"\n [attr.aria-valuenow]=\"canonicalForAria()\"\n [attr.aria-valuemin]=\"min() ?? null\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuetext]=\"valueTextForAria()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-readonly]=\"readOnly() ? true : null\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (wheel)=\"onWheel($event)\"\n />\n\n @if (format().suffix) {\n <span [class]=\"suffixClass()\" aria-hidden=\"true\">{{ format().suffix }}</span>\n }\n\n @if (showSteppers()) {\n @switch (layout()) {\n @case ('split') {\n <ng-container\n [ngTemplateOutlet]=\"incrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().incrementIcon ?? 'plus',\n }\"\n ></ng-container>\n }\n @case ('trailing') {\n <ng-container\n [ngTemplateOutlet]=\"decrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().decrementIcon ?? 'minus',\n }\"\n ></ng-container>\n <ng-container\n [ngTemplateOutlet]=\"incrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().incrementIcon ?? 'plus',\n }\"\n ></ng-container>\n }\n @case ('stacked') {\n <span [class]=\"stackedColumnClass()\">\n <ng-container\n [ngTemplateOutlet]=\"incrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stackedButtonClass(),\n icon: componentOptions().stepUpIcon ?? 'chevronUp',\n }\"\n ></ng-container>\n <ng-container\n [ngTemplateOutlet]=\"decrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stackedButtonClass(),\n icon: componentOptions().stepDownIcon ?? 'chevronDown',\n }\"\n ></ng-container>\n </span>\n }\n }\n }\n </div>\n</ng-template>\n\n<!-- `embedded` drops the field chrome for composers that supply their own. -->\n@if (embedded()) {\n <ng-container [ngTemplateOutlet]=\"fieldContent\"></ng-container>\n} @else {\n <!-- The inset moves onto the prefix (or the input when there is none), so an\n adornment sits at the field's leading edge instead of hugging its border. -->\n <uni-input-box\n [error]=\"showError()\"\n [disabled]=\"disabled()\"\n [fullWidth]=\"true\"\n [managedInset]=\"!!format().prefix\"\n >\n <ng-container [ngTemplateOutlet]=\"fieldContent\"></ng-container>\n </uni-input-box>\n}\n\n<!-- Said once per field rather than repeated on both buttons. -->\n<span [id]=\"hintId\" [class]=\"srOnly\">\n Use the up and down arrow keys to change the value.\n</span>\n\n<!-- Clamps, fences, rejections and expression results are each a purely visual\n event otherwise. Held stepping announces on release only. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UniIconComponent, selector: "uni-icon", inputs: ["color", "name", "size"] }, { kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6767
+ }
6768
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniNumberInputComponent, decorators: [{
6769
+ type: Component,
6770
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-number-input, NumberInput', imports: [NgTemplateOutlet, UniIconComponent, UniInputBoxComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'numberInput' }], host: { '[class]': 'className()' }, template: "<!-- Stepper buttons, shared by every layout. Real <button>s so they are\n announced and activated as buttons, but tabindex=\"-1\": they are pointer\n affordances, and the keyboard route is the arrow keys. The same reasoning\n governs uni-combobox's chevron and uni-tag-input's per-chip remove. -->\n<ng-template #decrementButton let-styleClass=\"styleClass\" let-icon=\"icon\">\n <button\n type=\"button\"\n tabindex=\"-1\"\n [class]=\"styleClass\"\n [disabled]=\"disabled() || atMin()\"\n [attr.aria-label]=\"'Decrease ' + label()\"\n (pointerdown)=\"decrement.press($event)\"\n (pointerup)=\"decrement.release()\"\n (pointercancel)=\"decrement.cancel()\"\n (lostpointercapture)=\"decrement.release()\"\n >\n <uni-icon [name]=\"icon\" [size]=\"glyphSize()\" />\n </button>\n</ng-template>\n\n<ng-template #incrementButton let-styleClass=\"styleClass\" let-icon=\"icon\">\n <button\n type=\"button\"\n tabindex=\"-1\"\n [class]=\"styleClass\"\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]=\"icon\" [size]=\"glyphSize()\" />\n </button>\n</ng-template>\n\n<ng-template #fieldContent>\n <div [class]=\"fieldRowClass()\">\n @if (showSteppers() && layout() === 'split') {\n <ng-container\n [ngTemplateOutlet]=\"decrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().decrementIcon ?? 'minus',\n }\"\n ></ng-container>\n }\n\n <!-- Affixes are adornments, not text: they live outside the <input>, so\n caret math, select-all and paste never step over them, and they are\n aria-hidden because aria-valuetext already speaks them. -->\n @if (format().prefix) {\n <span [class]=\"prefixClass()\" aria-hidden=\"true\">{{ format().prefix }}</span>\n }\n\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [value]=\"displayText()\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readOnly()\"\n [attr.inputmode]=\"format().inputMode\"\n [attr.placeholder]=\"placeholder() ?? null\"\n [attr.aria-label]=\"label()\"\n [attr.aria-valuenow]=\"canonicalForAria()\"\n [attr.aria-valuemin]=\"min() ?? null\"\n [attr.aria-valuemax]=\"max() ?? null\"\n [attr.aria-valuetext]=\"valueTextForAria()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-readonly]=\"readOnly() ? true : null\"\n (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (wheel)=\"onWheel($event)\"\n />\n\n @if (format().suffix) {\n <span [class]=\"suffixClass()\" aria-hidden=\"true\">{{ format().suffix }}</span>\n }\n\n @if (showSteppers()) {\n @switch (layout()) {\n @case ('split') {\n <ng-container\n [ngTemplateOutlet]=\"incrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().incrementIcon ?? 'plus',\n }\"\n ></ng-container>\n }\n @case ('trailing') {\n <ng-container\n [ngTemplateOutlet]=\"decrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().decrementIcon ?? 'minus',\n }\"\n ></ng-container>\n <ng-container\n [ngTemplateOutlet]=\"incrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stepperClass(),\n icon: componentOptions().incrementIcon ?? 'plus',\n }\"\n ></ng-container>\n }\n @case ('stacked') {\n <span [class]=\"stackedColumnClass()\">\n <ng-container\n [ngTemplateOutlet]=\"incrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stackedButtonClass(),\n icon: componentOptions().stepUpIcon ?? 'chevronUp',\n }\"\n ></ng-container>\n <ng-container\n [ngTemplateOutlet]=\"decrementButton\"\n [ngTemplateOutletContext]=\"{\n styleClass: stackedButtonClass(),\n icon: componentOptions().stepDownIcon ?? 'chevronDown',\n }\"\n ></ng-container>\n </span>\n }\n }\n }\n </div>\n</ng-template>\n\n<!-- `embedded` drops the field chrome for composers that supply their own. -->\n@if (embedded()) {\n <ng-container [ngTemplateOutlet]=\"fieldContent\"></ng-container>\n} @else {\n <!-- The inset moves onto the prefix (or the input when there is none), so an\n adornment sits at the field's leading edge instead of hugging its border. -->\n <uni-input-box\n [error]=\"showError()\"\n [disabled]=\"disabled()\"\n [fullWidth]=\"true\"\n [managedInset]=\"!!format().prefix\"\n >\n <ng-container [ngTemplateOutlet]=\"fieldContent\"></ng-container>\n </uni-input-box>\n}\n\n<!-- Said once per field rather than repeated on both buttons. -->\n<span [id]=\"hintId\" [class]=\"srOnly\">\n Use the up and down arrow keys to change the value.\n</span>\n\n<!-- Clamps, fences, rejections and expression results are each a purely visual\n event otherwise. Held stepping announces on release only. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n" }]
6771
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], valueAsString: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueAsString", required: false }] }, { type: i0.Output, args: ["valueAsStringChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], decimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "decimals", required: false }] }], grouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "grouping", required: false }] }], numberFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "numberFormat", required: false }] }], roundingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "roundingMode", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], valueIsFraction: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueIsFraction", required: false }] }], unitAnnouncement: [{ type: i0.Input, args: [{ isSignal: true, alias: "unitAnnouncement", required: false }] }], readOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnly", required: false }] }], embedded: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedded", required: false }] }], 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 }] }], largeStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "largeStep", required: false }] }], smallStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "smallStep", required: false }] }], stepOrigin: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepOrigin", required: false }] }], wrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrap", required: false }] }], clampOnCommit: [{ type: i0.Input, args: [{ isSignal: true, alias: "clampOnCommit", required: false }] }], emptyStepValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyStepValue", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], selectOnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectOnFocus", required: false }] }], allowExpressions: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowExpressions", required: false }] }], wheel: [{ type: i0.Input, args: [{ isSignal: true, alias: "wheel", required: false }] }], repeat: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeat", required: false }] }], parse: [{ type: i0.Input, args: [{ isSignal: true, alias: "parse", required: false }] }], stepperLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepperLayout", required: false }] }], stepped: [{ type: i0.Output, args: ["stepped"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
6772
+
6773
+ /**
6774
+ * Bounded numeric input by pointer, for values where the *position* is the
6775
+ * information: volume, opacity, weightings, price filters.
6776
+ *
6777
+ * Custom thumbs rather than `<input type="range">`, which the previous version
6778
+ * used: one native range input cannot carry two thumbs, marks or a tooltip, and
6779
+ * a second component for the range case would mean two keyboard maps to keep in
6780
+ * step. The step model and the keyboard map are the cdk's, shared with the
6781
+ * numeric fields, so nothing new is learned moving between them.
6782
+ *
6783
+ * All arithmetic on values runs through the cdk's exact decimal helpers —
6784
+ * stepping `0.1` never yields `0.30000000000000004`. Only pointer *positions*
6785
+ * use floats, and they are snapped to the grid before becoming a value.
6786
+ */
6787
+ class UniSliderComponent extends BaseComponent {
6788
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
6789
+ /** Shape follows `mode`: a number when `single`, a `UniNumberRange` when `range`. */
6790
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
6791
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
6792
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
6793
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
6794
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
6795
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
6796
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
6797
+ // --- Configuration -------------------------------------------------------
6798
+ /** Accessible name, e.g. "Opacity". Names the group in range mode. */
6799
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
6800
+ mode = input('single', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
6801
+ // `min`/`max` are part of the FormValueControl contract — Signal Forms syncs
6802
+ // them from min()/max() validators — so their type must admit undefined.
6803
+ min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
6804
+ max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
6805
+ step = input(1, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
6806
+ /** `PageUp`/`PageDown` and `Shift+Arrow`. Default: a tenth of the range. */
6807
+ largeStep = input(...(ngDevMode ? [undefined, { debugName: "largeStep" }] : /* istanbul ignore next */ []));
6808
+ /** Fill anchor. Defaults to `min`; set `0` for a slider that spans ±. */
6809
+ origin = input(...(ngDevMode ? [undefined, { debugName: "origin" }] : /* istanbul ignore next */ []));
6810
+ marks = input([], ...(ngDevMode ? [{ debugName: "marks" }] : /* istanbul ignore next */ []));
6811
+ /** Marks become the only valid stops — t-shirt sizing, Likert scales. */
6812
+ snapToMarks = input(false, ...(ngDevMode ? [{ debugName: "snapToMarks" }] : /* istanbul ignore next */ []));
6813
+ /**
6814
+ * Where the current value is shown. `tooltip` appears on hover, focus and
6815
+ * drag; `inline` sits at the trailing edge of the track; `input` seats a
6816
+ * compact `uni-number-input` there, two-way bound to the same value — drag
6817
+ * for the ballpark, type for the exact figure, which is the pairing that
6818
+ * makes bounded numeric input actually usable.
6819
+ *
6820
+ * `input` applies to `single` mode only; a range would need two fields, and
6821
+ * `inline` already reads well for two ends.
6822
+ */
6823
+ valueDisplay = input('none', ...(ngDevMode ? [{ debugName: "valueDisplay" }] : /* istanbul ignore next */ []));
6824
+ /** Overrides how a value is rendered and spoken. */
6825
+ formatValue = input(...(ngDevMode ? [undefined, { debugName: "formatValue" }] : /* istanbul ignore next */ []));
6826
+ /** Enforced distance between the two ends, in range mode. */
6827
+ minGap = input(...(ngDevMode ? [undefined, { debugName: "minGap" }] : /* istanbul ignore next */ []));
6828
+ variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
6829
+ // --- Events --------------------------------------------------------------
6830
+ /** Continuous, during a drag or a held key. Bind this for a live preview. */
6831
+ sliding = output();
6832
+ /**
6833
+ * Committed — on pointer release and key-up. **A form should bind this**:
6834
+ * piping a 60 Hz stream into a model is how sliders get blamed for jank.
6835
+ */
6836
+ changed = output();
6837
+ trackRef = viewChild.required('track');
6838
+ thumbRefs = viewChildren('thumb', ...(ngDevMode ? [{ debugName: "thumbRefs" }] : /* istanbul ignore next */ []));
6839
+ srOnly = css(visuallyHidden);
6840
+ /** Fences and swaps only — `aria-valuetext` already narrates movement. */
6841
+ announcer = createAnnouncer();
6842
+ groupId = uniqueId('uni-slider');
6843
+ /** True from pointerdown until release, to suppress the jump transition. */
6844
+ dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
6845
+ draggingThumb = null;
6846
+ /** Set by keydown, consumed by keyup, so one commit follows a key run. */
6847
+ keyed = false;
6848
+ isRange = computed(() => this.mode() === 'range', ...(ngDevMode ? [{ debugName: "isRange" }] : /* istanbul ignore next */ []));
6849
+ resolvedMin = computed(() => this.min() ?? 0, ...(ngDevMode ? [{ debugName: "resolvedMin" }] : /* istanbul ignore next */ []));
6850
+ resolvedMax = computed(() => this.max() ?? 100, ...(ngDevMode ? [{ debugName: "resolvedMax" }] : /* istanbul ignore next */ []));
6851
+ derivePair(value) {
6852
+ const min = this.resolvedMin();
6853
+ const max = this.resolvedMax();
6854
+ if (this.isRange()) {
6855
+ const range = (value ?? {});
6856
+ return [range.start ?? min, range.end ?? max];
6857
+ }
6858
+ return [typeof value === 'number' ? value : min, max];
6859
+ }
6860
+ /**
6861
+ * The two thumb positions, by **identity** rather than by order: thumb 0 is
6862
+ * whichever thumb the user grabbed first, not necessarily the lower one.
6863
+ *
6864
+ * A `linkedSignal` so an external write to `value` resets them, while a drag
6865
+ * moves them without writing the model on every frame. The computation
6866
+ * deliberately keeps the existing order when the incoming value describes the
6867
+ * same two positions — a commit writes the range back *sorted*, and
6868
+ * re-deriving from that would un-cross a crossed pair and yank the dragged
6869
+ * thumb out from under the pointer mid-drag.
6870
+ */
6871
+ thumbs = linkedSignal({ ...(ngDevMode ? { debugName: "thumbs" } : /* istanbul ignore next */ {}), source: () => this.value(),
6872
+ computation: (value, previous) => {
6873
+ const next = this.derivePair(value);
6874
+ const prior = previous?.value;
6875
+ if (prior &&
6876
+ Math.min(prior[0], prior[1]) === Math.min(next[0], next[1]) &&
6877
+ Math.max(prior[0], prior[1]) === Math.max(next[0], next[1])) {
6878
+ return prior;
6879
+ }
6880
+ return next;
6881
+ } });
6882
+ thumbIndexes = computed(() => (this.isRange() ? [0, 1] : [0]), ...(ngDevMode ? [{ debugName: "thumbIndexes" }] : /* istanbul ignore next */ []));
6883
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
6884
+ /** Default large step: a tenth of the range, snapped to the step grid. */
6885
+ resolvedLargeStep = computed(() => {
6886
+ const explicit = this.largeStep();
6887
+ if (explicit != null)
6888
+ return explicit;
6889
+ const step = this.step();
6890
+ const tenth = (this.resolvedMax() - this.resolvedMin()) / 10;
6891
+ const snapped = Math.round(tenth / step) * step;
6892
+ return snapped > 0 ? snapped : step;
6893
+ }, ...(ngDevMode ? [{ debugName: "resolvedLargeStep" }] : /* istanbul ignore next */ []));
6894
+ numberFormat = computed(() => resolveNumberFormat({ decimals: [0, Math.max(decimalScale(toDecimal(this.step())), 0)] }), ...(ngDevMode ? [{ debugName: "numberFormat" }] : /* istanbul ignore next */ []));
6895
+ formatted(value) {
6896
+ const custom = this.formatValue();
6897
+ if (custom)
6898
+ return custom(value);
6899
+ return formatNumber(toDecimal(value), this.numberFormat());
6900
+ }
6901
+ /** A mark's label speaks for its value, so a marks-only slider says "Medium". */
6902
+ markLabel(value) {
6903
+ return this.marks().find((mark) => mark.value === value && mark.label)?.label;
6904
+ }
6905
+ valueText(value) {
6906
+ return this.markLabel(value) ?? this.formatted(value);
6907
+ }
6908
+ // --- Geometry -------------------------------------------------------------
6909
+ percentOf(value) {
6910
+ const min = this.resolvedMin();
6911
+ const span = this.resolvedMax() - min;
6912
+ if (span <= 0)
6913
+ return 0;
6914
+ return Math.min(100, Math.max(0, ((value - min) / span) * 100));
6915
+ }
6916
+ lowValue = computed(() => {
6917
+ const [a, b] = this.thumbs();
6918
+ return this.isRange() ? Math.min(a, b) : a;
6919
+ }, ...(ngDevMode ? [{ debugName: "lowValue" }] : /* istanbul ignore next */ []));
6920
+ highValue = computed(() => {
6921
+ const [a, b] = this.thumbs();
6922
+ return this.isRange() ? Math.max(a, b) : a;
6923
+ }, ...(ngDevMode ? [{ debugName: "highValue" }] : /* istanbul ignore next */ []));
6924
+ /** The fill spans between the ends in range mode, or origin → value. */
6925
+ fillStart = computed(() => {
6926
+ if (this.isRange())
6927
+ return this.percentOf(this.lowValue());
6928
+ const origin = this.origin() ?? this.resolvedMin();
6929
+ return this.percentOf(Math.min(origin, this.thumbs()[0]));
6930
+ }, ...(ngDevMode ? [{ debugName: "fillStart" }] : /* istanbul ignore next */ []));
6931
+ fillEnd = computed(() => {
6932
+ if (this.isRange())
6933
+ return 100 - this.percentOf(this.highValue());
6934
+ const origin = this.origin() ?? this.resolvedMin();
6935
+ return 100 - this.percentOf(Math.max(origin, this.thumbs()[0]));
6936
+ }, ...(ngDevMode ? [{ debugName: "fillEnd" }] : /* istanbul ignore next */ []));
6937
+ hasMarkLabels = computed(() => this.marks().some((mark) => !!mark.label), ...(ngDevMode ? [{ debugName: "hasMarkLabels" }] : /* istanbul ignore next */ []));
6938
+ /** The number-field readout only makes sense for a single value. */
6939
+ showReadoutField = computed(() => this.valueDisplay() === 'input' && !this.isRange(), ...(ngDevMode ? [{ debugName: "showReadoutField" }] : /* istanbul ignore next */ []));
6940
+ /** Fraction digits the readout should accept, taken from the step. */
6941
+ readoutDecimals = computed(() => [
6942
+ 0,
6943
+ decimalScale(toDecimal(this.step())),
6944
+ ], ...(ngDevMode ? [{ debugName: "readoutDecimals" }] : /* istanbul ignore next */ []));
6945
+ /**
6946
+ * The readout drives the thumb. Guarded against the write-back cycle: the
6947
+ * field is fed from `value`, so a commit here would otherwise bounce.
6948
+ */
6949
+ onReadoutValue(next) {
6950
+ if (next == null)
6951
+ return;
6952
+ const snapped = this.snapToGrid(next);
6953
+ if (snapped === this.thumbs()[0])
6954
+ return;
6955
+ this.setThumb(0, snapped, true);
6956
+ }
6957
+ // --- ARIA per thumb -------------------------------------------------------
6958
+ thumbValue(index) {
6959
+ return this.thumbs()[index];
6960
+ }
6961
+ /**
6962
+ * Each thumb's bound is the *other thumb's* position, so a screen-reader user
6963
+ * is told where the wall actually is rather than where the track ends.
6964
+ */
6965
+ thumbMin(index) {
6966
+ if (!this.isRange())
6967
+ return this.resolvedMin();
6968
+ return this.isLower(index) ? this.resolvedMin() : this.lowValue();
6969
+ }
6970
+ thumbMax(index) {
6971
+ if (!this.isRange())
6972
+ return this.resolvedMax();
6973
+ return this.isLower(index) ? this.highValue() : this.resolvedMax();
6974
+ }
6975
+ /** Thumbs may cross; which one is "minimum" follows position, not identity. */
6976
+ isLower(index) {
6977
+ const [a, b] = this.thumbs();
6978
+ return index === 0 ? a <= b : b < a;
6979
+ }
6980
+ thumbLabel(index) {
6981
+ if (!this.isRange())
6982
+ return this.label();
6983
+ return `${this.label()}, ${this.isLower(index) ? 'minimum' : 'maximum'}`;
6984
+ }
6985
+ // --- Value plumbing -------------------------------------------------------
6986
+ currentValue() {
6987
+ if (!this.isRange())
6988
+ return this.thumbs()[0];
6989
+ return { start: this.lowValue(), end: this.highValue() };
6990
+ }
6991
+ /** Exact `min + n · step`, so a snapped position never carries float drift. */
6992
+ snapToGrid(raw) {
6993
+ const marks = this.marks();
6994
+ if (this.snapToMarks() && marks.length) {
6995
+ return marks.reduce((best, mark) => (Math.abs(mark.value - raw) < Math.abs(best - raw) ? mark.value : best), marks[0].value);
6996
+ }
6997
+ const min = toDecimal(this.resolvedMin());
6998
+ const step = toDecimal(this.step());
6999
+ if (Number(step) === 0)
7000
+ return this.resolvedMin();
7001
+ const steps = Math.round((raw - Number(min)) / Number(step));
7002
+ const scale = Math.max(decimalScale(min), decimalScale(step));
7003
+ const exact = fromScaled(toScaled(min, scale) + BigInt(steps) * toScaled(step, scale), scale);
7004
+ return this.clamp(Number(exact));
7005
+ }
7006
+ clamp(value) {
7007
+ return Math.min(this.resolvedMax(), Math.max(this.resolvedMin(), value));
7008
+ }
7009
+ /**
7010
+ * Move a thumb. `commit` writes the model and emits `changed`; without it the
7011
+ * move is visual and only emits `sliding`.
7012
+ */
7013
+ setThumb(index, next, commit) {
7014
+ let target = this.clamp(next);
7015
+ // With a minimum gap the ends fence each other instead of swapping — the
7016
+ // gap is the whole point of setting it.
7017
+ const gap = this.minGap();
7018
+ if (gap != null && this.isRange()) {
7019
+ const [a, b] = this.thumbs();
7020
+ const other = index === 0 ? b : a;
7021
+ const current = index === 0 ? a : b;
7022
+ if (current <= other)
7023
+ target = Math.min(target, other - gap);
7024
+ else
7025
+ target = Math.max(target, other + gap);
7026
+ target = this.clamp(target);
7027
+ }
7028
+ const previous = this.thumbs()[index];
7029
+ if (previous !== target) {
7030
+ this.thumbs.update((pair) => {
7031
+ const next = [...pair];
7032
+ next[index] = target;
7033
+ return next;
7034
+ });
7035
+ this.sliding.emit(this.currentValue());
7036
+ }
7037
+ if (commit)
7038
+ this.commit();
7039
+ }
7040
+ commit() {
7041
+ this.value.set(this.currentValue());
7042
+ this.changed.emit(this.currentValue());
7043
+ }
7044
+ announceValue(index) {
7045
+ this.announcer.announce(`${this.valueText(this.thumbs()[index])}.`);
7046
+ }
7047
+ // --- Pointer --------------------------------------------------------------
7048
+ /** True when the track is laid out right-to-left. */
7049
+ isRtl() {
7050
+ const track = this.trackRef().nativeElement;
7051
+ return getComputedStyle(track).direction === 'rtl';
7052
+ }
7053
+ /** Pointer x → a raw value. The track's visual direction flips in RTL; the value's does not. */
7054
+ valueFromPointer(event) {
7055
+ const rect = this.trackRef().nativeElement.getBoundingClientRect();
7056
+ if (rect.width <= 0)
7057
+ return this.resolvedMin();
7058
+ let ratio = (event.clientX - rect.left) / rect.width;
7059
+ if (this.isRtl())
7060
+ ratio = 1 - ratio;
7061
+ const min = this.resolvedMin();
7062
+ return min + Math.min(1, Math.max(0, ratio)) * (this.resolvedMax() - min);
7063
+ }
7064
+ nearestThumb(raw) {
7065
+ if (!this.isRange())
7066
+ return 0;
7067
+ const [a, b] = this.thumbs();
7068
+ return Math.abs(a - raw) <= Math.abs(b - raw) ? 0 : 1;
7069
+ }
7070
+ onTrackPointerDown(event) {
7071
+ if (this.disabled())
7072
+ return;
7073
+ event.preventDefault();
7074
+ const raw = this.valueFromPointer(event);
7075
+ const onThumb = event.target?.closest('[role="slider"]');
7076
+ const index = onThumb
7077
+ ? Number(onThumb.dataset['thumb'])
7078
+ : this.nearestThumb(raw);
7079
+ this.draggingThumb = index;
7080
+ this.thumbRefs()[index]?.nativeElement.focus();
7081
+ // Pressing the track jumps the nearest thumb there — no "grab the thumb
7082
+ // first" tax — and that jump animates. A drag never does.
7083
+ if (!onThumb)
7084
+ this.setThumb(index, this.snapToGrid(raw), false);
7085
+ this.dragging.set(true);
7086
+ const track = this.trackRef().nativeElement;
7087
+ if (typeof track.setPointerCapture === 'function') {
7088
+ try {
7089
+ track.setPointerCapture(event.pointerId);
7090
+ }
7091
+ catch {
7092
+ // Synthetic pointer id; the move/up listeners below still work.
7093
+ }
7094
+ }
7095
+ }
7096
+ onTrackPointerMove(event) {
7097
+ if (this.draggingThumb == null)
7098
+ return;
7099
+ this.setThumb(this.draggingThumb, this.snapToGrid(this.valueFromPointer(event)), false);
7100
+ }
7101
+ onTrackPointerUp() {
7102
+ const index = this.draggingThumb;
7103
+ this.dragging.set(false);
7104
+ if (index == null)
7105
+ return;
7106
+ this.draggingThumb = null;
7107
+ this.touched.set(true);
7108
+ this.commit();
7109
+ this.announceValue(index);
7110
+ }
7111
+ // --- Keyboard -------------------------------------------------------------
7112
+ /** Adjacent mark, when marks are the only stops. */
7113
+ markStep(current, direction) {
7114
+ const values = this.marks()
7115
+ .map((mark) => mark.value)
7116
+ .sort((a, b) => a - b);
7117
+ if (!values.length)
7118
+ return current;
7119
+ const at = values.indexOf(current);
7120
+ if (at < 0)
7121
+ return this.snapToGrid(current);
7122
+ return values[Math.min(values.length - 1, Math.max(0, at + direction))];
7123
+ }
7124
+ stepFrom(current, direction, large) {
7125
+ if (this.snapToMarks() && this.marks().length)
7126
+ return this.markStep(current, direction);
7127
+ const next = stepDecimal(toDecimal(current), direction, {
7128
+ step: large ? this.resolvedLargeStep() : this.step(),
7129
+ min: this.resolvedMin(),
7130
+ max: this.resolvedMax(),
7131
+ stepOrigin: 'min',
7132
+ });
7133
+ return Number(next);
7134
+ }
7135
+ onThumbKeydown(event, index) {
7136
+ if (this.disabled())
7137
+ return;
7138
+ const current = this.thumbs()[index];
7139
+ const large = event.shiftKey;
7140
+ // Horizontal arrows follow the picture, so they mirror in RTL; the vertical
7141
+ // ones follow the number and never do (APG's rule).
7142
+ const toward = this.isRtl() ? -1 : 1;
7143
+ // Every branch either assigns or returns, so this needs no initializer.
7144
+ let next;
7145
+ switch (event.key) {
7146
+ case 'ArrowUp':
7147
+ next = this.stepFrom(current, 1, large);
7148
+ break;
7149
+ case 'ArrowDown':
7150
+ next = this.stepFrom(current, -1, large);
7151
+ break;
7152
+ case 'ArrowRight':
7153
+ next = this.stepFrom(current, toward > 0 ? 1 : -1, large);
7154
+ break;
7155
+ case 'ArrowLeft':
7156
+ next = this.stepFrom(current, toward > 0 ? -1 : 1, large);
7157
+ break;
7158
+ case 'PageUp':
7159
+ next = this.stepFrom(current, 1, true);
7160
+ break;
7161
+ case 'PageDown':
7162
+ next = this.stepFrom(current, -1, true);
7163
+ break;
7164
+ case 'Home':
7165
+ next = this.resolvedMin();
7166
+ break;
7167
+ case 'End':
7168
+ next = this.resolvedMax();
7169
+ break;
7170
+ default:
7171
+ return;
7172
+ }
7173
+ event.preventDefault();
7174
+ this.keyed = true;
7175
+ this.setThumb(index, next, false);
7176
+ }
7177
+ /** One commit and one announcement per key run, not per repeat. */
7178
+ onThumbKeyup(index) {
7179
+ if (!this.keyed)
7180
+ return;
7181
+ this.keyed = false;
7182
+ this.touched.set(true);
7183
+ this.commit();
7184
+ this.announceValue(index);
7185
+ }
7186
+ onThumbBlur() {
7187
+ this.touched.set(true);
7188
+ }
7189
+ // --- Styling --------------------------------------------------------------
7190
+ className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
7191
+ fillColor = computed(() => {
7192
+ const colors = this.theme.colors();
7193
+ return colors[this.variant()] ?? colors['primary'];
7194
+ }, ...(ngDevMode ? [{ debugName: "fillColor" }] : /* istanbul ignore next */ []));
7195
+ rootClass = computed(() => css({
7196
+ display: 'flex',
7197
+ flexDirection: 'column',
7198
+ width: '100%',
7199
+ ...(this.disabled() ? { cursor: 'not-allowed' } : {}),
7200
+ }), ...(ngDevMode ? [{ debugName: "rootClass" }] : /* istanbul ignore next */ []));
7201
+ rowClass = computed(() => {
7202
+ const options = this.componentOptions();
7203
+ return css({
7204
+ display: 'flex',
7205
+ alignItems: 'center',
7206
+ ...this.theme.gap(options.labelTypeface ? 'md' : 'md'),
7207
+ });
7208
+ }, ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
7209
+ trackClass = computed(() => {
7210
+ const options = this.componentOptions();
7211
+ const colors = this.theme.colors();
7212
+ const height = options.trackHeight ?? 4;
7213
+ const target = options.minTouchTarget ?? 24;
7214
+ const radius = this.theme.radii()[options.borderRadius ?? 'max'];
7215
+ return css({
7216
+ position: 'relative',
7217
+ flex: '1 1 auto',
7218
+ height,
7219
+ borderRadius: radius,
7220
+ backgroundColor: this.disabled()
7221
+ ? colors['disabled-container']
7222
+ : colors[options.trackColor ?? 'primary-container'],
7223
+ cursor: this.disabled() ? 'not-allowed' : 'pointer',
7224
+ // Vertical room for the hit areas, horizontal room so a thumb at either
7225
+ // fence is not clipped by the track's own box.
7226
+ marginBlock: Math.max(0, (target - height) / 2),
7227
+ marginInline: target / 2,
7228
+ // Only the track: a vertical page scroll starting here still scrolls.
7229
+ touchAction: 'none',
7230
+ });
7231
+ }, ...(ngDevMode ? [{ debugName: "trackClass" }] : /* istanbul ignore next */ []));
7232
+ fillClass = computed(() => {
7233
+ const options = this.componentOptions();
7234
+ const colors = this.theme.colors();
7235
+ const duration = options.transitionMs ?? 120;
7236
+ return css({
7237
+ position: 'absolute',
7238
+ insetBlock: 0,
7239
+ borderRadius: this.theme.radii()[options.borderRadius ?? 'max'],
7240
+ backgroundColor: this.disabled() ? colors['disabled'] : this.fillColor(),
7241
+ ...(this.dragging()
7242
+ ? {}
7243
+ : motionSafe({
7244
+ transitionProperty: 'inset-inline-start, inset-inline-end',
7245
+ transitionDuration: `${duration}ms`,
7246
+ transitionTimingFunction: 'ease',
7247
+ })),
7248
+ });
7249
+ }, ...(ngDevMode ? [{ debugName: "fillClass" }] : /* istanbul ignore next */ []));
7250
+ markClass = computed(() => {
7251
+ const options = this.componentOptions();
7252
+ const colors = this.theme.colors();
7253
+ const size = options.markSize ?? 3;
7254
+ return css({
7255
+ position: 'absolute',
7256
+ top: '50%',
7257
+ width: size,
7258
+ height: size,
7259
+ borderRadius: '50%',
7260
+ backgroundColor: colors[options.markColor ?? 'on-primary-container'],
7261
+ transform: 'translate(-50%, -50%)',
7262
+ opacity: 0.7,
7263
+ pointerEvents: 'none',
7264
+ });
7265
+ }, ...(ngDevMode ? [{ debugName: "markClass" }] : /* istanbul ignore next */ []));
7266
+ thumbClass = computed(() => {
7267
+ const options = this.componentOptions();
7268
+ const colors = this.theme.colors();
7269
+ const size = options.thumbSize ?? 16;
7270
+ const target = options.minTouchTarget ?? 24;
7271
+ const duration = options.transitionMs ?? 120;
7272
+ return css({
7273
+ position: 'absolute',
7274
+ top: '50%',
7275
+ // The hit area is the element; the visual dot is the pseudo-element, so
7276
+ // a 16px thumb still presents a 24px target (WCAG 2.5.8).
7277
+ width: target,
7278
+ height: target,
7279
+ transform: 'translate(-50%, -50%)',
7280
+ display: 'grid',
7281
+ placeItems: 'center',
7282
+ borderRadius: '50%',
7283
+ cursor: this.disabled() ? 'not-allowed' : 'grab',
7284
+ touchAction: 'none',
7285
+ pointerEvents: this.disabled() ? 'none' : 'auto',
7286
+ '&:active': { cursor: 'grabbing' },
7287
+ '&::after': {
7288
+ content: '""',
7289
+ width: size,
7290
+ height: size,
7291
+ borderRadius: this.theme.radii()[options.thumbBorderRadius ?? 'max'],
7292
+ backgroundColor: this.disabled() ? colors['on-disabled'] : this.fillColor(),
7293
+ border: `2px solid ${colors['background']}`,
7294
+ boxSizing: 'border-box',
7295
+ },
7296
+ ...this.theme.focusRing(),
7297
+ ...(this.dragging()
7298
+ ? {}
7299
+ : motionSafe({
7300
+ transitionProperty: 'inset-inline-start',
7301
+ transitionDuration: `${duration}ms`,
7302
+ transitionTimingFunction: 'ease',
7303
+ })),
7304
+ });
7305
+ }, ...(ngDevMode ? [{ debugName: "thumbClass" }] : /* istanbul ignore next */ []));
7306
+ tooltipClass = computed(() => {
7307
+ const options = this.componentOptions();
7308
+ const colors = this.theme.colors();
7309
+ return css({
7310
+ position: 'absolute',
7311
+ bottom: '100%',
7312
+ left: '50%',
7313
+ transform: 'translateX(-50%)',
7314
+ marginBottom: 4,
7315
+ padding: '2px 6px',
7316
+ whiteSpace: 'nowrap',
7317
+ pointerEvents: 'none',
7318
+ opacity: 0,
7319
+ backgroundColor: colors[options.tooltipColor ?? 'inverse-surface'],
7320
+ borderRadius: this.theme.radii()[options.tooltipBorderRadius ?? 'xs'],
7321
+ ...this.theme.color(options.tooltipTextColor ?? 'on-inverse-surface'),
7322
+ ...this.theme.typeface(options.labelTypeface ?? 'label'),
7323
+ ...this.theme.boxShadow(options.tooltipShadow ?? 'menu'),
7324
+ // Shown on hover and focus, and throughout a drag.
7325
+ '[role="slider"]:hover > &, [role="slider"]:focus-visible > &': { opacity: 1 },
7326
+ });
7327
+ }, ...(ngDevMode ? [{ debugName: "tooltipClass" }] : /* istanbul ignore next */ []));
7328
+ labelsClass = computed(() => {
7329
+ const options = this.componentOptions();
7330
+ const target = options.minTouchTarget ?? 24;
7331
+ return css({
7332
+ position: 'relative',
7333
+ height: 18,
7334
+ marginInline: target / 2,
7335
+ });
7336
+ }, ...(ngDevMode ? [{ debugName: "labelsClass" }] : /* istanbul ignore next */ []));
7337
+ labelClass = computed(() => {
7338
+ const options = this.componentOptions();
7339
+ return css({
7340
+ position: 'absolute',
7341
+ transform: 'translateX(-50%)',
7342
+ whiteSpace: 'nowrap',
7343
+ ...this.theme.color(options.labelColor ?? 'on-surface-variant'),
7344
+ ...this.theme.typeface(options.labelTypeface ?? 'label'),
7345
+ });
7346
+ }, ...(ngDevMode ? [{ debugName: "labelClass" }] : /* istanbul ignore next */ []));
7347
+ /** Narrow enough that the track keeps most of the row. */
7348
+ readoutFieldClass = computed(() => css({ flex: 'none', width: 130 }), ...(ngDevMode ? [{ debugName: "readoutFieldClass" }] : /* istanbul ignore next */ []));
7349
+ readoutClass = computed(() => {
7350
+ const options = this.componentOptions();
7351
+ return css({
7352
+ flex: 'none',
7353
+ minWidth: '4ch',
7354
+ textAlign: 'end',
7355
+ fontVariantNumeric: 'tabular-nums',
7356
+ ...this.theme.color(options.labelColor ?? 'on-surface-variant'),
7357
+ ...this.theme.typeface(options.labelTypeface ?? 'label'),
7358
+ });
7359
+ }, ...(ngDevMode ? [{ debugName: "readoutClass" }] : /* istanbul ignore next */ []));
7360
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSliderComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7361
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniSliderComponent, isStandalone: true, selector: "uni-slider, Slider", 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 }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, 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 }, largeStep: { classPropertyName: "largeStep", publicName: "largeStep", isSignal: true, isRequired: false, transformFunction: null }, origin: { classPropertyName: "origin", publicName: "origin", isSignal: true, isRequired: false, transformFunction: null }, marks: { classPropertyName: "marks", publicName: "marks", isSignal: true, isRequired: false, transformFunction: null }, snapToMarks: { classPropertyName: "snapToMarks", publicName: "snapToMarks", isSignal: true, isRequired: false, transformFunction: null }, valueDisplay: { classPropertyName: "valueDisplay", publicName: "valueDisplay", isSignal: true, isRequired: false, transformFunction: null }, formatValue: { classPropertyName: "formatValue", publicName: "formatValue", isSignal: true, isRequired: false, transformFunction: null }, minGap: { classPropertyName: "minGap", publicName: "minGap", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", sliding: "sliding", changed: "changed" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'slider' }], viewQueries: [{ propertyName: "trackRef", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "thumbRefs", predicate: ["thumb"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Range mode is two questions, so it is a group with two tab stops; single\n mode needs no wrapper role \u2014 the thumb itself is the control. -->\n<div\n [class]=\"rootClass()\"\n [attr.role]=\"isRange() ? 'group' : null\"\n [attr.aria-label]=\"isRange() ? label() : null\"\n>\n <div [class]=\"rowClass()\">\n <!-- Pointer move/up ride the track because it holds the pointer capture,\n so a drag that leaves the element still tracks and still releases. -->\n <div\n #track\n [class]=\"trackClass()\"\n (pointerdown)=\"onTrackPointerDown($event)\"\n (pointermove)=\"onTrackPointerMove($event)\"\n (pointerup)=\"onTrackPointerUp()\"\n (pointercancel)=\"onTrackPointerUp()\"\n (lostpointercapture)=\"onTrackPointerUp()\"\n >\n <div\n [class]=\"fillClass()\"\n [style.inset-inline-start.%]=\"fillStart()\"\n [style.inset-inline-end.%]=\"fillEnd()\"\n ></div>\n\n @for (mark of marks(); track mark.value) {\n <div [class]=\"markClass()\" [style.inset-inline-start.%]=\"percentOf(mark.value)\"></div>\n }\n\n @for (index of thumbIndexes(); track index) {\n <div\n #thumb\n role=\"slider\"\n tabindex=\"0\"\n [attr.data-thumb]=\"index\"\n [class]=\"thumbClass()\"\n [style.inset-inline-start.%]=\"percentOf(thumbValue(index))\"\n [attr.aria-label]=\"thumbLabel(index)\"\n [attr.aria-valuenow]=\"thumbValue(index)\"\n [attr.aria-valuemin]=\"thumbMin(index)\"\n [attr.aria-valuemax]=\"thumbMax(index)\"\n [attr.aria-valuetext]=\"valueText(thumbValue(index))\"\n [attr.aria-orientation]=\"'horizontal'\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (keydown)=\"onThumbKeydown($event, index)\"\n (keyup)=\"onThumbKeyup(index)\"\n (blur)=\"onThumbBlur()\"\n >\n @if (valueDisplay() === 'tooltip') {\n <span [class]=\"tooltipClass()\" aria-hidden=\"true\">\n {{ valueText(thumbValue(index)) }}\n </span>\n }\n </div>\n }\n </div>\n\n @if (showReadoutField()) {\n <!-- The field is the slider's precise-entry escape hatch: drag for the\n ballpark, type for the exact value. It carries its own label because\n it is a second tab stop, and the thumb's ARIA already names the\n slider itself. -->\n <uni-number-input\n [class]=\"readoutFieldClass()\"\n [label]=\"label() + ' value'\"\n [value]=\"thumbValue(0)\"\n [min]=\"resolvedMin()\"\n [max]=\"resolvedMax()\"\n [step]=\"step()\"\n [decimals]=\"readoutDecimals()\"\n [disabled]=\"disabled()\"\n align=\"end\"\n (valueChange)=\"onReadoutValue($event)\"\n />\n }\n\n @if (valueDisplay() === 'inline') {\n <!-- Duplicated from aria-valuetext, so it is hidden from the reader. -->\n <span [class]=\"readoutClass()\" aria-hidden=\"true\">\n @if (isRange()) {\n {{ valueText(lowValue()) }} \u2013 {{ valueText(highValue()) }}\n } @else {\n {{ valueText(thumbValue(0)) }}\n }\n </span>\n }\n </div>\n\n @if (hasMarkLabels()) {\n <!-- Presentational: the text is folded into aria-valuetext at the matching\n value rather than being separately focusable. -->\n <div [class]=\"labelsClass()\" role=\"presentation\">\n @for (mark of marks(); track mark.value) {\n @if (mark.label) {\n <span [class]=\"labelClass()\" [style.inset-inline-start.%]=\"percentOf(mark.value)\">\n {{ mark.label }}\n </span>\n }\n }\n </div>\n }\n\n <!-- Fences and swaps only; ordinary movement is already narrated by\n aria-valuetext, and doubling it is noise. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniNumberInputComponent, selector: "uni-number-input, NumberInput", inputs: ["value", "disabled", "touched", "invalid", "dirty", "required", "ariaDescribedBy", "valueAsString", "label", "placeholder", "preset", "currency", "locale", "prefix", "suffix", "decimals", "grouping", "numberFormat", "roundingMode", "align", "valueIsFraction", "unitAnnouncement", "readOnly", "embedded", "min", "max", "step", "largeStep", "smallStep", "stepOrigin", "wrap", "clampOnCommit", "emptyStepValue", "commitOnBlur", "selectOnFocus", "allowExpressions", "wheel", "repeat", "parse", "stepperLayout"], outputs: ["valueChange", "touchedChange", "valueAsStringChange", "stepped", "rejected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7362
+ }
7363
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSliderComponent, decorators: [{
7364
+ type: Component,
7365
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-slider, Slider', imports: [UniNumberInputComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'slider' }], host: { '[class]': 'className()' }, template: "<!-- Range mode is two questions, so it is a group with two tab stops; single\n mode needs no wrapper role \u2014 the thumb itself is the control. -->\n<div\n [class]=\"rootClass()\"\n [attr.role]=\"isRange() ? 'group' : null\"\n [attr.aria-label]=\"isRange() ? label() : null\"\n>\n <div [class]=\"rowClass()\">\n <!-- Pointer move/up ride the track because it holds the pointer capture,\n so a drag that leaves the element still tracks and still releases. -->\n <div\n #track\n [class]=\"trackClass()\"\n (pointerdown)=\"onTrackPointerDown($event)\"\n (pointermove)=\"onTrackPointerMove($event)\"\n (pointerup)=\"onTrackPointerUp()\"\n (pointercancel)=\"onTrackPointerUp()\"\n (lostpointercapture)=\"onTrackPointerUp()\"\n >\n <div\n [class]=\"fillClass()\"\n [style.inset-inline-start.%]=\"fillStart()\"\n [style.inset-inline-end.%]=\"fillEnd()\"\n ></div>\n\n @for (mark of marks(); track mark.value) {\n <div [class]=\"markClass()\" [style.inset-inline-start.%]=\"percentOf(mark.value)\"></div>\n }\n\n @for (index of thumbIndexes(); track index) {\n <div\n #thumb\n role=\"slider\"\n tabindex=\"0\"\n [attr.data-thumb]=\"index\"\n [class]=\"thumbClass()\"\n [style.inset-inline-start.%]=\"percentOf(thumbValue(index))\"\n [attr.aria-label]=\"thumbLabel(index)\"\n [attr.aria-valuenow]=\"thumbValue(index)\"\n [attr.aria-valuemin]=\"thumbMin(index)\"\n [attr.aria-valuemax]=\"thumbMax(index)\"\n [attr.aria-valuetext]=\"valueText(thumbValue(index))\"\n [attr.aria-orientation]=\"'horizontal'\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.aria-describedby]=\"ariaDescribedBy() || null\"\n (keydown)=\"onThumbKeydown($event, index)\"\n (keyup)=\"onThumbKeyup(index)\"\n (blur)=\"onThumbBlur()\"\n >\n @if (valueDisplay() === 'tooltip') {\n <span [class]=\"tooltipClass()\" aria-hidden=\"true\">\n {{ valueText(thumbValue(index)) }}\n </span>\n }\n </div>\n }\n </div>\n\n @if (showReadoutField()) {\n <!-- The field is the slider's precise-entry escape hatch: drag for the\n ballpark, type for the exact value. It carries its own label because\n it is a second tab stop, and the thumb's ARIA already names the\n slider itself. -->\n <uni-number-input\n [class]=\"readoutFieldClass()\"\n [label]=\"label() + ' value'\"\n [value]=\"thumbValue(0)\"\n [min]=\"resolvedMin()\"\n [max]=\"resolvedMax()\"\n [step]=\"step()\"\n [decimals]=\"readoutDecimals()\"\n [disabled]=\"disabled()\"\n align=\"end\"\n (valueChange)=\"onReadoutValue($event)\"\n />\n }\n\n @if (valueDisplay() === 'inline') {\n <!-- Duplicated from aria-valuetext, so it is hidden from the reader. -->\n <span [class]=\"readoutClass()\" aria-hidden=\"true\">\n @if (isRange()) {\n {{ valueText(lowValue()) }} \u2013 {{ valueText(highValue()) }}\n } @else {\n {{ valueText(thumbValue(0)) }}\n }\n </span>\n }\n </div>\n\n @if (hasMarkLabels()) {\n <!-- Presentational: the text is folded into aria-valuetext at the matching\n value rather than being separately focusable. -->\n <div [class]=\"labelsClass()\" role=\"presentation\">\n @for (mark of marks(); track mark.value) {\n @if (mark.label) {\n <span [class]=\"labelClass()\" [style.inset-inline-start.%]=\"percentOf(mark.value)\">\n {{ mark.label }}\n </span>\n }\n }\n </div>\n }\n\n <!-- Fences and swaps only; ordinary movement is already narrated by\n aria-valuetext, and doubling it is noise. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n" }]
7366
+ }], 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 }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], 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 }] }], largeStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "largeStep", required: false }] }], origin: [{ type: i0.Input, args: [{ isSignal: true, alias: "origin", required: false }] }], marks: [{ type: i0.Input, args: [{ isSignal: true, alias: "marks", required: false }] }], snapToMarks: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapToMarks", required: false }] }], valueDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueDisplay", required: false }] }], formatValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatValue", required: false }] }], minGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "minGap", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], sliding: [{ type: i0.Output, args: ["sliding"] }], changed: [{ type: i0.Output, args: ["changed"] }], trackRef: [{ type: i0.ViewChild, args: ['track', { isSignal: true }] }], thumbRefs: [{ type: i0.ViewChildren, args: ['thumb', { isSignal: true }] }] } });
7367
+
7368
+ /**
7369
+ * Two linked numeric fields in one chrome, with one `{ start, end }` value —
7370
+ * price filters, thresholds, tolerances.
7371
+ *
7372
+ * `start`/`end` deliberately match `UniDateRange`, so the library has one range
7373
+ * vocabulary, and they avoid colliding with the `min`/`max` **inputs**, which
7374
+ * mean the fence rather than the value.
7375
+ *
7376
+ * It owns its commit path rather than nesting two `uni-number-input`s, because
7377
+ * the two behaviours the spec asks for need *different* bounds: a stepper must
7378
+ * be fenced at the other end, while a typed commit must reach the parent
7379
+ * un-clamped so a backwards range can be swapped instead of destroyed. A child
7380
+ * field applies one bound pair to both. The arithmetic, parsing and formatting
7381
+ * are still the cdk's, shared with every other numeric control.
7382
+ */
7383
+ class UniNumberRangeInputComponent extends BaseComponent {
7384
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
7385
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
7386
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
7387
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
7388
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
7389
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
7390
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
7391
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
7392
+ // --- Configuration -------------------------------------------------------
7393
+ /** Names the group, e.g. "Price range". */
7394
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
7395
+ startLabel = input('Minimum', ...(ngDevMode ? [{ debugName: "startLabel" }] : /* istanbul ignore next */ []));
7396
+ endLabel = input('Maximum', ...(ngDevMode ? [{ debugName: "endLabel" }] : /* istanbul ignore next */ []));
7397
+ // Forwarded to both parts, so the two ends always read alike.
7398
+ preset = input('decimal', ...(ngDevMode ? [{ debugName: "preset" }] : /* istanbul ignore next */ []));
7399
+ currency = input(...(ngDevMode ? [undefined, { debugName: "currency" }] : /* istanbul ignore next */ []));
7400
+ locale = input(...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
7401
+ prefix = input(...(ngDevMode ? [undefined, { debugName: "prefix" }] : /* istanbul ignore next */ []));
7402
+ suffix = input(...(ngDevMode ? [undefined, { debugName: "suffix" }] : /* istanbul ignore next */ []));
7403
+ decimals = input(...(ngDevMode ? [undefined, { debugName: "decimals" }] : /* istanbul ignore next */ []));
7404
+ grouping = input(...(ngDevMode ? [undefined, { debugName: "grouping" }] : /* istanbul ignore next */ []));
7405
+ roundingMode = input('half-up', ...(ngDevMode ? [{ debugName: "roundingMode" }] : /* istanbul ignore next */ []));
7406
+ placeholderStart = input(...(ngDevMode ? [undefined, { debugName: "placeholderStart" }] : /* istanbul ignore next */ []));
7407
+ placeholderEnd = input(...(ngDevMode ? [undefined, { debugName: "placeholderEnd" }] : /* istanbul ignore next */ []));
7408
+ // `min`/`max` are part of the FormValueControl contract — Signal Forms syncs
7409
+ // them from min()/max() validators — so their type must admit undefined.
7410
+ min = input(...(ngDevMode ? [undefined, { debugName: "min" }] : /* istanbul ignore next */ []));
7411
+ max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
7412
+ step = input(1, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
7413
+ /** Enforced distance between the two ends. */
7414
+ minGap = input(...(ngDevMode ? [undefined, { debugName: "minGap" }] : /* istanbul ignore next */ []));
7415
+ // --- Events --------------------------------------------------------------
7416
+ /** The ends were entered backwards and have been exchanged. */
7417
+ swapped = output();
7418
+ /** A typed commit on one end could not be read; its text stays in place. */
7419
+ rejected = output();
7420
+ inputRefs = viewChildren('field', ...(ngDevMode ? [{ debugName: "inputRefs" }] : /* istanbul ignore next */ []));
7421
+ srOnly = css(visuallyHidden);
7422
+ announcer = createAnnouncer();
7423
+ hintId = uniqueId('uni-number-range-hint');
7424
+ groupId = uniqueId('uni-number-range');
7425
+ /** Uncommitted text per part. `null` means "show the committed value". */
7426
+ drafts = signal({
7427
+ start: null,
7428
+ end: null,
7429
+ }, ...(ngDevMode ? [{ debugName: "drafts" }] : /* istanbul ignore next */ []));
7430
+ focusedPart = signal(null, ...(ngDevMode ? [{ debugName: "focusedPart" }] : /* istanbul ignore next */ []));
7431
+ invalidPart = signal(null, ...(ngDevMode ? [{ debugName: "invalidPart" }] : /* istanbul ignore next */ []));
7432
+ parts = ['start', 'end'];
7433
+ fieldChrome = this.theme.getComponentOptions('input');
7434
+ format = computed(() => resolveNumberFormat({
7435
+ preset: this.preset(),
7436
+ currency: this.currency(),
7437
+ locale: this.locale() ?? (document.documentElement.lang || navigator.language || 'en-US'),
7438
+ decimals: this.decimals(),
7439
+ grouping: this.grouping(),
7440
+ prefix: this.prefix(),
7441
+ suffix: this.suffix(),
7442
+ roundingMode: this.roundingMode(),
7443
+ min: this.min(),
7444
+ }), ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
7445
+ /** Form-level error, which belongs to both ends. */
7446
+ formError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "formError" }] : /* istanbul ignore next */ []));
7447
+ /**
7448
+ * Box-level error. A refused draft in *one* end flags the shared chrome, but
7449
+ * must not flag the other end's input — that end is fine.
7450
+ */
7451
+ showError = computed(() => this.formError() || this.invalidPart() != null, ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
7452
+ describedBy = computed(() => [this.ariaDescribedBy(), this.hintId].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
7453
+ // --- Per-part reads -------------------------------------------------------
7454
+ /** The committed canonical decimal for a part, or `null` when that end is open. */
7455
+ canonicalOf(part) {
7456
+ const range = this.value();
7457
+ const raw = part === 'start' ? range?.start : range?.end;
7458
+ return raw == null ? null : toDecimal(raw);
7459
+ }
7460
+ valueOf(part) {
7461
+ const range = this.value();
7462
+ return (part === 'start' ? range?.start : range?.end) ?? null;
7463
+ }
7464
+ displayText(part) {
7465
+ const draft = this.drafts()[part];
7466
+ if (draft != null)
7467
+ return draft;
7468
+ const canonical = this.canonicalOf(part);
7469
+ if (canonical == null)
7470
+ return '';
7471
+ return this.focusedPart() === part
7472
+ ? rawNumberText(canonical, this.format())
7473
+ : formatNumber(canonical, this.format());
7474
+ }
7475
+ partLabel(part) {
7476
+ return `${this.label()}, ${part === 'start' ? this.startLabel() : this.endLabel()}`;
7477
+ }
7478
+ valueTextOf(part) {
7479
+ return speakNumber(this.canonicalOf(part), this.format());
7480
+ }
7481
+ isInvalid(part) {
7482
+ return this.invalidPart() === part;
7483
+ }
7484
+ // --- Fences ---------------------------------------------------------------
7485
+ /** Exact `a ± b` without a float, for the gap arithmetic. */
7486
+ shiftBy(value, by, direction) {
7487
+ const scale = Math.max(decimalScale(value), decimalScale(by));
7488
+ const moved = toScaled(value, scale) + BigInt(direction) * toScaled(by, scale);
7489
+ return fromScaled(moved, scale);
7490
+ }
7491
+ /**
7492
+ * The fence a part's **stepping** and its ARIA see: the other end, held off
7493
+ * by `minGap`, intersected with the outer bounds. This is deliberately
7494
+ * tighter than what a typed commit is measured against — the steppers must
7495
+ * not walk one end through the other, while typing a backwards range should
7496
+ * be swapped rather than clamped away.
7497
+ */
7498
+ stepFence(part) {
7499
+ const gap = toDecimal(this.minGap() ?? 0);
7500
+ const outerMin = this.min();
7501
+ const outerMax = this.max();
7502
+ if (part === 'start') {
7503
+ const other = this.canonicalOf('end');
7504
+ if (other == null)
7505
+ return { min: outerMin, max: outerMax };
7506
+ const cap = this.shiftBy(other, gap, -1);
7507
+ const capped = outerMax != null && compareDecimal(cap, toDecimal(outerMax)) > 0 ? outerMax : Number(cap);
7508
+ return { min: outerMin, max: capped };
7509
+ }
7510
+ const other = this.canonicalOf('start');
7511
+ if (other == null)
7512
+ return { min: outerMin, max: outerMax };
7513
+ const floor = this.shiftBy(other, gap, 1);
7514
+ const floored = outerMin != null && compareDecimal(floor, toDecimal(outerMin)) < 0 ? outerMin : Number(floor);
7515
+ return { min: floored, max: outerMax };
7516
+ }
7517
+ // --- Writing --------------------------------------------------------------
7518
+ writeRange(start, end) {
7519
+ if (start == null && end == null) {
7520
+ this.value.set(null);
7521
+ return;
7522
+ }
7523
+ this.value.set({
7524
+ ...(start == null ? {} : { start: Number(start) }),
7525
+ ...(end == null ? {} : { end: Number(end) }),
7526
+ });
7527
+ }
7528
+ onInput(part, text) {
7529
+ this.drafts.update((drafts) => ({ ...drafts, [part]: text }));
7530
+ if (this.invalidPart() === part)
7531
+ this.invalidPart.set(null);
7532
+ }
7533
+ /**
7534
+ * Commit one part. Out-of-range clamps to the **outer** bounds only, so the
7535
+ * other end never destroys what was typed; the ends are then reconciled.
7536
+ */
7537
+ commitPart(part) {
7538
+ const draft = this.drafts()[part];
7539
+ if (draft == null)
7540
+ return;
7541
+ const result = parseNumber(draft, this.format(), { currency: this.currency() });
7542
+ if (result.status === 'error') {
7543
+ this.invalidPart.set(part);
7544
+ this.rejected.emit({ part, raw: draft, reason: result.reason });
7545
+ this.announcer.announce(`${draft} is not a number.`);
7546
+ return;
7547
+ }
7548
+ this.drafts.update((drafts) => ({ ...drafts, [part]: null }));
7549
+ this.invalidPart.set(null);
7550
+ let start = this.canonicalOf('start');
7551
+ let end = this.canonicalOf('end');
7552
+ if (result.status === 'empty') {
7553
+ if (part === 'start')
7554
+ start = null;
7555
+ else
7556
+ end = null;
7557
+ this.writeRange(start, end);
7558
+ return;
7559
+ }
7560
+ const settled = clampDecimal(settleNumber(result.value, this.format()), this.min(), this.max()).value;
7561
+ if (part === 'start')
7562
+ start = settled;
7563
+ else
7564
+ end = settled;
7565
+ this.reconcile(part, start, end);
7566
+ }
7567
+ /**
7568
+ * Put the two ends in order. A backwards pair is **swapped**, not refused —
7569
+ * the same rule `uni-calendar` applies to a backwards date range, because the
7570
+ * user pointed at the range they meant. Otherwise `minGap` is honoured by
7571
+ * pushing the end that was just edited back to the boundary, which is what
7572
+ * makes stepping behave as a fence rather than dragging the other end along.
7573
+ */
7574
+ reconcile(edited, start, end) {
7575
+ if (start != null && end != null) {
7576
+ if (compareDecimal(start, end) > 0) {
7577
+ const swapped = { start: Number(end), end: Number(start) };
7578
+ this.value.set(swapped);
7579
+ this.swapped.emit(swapped);
7580
+ this.announcer.announce(`Range ${formatNumber(end, this.format())} to ${formatNumber(start, this.format())}. Ends swapped.`);
7581
+ return;
7582
+ }
7583
+ const gap = toDecimal(this.minGap() ?? 0);
7584
+ if (Number(gap) > 0) {
7585
+ const distance = this.shiftBy(end, start, -1);
7586
+ if (compareDecimal(distance, gap) < 0) {
7587
+ if (edited === 'end')
7588
+ end = this.shiftBy(start, gap, 1);
7589
+ else
7590
+ start = this.shiftBy(end, gap, -1);
7591
+ this.announcer.announce(`Kept ${formatNumber(gap, this.format())} between the ends.`);
7592
+ }
7593
+ }
7594
+ }
7595
+ this.writeRange(start, end);
7596
+ }
7597
+ // --- Stepping -------------------------------------------------------------
7598
+ applyStep(part, direction, large = false) {
7599
+ if (this.disabled())
7600
+ return;
7601
+ if (this.drafts()[part] != null)
7602
+ this.commitPart(part);
7603
+ const fence = this.stepFence(part);
7604
+ const current = this.canonicalOf(part);
7605
+ if (current == null) {
7606
+ const seed = toDecimal(fence.min ?? this.min() ?? 0);
7607
+ this.reconcile(part, part === 'start' ? seed : this.canonicalOf('start'), part === 'end' ? seed : this.canonicalOf('end'));
7608
+ this.announceValue(part);
7609
+ return;
7610
+ }
7611
+ const next = stepDecimal(current, direction, {
7612
+ step: large ? this.step() * 10 : this.step(),
7613
+ min: fence.min,
7614
+ max: fence.max,
7615
+ stepOrigin: 'min',
7616
+ });
7617
+ if (next === current) {
7618
+ const bound = direction > 0 ? fence.max : fence.min;
7619
+ if (bound != null) {
7620
+ this.announcer.announce(`${direction > 0 ? 'Maximum' : 'Minimum'}, ${bound}.`);
7621
+ }
7622
+ return;
7623
+ }
7624
+ this.reconcile(part, part === 'start' ? next : this.canonicalOf('start'), part === 'end' ? next : this.canonicalOf('end'));
7625
+ this.announceValue(part);
7626
+ }
7627
+ announceValue(part) {
7628
+ this.announcer.announce(`${this.valueTextOf(part)}.`);
7629
+ }
7630
+ // --- Events ---------------------------------------------------------------
7631
+ onFocus(part) {
7632
+ this.focusedPart.set(part);
7633
+ }
7634
+ onBlur(part) {
7635
+ if (this.focusedPart() === part)
7636
+ this.focusedPart.set(null);
7637
+ this.touched.set(true);
7638
+ this.commitPart(part);
7639
+ }
7640
+ onKeydown(event, part) {
7641
+ const fence = this.stepFence(part);
7642
+ switch (event.key) {
7643
+ case 'ArrowUp':
7644
+ event.preventDefault();
7645
+ this.applyStep(part, 1, event.shiftKey);
7646
+ break;
7647
+ case 'ArrowDown':
7648
+ event.preventDefault();
7649
+ this.applyStep(part, -1, event.shiftKey);
7650
+ break;
7651
+ case 'PageUp':
7652
+ event.preventDefault();
7653
+ this.applyStep(part, 1, true);
7654
+ break;
7655
+ case 'PageDown':
7656
+ event.preventDefault();
7657
+ this.applyStep(part, -1, true);
7658
+ break;
7659
+ case 'Home':
7660
+ if (fence.min == null)
7661
+ return;
7662
+ event.preventDefault();
7663
+ this.drafts.update((d) => ({ ...d, [part]: null }));
7664
+ this.reconcile(part, part === 'start' ? toDecimal(fence.min) : this.canonicalOf('start'), part === 'end' ? toDecimal(fence.min) : this.canonicalOf('end'));
7665
+ break;
7666
+ case 'End':
7667
+ if (fence.max == null)
7668
+ return;
7669
+ event.preventDefault();
7670
+ this.drafts.update((d) => ({ ...d, [part]: null }));
7671
+ this.reconcile(part, part === 'start' ? toDecimal(fence.max) : this.canonicalOf('start'), part === 'end' ? toDecimal(fence.max) : this.canonicalOf('end'));
7672
+ break;
7673
+ case 'Enter':
7674
+ if (this.drafts()[part] != null)
7675
+ event.preventDefault();
7676
+ this.commitPart(part);
7677
+ break;
7678
+ case 'Escape':
7679
+ event.preventDefault();
7680
+ this.drafts.update((d) => ({ ...d, [part]: null }));
7681
+ this.invalidPart.set(null);
7682
+ break;
7683
+ case 'Tab':
7684
+ this.commitPart(part);
7685
+ break;
7686
+ }
7687
+ }
7688
+ // --- Styling --------------------------------------------------------------
7689
+ className = computed(() => css({ display: 'block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
7690
+ rowClass = computed(() => {
7691
+ const options = this.componentOptions();
7692
+ return css([
7693
+ {
7694
+ display: 'flex',
7695
+ alignItems: 'center',
7696
+ width: '100%',
7697
+ height: '100%',
5559
7698
  },
7699
+ this.theme.gap(options.partGap ?? 'sm'),
7700
+ // Both insets ride the row, not the `<input>`s: uni-input-box styles
7701
+ // `& input` at a higher specificity than this class can reach, so an
7702
+ // inset set on an input is silently overridden. See `managedInset`,
7703
+ // which is why the box is not applying the leading one either. There are
7704
+ // never steppers here, so the trailing edge always gets one — otherwise
7705
+ // the upper end's suffix sits against the border.
7706
+ this.theme.paddingLeft(this.fieldChrome().paddingLeft),
7707
+ this.theme.paddingRight(this.fieldChrome().paddingLeft),
7708
+ ]);
7709
+ }, ...(ngDevMode ? [{ debugName: "rowClass" }] : /* istanbul ignore next */ []));
7710
+ /** Each end is its own `[prefix][number][suffix]` group. */
7711
+ partWrapClass = computed(() => {
7712
+ const options = this.componentOptions();
7713
+ return css([
7714
+ { display: 'flex', alignItems: 'center', flex: '1 1 0', minWidth: 0 },
7715
+ this.theme.gap(options.affixGap ?? 'xs'),
7716
+ ]);
7717
+ }, ...(ngDevMode ? [{ debugName: "partWrapClass" }] : /* istanbul ignore next */ []));
7718
+ affixClass = computed(() => {
7719
+ const options = this.componentOptions();
7720
+ return css([
7721
+ { flex: 'none', userSelect: 'none' },
7722
+ this.theme.color(options.affixColor ?? 'on-primary-surface-variant'),
7723
+ ]);
7724
+ }, ...(ngDevMode ? [{ debugName: "affixClass" }] : /* istanbul ignore next */ []));
7725
+ partClass = computed(() => css([this.partBase()]), ...(ngDevMode ? [{ debugName: "partClass" }] : /* istanbul ignore next */ []));
7726
+ partBase() {
7727
+ return {
7728
+ flex: '1 1 0',
7729
+ minWidth: 0,
7730
+ border: 0,
7731
+ outline: 'none',
7732
+ background: 'transparent',
7733
+ color: 'inherit',
7734
+ font: 'inherit',
7735
+ padding: 0,
7736
+ fontVariantNumeric: 'tabular-nums',
7737
+ };
7738
+ }
7739
+ invalidClass = computed(() => {
7740
+ // Colour alone cannot carry "this is not a number" (WCAG 1.4.1).
7741
+ return css({
7742
+ textDecoration: 'underline dashed',
7743
+ textUnderlineOffset: 3,
7744
+ textDecorationColor: this.theme.colors()['warn'],
5560
7745
  });
5561
- }, ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
5562
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSliderComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
5563
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniSliderComponent, isStandalone: true, selector: "uni-slider", 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 } }, outputs: { value: "valueChange", touched: "touchedChange" }, providers: [{ provide: COMPONENT_NAME, useValue: 'slider' }], usesInheritance: true, ngImport: i0, template: `
5564
- <input
5565
- type="range"
5566
- [class]="inputClass()"
5567
- [style.--uni-slider-fill]="fillPercent()"
5568
- [min]="resolvedMin()"
5569
- [max]="resolvedMax()"
5570
- [step]="step()"
5571
- [value]="value()"
5572
- [disabled]="disabled()"
5573
- (input)="handleInput($event)"
5574
- (blur)="markAsTouched()"
5575
- [attr.aria-label]="label()"
5576
- [attr.aria-describedby]="ariaDescribedBy() || null"
5577
- />
5578
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
7746
+ }, ...(ngDevMode ? [{ debugName: "invalidClass" }] : /* istanbul ignore next */ []));
7747
+ dividerClass = computed(() => {
7748
+ const options = this.componentOptions();
7749
+ return css({
7750
+ flex: 'none',
7751
+ userSelect: 'none',
7752
+ ...this.theme.color(options.dividerColor ?? 'outline'),
7753
+ });
7754
+ }, ...(ngDevMode ? [{ debugName: "dividerClass" }] : /* istanbul ignore next */ []));
7755
+ dividerText = computed(() => this.componentOptions().dividerText ?? '–', ...(ngDevMode ? [{ debugName: "dividerText" }] : /* istanbul ignore next */ []));
7756
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniNumberRangeInputComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
7757
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniNumberRangeInputComponent, isStandalone: true, selector: "uni-number-range-input, NumberRangeInput", 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 }, startLabel: { classPropertyName: "startLabel", publicName: "startLabel", isSignal: true, isRequired: false, transformFunction: null }, endLabel: { classPropertyName: "endLabel", publicName: "endLabel", isSignal: true, isRequired: false, transformFunction: null }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, prefix: { classPropertyName: "prefix", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null }, suffix: { classPropertyName: "suffix", publicName: "suffix", isSignal: true, isRequired: false, transformFunction: null }, decimals: { classPropertyName: "decimals", publicName: "decimals", isSignal: true, isRequired: false, transformFunction: null }, grouping: { classPropertyName: "grouping", publicName: "grouping", isSignal: true, isRequired: false, transformFunction: null }, roundingMode: { classPropertyName: "roundingMode", publicName: "roundingMode", isSignal: true, isRequired: false, transformFunction: null }, placeholderStart: { classPropertyName: "placeholderStart", publicName: "placeholderStart", isSignal: true, isRequired: false, transformFunction: null }, placeholderEnd: { classPropertyName: "placeholderEnd", publicName: "placeholderEnd", isSignal: true, isRequired: false, 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 }, minGap: { classPropertyName: "minGap", publicName: "minGap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", swapped: "swapped", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'numberRangeInput' }], viewQueries: [{ propertyName: "inputRefs", predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- One label names the group; the parts are named \"{label}, Minimum\" and\n \"{label}, Maximum\". Two tab stops, honestly \u2014 it is two questions. -->\n<div role=\"group\" [attr.aria-label]=\"label()\" [attr.aria-describedby]=\"describedBy()\">\n <uni-input-box\n [error]=\"showError()\"\n [disabled]=\"disabled()\"\n [fullWidth]=\"true\"\n [managedInset]=\"true\"\n >\n <div [class]=\"rowClass()\">\n @for (part of parts; track part) {\n @if (part === 'end') {\n <!-- Punctuation between two numbers, not a glyph: aria-hidden, since\n the two parts are already named minimum and maximum. -->\n <span [class]=\"dividerClass()\" aria-hidden=\"true\">{{ dividerText() }}</span>\n }\n\n <span [class]=\"partWrapClass()\">\n <!-- Adornments, not text: outside the <input> so the caret,\n select-all and paste never step over them, and aria-hidden\n because aria-valuetext already speaks them. -->\n @if (format().prefix) {\n <span [class]=\"affixClass()\" aria-hidden=\"true\">{{ format().prefix }}</span>\n }\n\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n autocomplete=\"off\"\n [class]=\"partClass() + (isInvalid(part) ? ' ' + invalidClass() : '')\"\n [value]=\"displayText(part)\"\n [disabled]=\"disabled()\"\n [attr.inputmode]=\"format().inputMode\"\n [attr.placeholder]=\"\n (part === 'start' ? placeholderStart() : placeholderEnd()) ?? null\n \"\n [attr.aria-label]=\"partLabel(part)\"\n [attr.aria-valuenow]=\"valueOf(part)\"\n [attr.aria-valuemin]=\"stepFence(part).min ?? null\"\n [attr.aria-valuemax]=\"stepFence(part).max ?? null\"\n [attr.aria-valuetext]=\"valueTextOf(part)\"\n [attr.aria-invalid]=\"isInvalid(part) || formError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (input)=\"onInput(part, $any($event.target).value)\"\n (keydown)=\"onKeydown($event, part)\"\n (focus)=\"onFocus(part)\"\n (blur)=\"onBlur(part)\"\n />\n\n @if (format().suffix) {\n <span [class]=\"affixClass()\" aria-hidden=\"true\">{{ format().suffix }}</span>\n }\n </span>\n }\n </div>\n </uni-input-box>\n</div>\n\n<!-- Said once for the whole field rather than on both ends. -->\n<span [id]=\"hintId\" [class]=\"srOnly\">\n Two values. Use the up and down arrow keys to change either end; entering them\n the wrong way round swaps them.\n</span>\n\n<!-- Swaps, gap corrections, fences and refusals are each otherwise silent. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5579
7758
  }
5580
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniSliderComponent, decorators: [{
7759
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniNumberRangeInputComponent, decorators: [{
5581
7760
  type: Component,
5582
- args: [{
5583
- changeDetection: ChangeDetectionStrategy.OnPush,
5584
- selector: 'uni-slider',
5585
- providers: [{ provide: COMPONENT_NAME, useValue: 'slider' }],
5586
- template: `
5587
- <input
5588
- type="range"
5589
- [class]="inputClass()"
5590
- [style.--uni-slider-fill]="fillPercent()"
5591
- [min]="resolvedMin()"
5592
- [max]="resolvedMax()"
5593
- [step]="step()"
5594
- [value]="value()"
5595
- [disabled]="disabled()"
5596
- (input)="handleInput($event)"
5597
- (blur)="markAsTouched()"
5598
- [attr.aria-label]="label()"
5599
- [attr.aria-describedby]="ariaDescribedBy() || null"
5600
- />
5601
- `,
5602
- }]
5603
- }], 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 }] }] } });
7761
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-number-range-input, NumberRangeInput', imports: [UniInputBoxComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'numberRangeInput' }], host: { '[class]': 'className()' }, template: "<!-- One label names the group; the parts are named \"{label}, Minimum\" and\n \"{label}, Maximum\". Two tab stops, honestly \u2014 it is two questions. -->\n<div role=\"group\" [attr.aria-label]=\"label()\" [attr.aria-describedby]=\"describedBy()\">\n <uni-input-box\n [error]=\"showError()\"\n [disabled]=\"disabled()\"\n [fullWidth]=\"true\"\n [managedInset]=\"true\"\n >\n <div [class]=\"rowClass()\">\n @for (part of parts; track part) {\n @if (part === 'end') {\n <!-- Punctuation between two numbers, not a glyph: aria-hidden, since\n the two parts are already named minimum and maximum. -->\n <span [class]=\"dividerClass()\" aria-hidden=\"true\">{{ dividerText() }}</span>\n }\n\n <span [class]=\"partWrapClass()\">\n <!-- Adornments, not text: outside the <input> so the caret,\n select-all and paste never step over them, and aria-hidden\n because aria-valuetext already speaks them. -->\n @if (format().prefix) {\n <span [class]=\"affixClass()\" aria-hidden=\"true\">{{ format().prefix }}</span>\n }\n\n <input\n #field\n type=\"text\"\n role=\"spinbutton\"\n autocomplete=\"off\"\n [class]=\"partClass() + (isInvalid(part) ? ' ' + invalidClass() : '')\"\n [value]=\"displayText(part)\"\n [disabled]=\"disabled()\"\n [attr.inputmode]=\"format().inputMode\"\n [attr.placeholder]=\"\n (part === 'start' ? placeholderStart() : placeholderEnd()) ?? null\n \"\n [attr.aria-label]=\"partLabel(part)\"\n [attr.aria-valuenow]=\"valueOf(part)\"\n [attr.aria-valuemin]=\"stepFence(part).min ?? null\"\n [attr.aria-valuemax]=\"stepFence(part).max ?? null\"\n [attr.aria-valuetext]=\"valueTextOf(part)\"\n [attr.aria-invalid]=\"isInvalid(part) || formError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (input)=\"onInput(part, $any($event.target).value)\"\n (keydown)=\"onKeydown($event, part)\"\n (focus)=\"onFocus(part)\"\n (blur)=\"onBlur(part)\"\n />\n\n @if (format().suffix) {\n <span [class]=\"affixClass()\" aria-hidden=\"true\">{{ format().suffix }}</span>\n }\n </span>\n }\n </div>\n </uni-input-box>\n</div>\n\n<!-- Said once for the whole field rather than on both ends. -->\n<span [id]=\"hintId\" [class]=\"srOnly\">\n Two values. Use the up and down arrow keys to change either end; entering them\n the wrong way round swaps them.\n</span>\n\n<!-- Swaps, gap corrections, fences and refusals are each otherwise silent. -->\n<span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n" }]
7762
+ }], 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 }] }], startLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "startLabel", required: false }] }], endLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "endLabel", required: false }] }], preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], decimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "decimals", required: false }] }], grouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "grouping", required: false }] }], roundingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "roundingMode", required: false }] }], placeholderStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderStart", required: false }] }], placeholderEnd: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderEnd", required: false }] }], 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 }] }], minGap: [{ type: i0.Input, args: [{ isSignal: true, alias: "minGap", required: false }] }], swapped: [{ type: i0.Output, args: ["swapped"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRefs: [{ type: i0.ViewChildren, args: ['field', { isSignal: true }] }] } });
7763
+
7764
+ /**
7765
+ * `− 3 +` for cart lines, table cells and seat counts: the numeric core with no
7766
+ * field chrome, no label and no room for either.
7767
+ *
7768
+ * A separate component rather than a `chrome="bare"` flag on
7769
+ * `uni-number-input`, because this control is defined by what it does *not*
7770
+ * have — presets, affixes, expressions, four stepper layouts — and eight inputs
7771
+ * are easier to write correctly than forty with a list of which ones to leave
7772
+ * alone. The arithmetic, parsing and hold-to-repeat are the cdk's, shared with
7773
+ * the field, so `1,200` and the keyboard map behave identically in both.
7774
+ *
7775
+ * The middle stays a real input by default: typing `12` beats tapping `+`
7776
+ * eleven times. `editable=false` is for read-mostly tables.
7777
+ */
7778
+ class UniQuantityStepperComponent extends BaseComponent {
7779
+ // --- Signal Forms block (explicit per AGENTS.md, not a base class) --------
7780
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
7781
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
7782
+ touched = model(false, ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
7783
+ invalid = input(false, ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
7784
+ dirty = input(false, ...(ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []));
7785
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
7786
+ ariaDescribedBy = input(...(ngDevMode ? [undefined, { debugName: "ariaDescribedBy" }] : /* istanbul ignore next */ []));
7787
+ // --- Configuration -------------------------------------------------------
7788
+ /**
7789
+ * Accessible name. Never visible and always needed — a cart with six of these
7790
+ * needs "Quantity, Blue T-shirt (M)", not six controls called "Quantity".
7791
+ */
7792
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
7793
+ // `min`/`max` are part of the FormValueControl contract — Signal Forms syncs
7794
+ // them from min()/max() validators — so their type must admit undefined.
7795
+ // Read `resolvedMin()` internally, never `min()`.
7796
+ min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
7797
+ max = input(...(ngDevMode ? [undefined, { debugName: "max" }] : /* istanbul ignore next */ []));
7798
+ step = input(1, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
7799
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
7800
+ /** `false` renders the number as text: read-mostly tables. */
7801
+ editable = input(true, ...(ngDevMode ? [{ debugName: "editable" }] : /* istanbul ignore next */ []));
7802
+ /**
7803
+ * The cart pattern in one attribute: at `min` the decrement button becomes a
7804
+ * remove affordance and emits `removed` rather than stepping. Without it
7805
+ * every shop reimplements the same `value === 1 ? remove() : step(-1)` branch
7806
+ * outside the component.
7807
+ */
7808
+ deleteAtMin = input(false, ...(ngDevMode ? [{ debugName: "deleteAtMin" }] : /* istanbul ignore next */ []));
7809
+ // --- Events --------------------------------------------------------------
7810
+ /**
7811
+ * The remove affordance was activated — the row should come out.
7812
+ *
7813
+ * Named `removed`, not the spec's `emptied`: that word is a native
7814
+ * `HTMLMediaElement` event, which `@angular-eslint/no-output-native` bans for
7815
+ * good reason, and `removed` is already what `uni-tag` calls this same
7816
+ * request.
7817
+ */
7818
+ removed = output();
7819
+ inputRef = viewChild('field', ...(ngDevMode ? [{ debugName: "inputRef" }] : /* istanbul ignore next */ []));
7820
+ srOnly = css(visuallyHidden);
7821
+ announcer = createAnnouncer();
7822
+ hintId = uniqueId('uni-quantity-stepper');
7823
+ /** Uncommitted text. `null` means "show the committed value". */
7824
+ draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
7825
+ canonical = computed(() => {
7826
+ const value = this.value();
7827
+ return value == null ? null : toDecimal(value);
7828
+ }, ...(ngDevMode ? [{ debugName: "canonical" }] : /* istanbul ignore next */ []));
7829
+ /** Quantities are plain numbers; precision follows the step. */
7830
+ format = computed(() => resolveNumberFormat({ decimals: [0, decimalScale(toDecimal(this.step()))] }), ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
7831
+ displayText = computed(() => {
7832
+ const draft = this.draft();
7833
+ if (draft != null)
7834
+ return draft;
7835
+ const canonical = this.canonical();
7836
+ return canonical == null ? '' : formatNumber(canonical, this.format());
7837
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : /* istanbul ignore next */ []));
7838
+ showError = computed(() => this.invalid() && (this.touched() || this.dirty()), ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
7839
+ describedBy = computed(() => [this.ariaDescribedBy(), this.hintId].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
7840
+ // --- Fences ---------------------------------------------------------------
7841
+ /** A quantity has a floor even when a validator has not supplied one. */
7842
+ resolvedMin = computed(() => this.min() ?? 0, ...(ngDevMode ? [{ debugName: "resolvedMin" }] : /* istanbul ignore next */ []));
7843
+ atMin = computed(() => {
7844
+ const canonical = this.canonical();
7845
+ return canonical != null && Number(canonical) <= this.resolvedMin();
7846
+ }, ...(ngDevMode ? [{ debugName: "atMin" }] : /* istanbul ignore next */ []));
7847
+ atMax = computed(() => {
7848
+ const max = this.max();
7849
+ const canonical = this.canonical();
7850
+ return max != null && canonical != null && Number(canonical) >= max;
7851
+ }, ...(ngDevMode ? [{ debugName: "atMax" }] : /* istanbul ignore next */ []));
7852
+ /** At the floor with `deleteAtMin`, the − is a remove control instead. */
7853
+ showDelete = computed(() => this.deleteAtMin() && this.atMin(), ...(ngDevMode ? [{ debugName: "showDelete" }] : /* istanbul ignore next */ []));
7854
+ decrementIcon = computed(() => {
7855
+ const options = this.componentOptions();
7856
+ return this.showDelete()
7857
+ ? (options.deleteIcon ?? 'delete')
7858
+ : (options.decrementIcon ?? 'minus');
7859
+ }, ...(ngDevMode ? [{ debugName: "decrementIcon" }] : /* istanbul ignore next */ []));
7860
+ decrementLabel = computed(() => this.showDelete() ? `Remove ${this.label()}` : `Decrease ${this.label()}`, ...(ngDevMode ? [{ debugName: "decrementLabel" }] : /* istanbul ignore next */ []));
7861
+ // --- Committing -----------------------------------------------------------
7862
+ onInput(text) {
7863
+ this.draft.set(text);
7864
+ }
7865
+ /**
7866
+ * The same parse path as the field, so `1,200` commits as 1200 here too.
7867
+ * Unreadable text reverts rather than being kept: this control has no room to
7868
+ * show an error, and no `rejected` output to report one through.
7869
+ */
7870
+ commitDraft() {
7871
+ const draft = this.draft();
7872
+ if (draft == null)
7873
+ return;
7874
+ const result = parseNumber(draft, this.format());
7875
+ if (result.status !== 'ok') {
7876
+ this.draft.set(null);
7877
+ if (result.status === 'empty')
7878
+ return;
7879
+ this.announcer.announce(`${draft} is not a number.`);
7880
+ return;
7881
+ }
7882
+ const settled = settleNumber(result.value, this.format());
7883
+ const clamped = clampDecimal(settled, this.resolvedMin(), this.max());
7884
+ this.draft.set(null);
7885
+ this.value.set(Number(clamped.value));
7886
+ if (clamped.hit) {
7887
+ const bound = clamped.hit === 'min' ? this.resolvedMin() : this.max();
7888
+ this.announcer.announce(`${clamped.hit === 'min' ? 'Minimum' : 'Maximum'} is ${bound}.`);
7889
+ }
7890
+ }
7891
+ // --- Stepping -------------------------------------------------------------
7892
+ applyStep(direction, announce = true) {
7893
+ if (this.disabled())
7894
+ return;
7895
+ if (this.draft() != null)
7896
+ this.commitDraft();
7897
+ const current = this.canonical();
7898
+ if (current == null) {
7899
+ const seed = toDecimal(this.resolvedMin());
7900
+ this.value.set(Number(seed));
7901
+ if (announce)
7902
+ this.announceValue();
7903
+ return;
7904
+ }
7905
+ const next = stepDecimal(current, direction, {
7906
+ step: this.step(),
7907
+ min: this.resolvedMin(),
7908
+ max: this.max(),
7909
+ stepOrigin: 'min',
7910
+ });
7911
+ if (next === current) {
7912
+ if (announce)
7913
+ this.announceFence(direction);
7914
+ return;
7915
+ }
7916
+ this.value.set(Number(next));
7917
+ if (announce)
7918
+ this.announceValue();
7919
+ }
7920
+ /**
7921
+ * The decrement button has two jobs. Below the floor with `deleteAtMin` it is
7922
+ * a remove control — a single click, with nothing to hold and repeat — so the
7923
+ * press/repeat machinery is skipped entirely in that state.
7924
+ */
7925
+ onDecrementPress(event) {
7926
+ if (this.showDelete())
7927
+ return;
7928
+ this.decrement.press(event);
7929
+ }
7930
+ onDecrementClick() {
7931
+ if (this.disabled() || !this.showDelete())
7932
+ return;
7933
+ this.removed.emit();
7934
+ this.announcer.announce(`${this.label()} removed.`);
7935
+ }
7936
+ announceValue() {
7937
+ this.announcer.announce(`${this.displayText()}.`);
7938
+ }
7939
+ announceFence(direction) {
7940
+ const bound = direction > 0 ? this.max() : this.resolvedMin();
7941
+ if (bound == null)
7942
+ return;
7943
+ this.announcer.announce(`${direction > 0 ? 'Maximum' : 'Minimum'}, ${bound}.`);
7944
+ }
7945
+ // --- Hold to repeat -------------------------------------------------------
7946
+ /** Announced on release only; narrating every intermediate value is noise. */
7947
+ increment = createPressRepeat({
7948
+ onStep: () => this.applyStep(1, false),
7949
+ onRelease: () => this.announceValue(),
7950
+ disabled: () => this.disabled() || this.atMax(),
7951
+ });
7952
+ decrement = createPressRepeat({
7953
+ onStep: () => this.applyStep(-1, false),
7954
+ onRelease: () => this.announceValue(),
7955
+ disabled: () => this.disabled() || this.atMin(),
7956
+ });
7957
+ // --- Keyboard -------------------------------------------------------------
7958
+ onKeydown(event) {
7959
+ switch (event.key) {
7960
+ case 'ArrowUp':
7961
+ event.preventDefault();
7962
+ this.applyStep(1);
7963
+ break;
7964
+ case 'ArrowDown':
7965
+ event.preventDefault();
7966
+ this.applyStep(-1);
7967
+ break;
7968
+ case 'Enter':
7969
+ if (this.draft() != null)
7970
+ event.preventDefault();
7971
+ this.commitDraft();
7972
+ break;
7973
+ case 'Escape':
7974
+ event.preventDefault();
7975
+ this.draft.set(null);
7976
+ break;
7977
+ case 'Tab':
7978
+ this.commitDraft();
7979
+ break;
7980
+ }
7981
+ }
7982
+ onBlur() {
7983
+ this.touched.set(true);
7984
+ this.commitDraft();
7985
+ this.increment.cancel();
7986
+ this.decrement.cancel();
7987
+ }
7988
+ focusField() {
7989
+ this.inputRef()?.nativeElement.focus();
7990
+ }
7991
+ // --- Styling --------------------------------------------------------------
7992
+ className = computed(() => css({ display: 'inline-block' }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
7993
+ /** Overall height, from the theme's `sizes` block. */
7994
+ height = computed(() => Number(this.style()['height'] ?? 32), ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
7995
+ /**
7996
+ * The shared field chrome, read from the same `input` theme entry
7997
+ * `uni-input-box` resolves. This control has its own container tokens, but the
7998
+ * **focus indicator** has to be the one every other field uses — a stepper
7999
+ * that highlights differently from the field beside it reads as a bug.
8000
+ */
8001
+ fieldChrome = this.theme.getComponentOptions('input');
8002
+ rootClass = computed(() => {
8003
+ const options = this.componentOptions();
8004
+ const colors = this.theme.colors();
8005
+ const chrome = this.fieldChrome();
8006
+ return css({
8007
+ display: 'inline-flex',
8008
+ alignItems: 'stretch',
8009
+ height: this.height(),
8010
+ // The themed size is the *outer* height, so a md stepper is 32px like the
8011
+ // field beside it rather than 32 plus its border.
8012
+ boxSizing: 'border-box',
8013
+ 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'),
8017
+ ...(this.showError() ? { borderColor: colors['warn'] } : {}),
8018
+ ...(this.disabled() ? { cursor: 'not-allowed' } : {}),
8019
+ // The middle input clears its own outline (removeInputPlatformStyling),
8020
+ // so the focus indicator belongs on the container — the same `:has()`
8021
+ // rule and the same `input` tokens uni-input-box uses, so a stepper
8022
+ // highlights exactly like the field next to it. Error state wins, to
8023
+ // keep a flagged control visibly flagged while it is being corrected.
8024
+ '&:has(input:focus)': {
8025
+ outline: chrome.focusOutline,
8026
+ outlineOffset: chrome.focusOutlineOffset,
8027
+ ...(this.showError()
8028
+ ? {}
8029
+ : {
8030
+ ...this.theme.border(chrome.focusBorder),
8031
+ ...this.theme.boxShadow(chrome.focusShadow),
8032
+ ...this.theme.backgroundColor(chrome.focusColor),
8033
+ }),
8034
+ },
8035
+ // The dividers move with the frame, so a focused control does not end up
8036
+ // amber on the outside and grey down the middle. Falls back to the resting
8037
+ // border, which is a no-op in themes that show focus as an outline.
8038
+ '&:has(input:focus) > input': this.showError()
8039
+ ? {}
8040
+ : {
8041
+ ...this.theme.borderLeft(chrome.focusBorder ?? this.dividerBorder()),
8042
+ ...this.theme.borderRight(chrome.focusBorder ?? this.dividerBorder()),
8043
+ },
8044
+ });
8045
+ }, ...(ngDevMode ? [{ debugName: "rootClass" }] : /* istanbul ignore next */ []));
8046
+ /** Square at the field height, so the pointer target is legal at every size. */
8047
+ buttonClass = computed(() => {
8048
+ const colors = this.theme.colors();
8049
+ return css({
8050
+ display: 'grid',
8051
+ placeItems: 'center',
8052
+ flex: 'none',
8053
+ width: this.height(),
8054
+ padding: 0,
8055
+ border: 0,
8056
+ background: 'transparent',
8057
+ color: this.disabled() ? colors['on-disabled-surface'] : 'inherit',
8058
+ cursor: this.disabled() ? 'not-allowed' : 'pointer',
8059
+ touchAction: 'none',
8060
+ '&:disabled': { opacity: 0.4, cursor: 'not-allowed' },
8061
+ ...this.theme.focusRing(),
8062
+ });
8063
+ }, ...(ngDevMode ? [{ debugName: "buttonClass" }] : /* istanbul ignore next */ []));
8064
+ /** 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 */ []));
8066
+ valueBase() {
8067
+ const options = this.componentOptions();
8068
+ const colors = this.theme.colors();
8069
+ return {
8070
+ flex: '1 1 auto',
8071
+ minWidth: options.valueWidth ?? '3ch',
8072
+ textAlign: 'center',
8073
+ alignSelf: 'stretch',
8074
+ border: 0,
8075
+ background: 'transparent',
8076
+ color: 'inherit',
8077
+ font: 'inherit',
8078
+ padding: 0,
8079
+ outline: 'none',
8080
+ ...(options.tabularNumerals === false ? {} : { fontVariantNumeric: 'tabular-nums' }),
8081
+ // A rule either side, which is what makes the three parts read as one
8082
+ // control rather than three loose ones. It uses the **same token as the
8083
+ // outer border** so the frame reads as one weight — a heavier divider
8084
+ // makes the control look like three stuck together. A theme wanting a
8085
+ // distinct rule overrides just its colour.
8086
+ ...this.theme.borderLeft(this.dividerBorder()),
8087
+ ...this.theme.borderRight(this.dividerBorder()),
8088
+ ...(options.dividerColor
8089
+ ? { borderInlineColor: colors[options.dividerColor] }
8090
+ : {}),
8091
+ };
8092
+ }
8093
+ inputClass = computed(() => css([this.valueBase()]), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
8094
+ /** Read-only presentation: centred text on the same grid as the input. */
8095
+ readoutClass = computed(() => css([this.valueBase(), { display: 'grid', placeItems: 'center' }]), ...(ngDevMode ? [{ debugName: "readoutClass" }] : /* istanbul ignore next */ []));
8096
+ glyphSize = computed(() => Math.max(12, Math.round(this.height() / 2)), ...(ngDevMode ? [{ debugName: "glyphSize" }] : /* istanbul ignore next */ []));
8097
+ 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 });
8099
+ }
8100
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniQuantityStepperComponent, decorators: [{
8101
+ 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" }]
8103
+ }], 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 }] }] } });
5604
8104
 
5605
8105
  /**
5606
8106
  * Compact chip for categories, states, filters and tokens.
@@ -6112,6 +8612,13 @@ class UniTagInputComponent extends BaseComponent {
6112
8612
  has the same geometry but is only the fallback's positioning context. */
6113
8613
  className = computed(() => css({ display: 'block', ...this.anchor.style }), ...(ngDevMode ? [{ debugName: "className" }] : /* istanbul ignore next */ []));
6114
8614
  wrapperClass = computed(() => css({ position: 'relative' }), ...(ngDevMode ? [{ debugName: "wrapperClass" }] : /* istanbul ignore next */ []));
8615
+ /**
8616
+ * The shared field chrome, from the same `input` theme entry
8617
+ * `uni-input-box` resolves — not a duplicate token, because the inset has to
8618
+ * match every other field or a chip field stops lining up with the text
8619
+ * field above it.
8620
+ */
8621
+ fieldChrome = this.theme.getComponentOptions('input');
6115
8622
  fieldClass = computed(() => {
6116
8623
  const options = this.componentOptions();
6117
8624
  return css({
@@ -6122,6 +8629,14 @@ class UniTagInputComponent extends BaseComponent {
6122
8629
  listStyle: 'none',
6123
8630
  margin: 0,
6124
8631
  padding: 0,
8632
+ // Wrapped chip rows keep clear of the field border; one 24px chip row
8633
+ // plus this padding fills the themed 32px minimum exactly.
8634
+ ...this.theme.paddingTop('xs'),
8635
+ ...this.theme.paddingBottom('xs'),
8636
+ // The leading inset lives here rather than on the inner <input> (see
8637
+ // `managedInset`): the chips are this field's leading edge, and an inset
8638
+ // on the text alone leaves the first chip riding the border.
8639
+ ...this.theme.paddingLeft(this.fieldChrome().paddingLeft),
6125
8640
  ...this.theme.gap(options.chipGap),
6126
8641
  });
6127
8642
  }, ...(ngDevMode ? [{ debugName: "fieldClass" }] : /* istanbul ignore next */ []));
@@ -6137,11 +8652,11 @@ class UniTagInputComponent extends BaseComponent {
6137
8652
  }), ...(ngDevMode ? [{ debugName: "inputClass" }] : /* istanbul ignore next */ []));
6138
8653
  listClass = computed(() => css(listboxPopupStyles(this.theme, this.componentOptions(), { anchor: this.anchor.name })), ...(ngDevMode ? [{ debugName: "listClass" }] : /* istanbul ignore next */ []));
6139
8654
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6140
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagInputComponent, isStandalone: true, selector: "uni-tag-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, tagVariant: { classPropertyName: "tagVariant", publicName: "tagVariant", isSignal: true, isRequired: false, transformFunction: null }, tagTone: { classPropertyName: "tagTone", publicName: "tagTone", isSignal: true, isRequired: false, transformFunction: null }, tagSize: { classPropertyName: "tagSize", publicName: "tagSize", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", added: "added", removed: "removed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chipRefs", predicate: ["chip"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div [class]=\"wrapperClass()\" (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" height=\"auto\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }, { kind: "component", type: UniTagComponent, selector: "uni-tag", inputs: ["size", "tone", "label", "value", "maxWidth", "avatarSrc", "avatarName", "iconName", "symbolName", "dot", "removable", "interactive", "selected", "invalid", "disabled", "removeLabel", "controlTabIndex"], outputs: ["removed", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8655
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: UniTagInputComponent, isStandalone: true, selector: "uni-tag-input", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, preset: { classPropertyName: "preset", publicName: "preset", isSignal: true, isRequired: false, transformFunction: null }, separators: { classPropertyName: "separators", publicName: "separators", isSignal: true, isRequired: false, transformFunction: null }, commitOnBlur: { classPropertyName: "commitOnBlur", publicName: "commitOnBlur", isSignal: true, isRequired: false, transformFunction: null }, allowDuplicates: { classPropertyName: "allowDuplicates", publicName: "allowDuplicates", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, validate: { classPropertyName: "validate", publicName: "validate", isSignal: true, isRequired: false, transformFunction: null }, parse: { classPropertyName: "parse", publicName: "parse", isSignal: true, isRequired: false, transformFunction: null }, tagVariant: { classPropertyName: "tagVariant", publicName: "tagVariant", isSignal: true, isRequired: false, transformFunction: null }, tagTone: { classPropertyName: "tagTone", publicName: "tagTone", isSignal: true, isRequired: false, transformFunction: null }, tagSize: { classPropertyName: "tagSize", publicName: "tagSize", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, debounceTime: { classPropertyName: "debounceTime", publicName: "debounceTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange", query: "query", added: "added", removed: "removed", rejected: "rejected" }, host: { properties: { "class": "className()" } }, providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chipRefs", predicate: ["chip"], descendants: true, isSignal: true }, { propertyName: "listRef", first: true, predicate: ["listbox"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div [class]=\"wrapperClass()\" (focusout)=\"onFocusOut($event)\">\n <!-- The chip row owns the leading inset, so the first chip sits at the\n field's edge instead of riding its border. -->\n <uni-input-box [error]=\"showError()\" height=\"auto\" [managedInset]=\"true\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }, { kind: "component", type: UniTagComponent, selector: "uni-tag", inputs: ["size", "tone", "label", "value", "maxWidth", "avatarSrc", "avatarName", "iconName", "symbolName", "dot", "removable", "interactive", "selected", "invalid", "disabled", "removeLabel", "controlTabIndex"], outputs: ["removed", "activated"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6141
8656
  }
6142
8657
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTagInputComponent, decorators: [{
6143
8658
  type: Component,
6144
- args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag-input', imports: [UniInputBoxComponent, UniTagComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div [class]=\"wrapperClass()\" (focusout)=\"onFocusOut($event)\">\n <uni-input-box [error]=\"showError()\" height=\"auto\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n" }]
8659
+ args: [{ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'uni-tag-input', imports: [UniInputBoxComponent, UniTagComponent], providers: [{ provide: COMPONENT_NAME, useValue: 'tagInput' }], host: { '[class]': 'className()' }, template: "<!-- Focusout rides the bubbling event so the list closes only when focus\n leaves the whole field, not while moving between chips and the input. -->\n<div [class]=\"wrapperClass()\" (focusout)=\"onFocusOut($event)\">\n <!-- The chip row owns the leading inset, so the first chip sits at the\n field's edge instead of riding its border. -->\n <uni-input-box [error]=\"showError()\" height=\"auto\" [managedInset]=\"true\">\n <ul [class]=\"fieldClass()\">\n @for (item of value(); track item.value; let i = $index) {\n <!-- The focusable element is the chip's own button; this <li> only\n receives keys bubbling up from it, so it must stay out of the tab\n order (same delegation the menu uses for its roving focus). -->\n <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->\n <li #chip (keydown)=\"onChipKeydown($event, i)\" (dblclick)=\"editChip(i)\">\n <uni-tag\n [label]=\"labelOf(item)\"\n [value]=\"item.value\"\n [avatarSrc]=\"item.avatarSrc\"\n [variant]=\"item.invalid ? 'warn' : tagVariant()\"\n [tone]=\"tagTone()\"\n [size]=\"tagSize() ?? componentOptions().chipSize ?? 'md'\"\n [invalid]=\"!!item.invalid\"\n [disabled]=\"!!item.disabled || disabled()\"\n [interactive]=\"true\"\n [removable]=\"!item.disabled && !disabled()\"\n [controlTabIndex]=\"-1\"\n (removed)=\"removeAt(i)\"\n />\n </li>\n }\n\n <li [class]=\"inputClass()\">\n <!-- The field is one tab stop: chips carry tabindex=\"-1\" and are\n entered with Backspace/ArrowLeft, so Tab never walks through\n eight recipients to reach the next control. -->\n <input\n #field\n type=\"text\"\n role=\"combobox\"\n autocomplete=\"off\"\n [class]=\"inputClass()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() ?? ''\"\n [attr.aria-label]=\"label()\"\n [attr.aria-expanded]=\"list.open()\"\n [attr.aria-controls]=\"list.listboxId\"\n [attr.aria-activedescendant]=\"list.activeDescendantId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (keydown)=\"onInputKeydown($event)\"\n (input)=\"onInput($any($event.target).value)\"\n (paste)=\"onPaste($event)\"\n (blur)=\"onBlur()\"\n />\n </li>\n </ul>\n </uni-input-box>\n\n @if (list.open()) {\n <ul\n #listbox\n [id]=\"list.listboxId\"\n role=\"listbox\"\n [attr.popover]=\"popupAttr\"\n [attr.aria-label]=\"label()\"\n [class]=\"listClass()\"\n >\n @for (suggestion of visibleSuggestions(); track suggestion.value; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"list.optionId(i)\"\n [class.active]=\"i === list.activeIndex()\"\n [attr.aria-selected]=\"i === list.activeIndex()\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"selectSuggestion(suggestion)\"\n >\n {{ labelOf(suggestion) }}\n @if (suggestion.description) {\n <span [class]=\"srOnly\">{{ suggestion.description }}</span>\n }\n </li>\n }\n </ul>\n }\n\n <!-- Said once for the whole field, rather than every chip advertising it. -->\n <span [id]=\"hintId\" [class]=\"srOnly\">\n Press Backspace to reach the last entry, then Backspace again to remove it.\n </span>\n\n <!-- Adds and removes are otherwise silent for a screen reader. -->\n <span role=\"status\" aria-live=\"polite\" [class]=\"srOnly\">{{ announcer.message() }}</span>\n</div>\n" }]
6145
8660
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }, { type: i0.Output, args: ["touchedChange"] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], dirty: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirty", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedBy", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], preset: [{ type: i0.Input, args: [{ isSignal: true, alias: "preset", required: false }] }], separators: [{ type: i0.Input, args: [{ isSignal: true, alias: "separators", required: false }] }], commitOnBlur: [{ type: i0.Input, args: [{ isSignal: true, alias: "commitOnBlur", required: false }] }], allowDuplicates: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDuplicates", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], validate: [{ type: i0.Input, args: [{ isSignal: true, alias: "validate", required: false }] }], parse: [{ type: i0.Input, args: [{ isSignal: true, alias: "parse", required: false }] }], tagVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagVariant", required: false }] }], tagTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagTone", required: false }] }], tagSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "tagSize", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], query: [{ type: i0.Output, args: ["query"] }], debounceTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceTime", required: false }] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], rejected: [{ type: i0.Output, args: ["rejected"] }], inputRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], chipRefs: [{ type: i0.ViewChildren, args: ['chip', { isSignal: true }] }], listRef: [{ type: i0.ViewChild, args: ['listbox', { isSignal: true }] }] } });
6146
8661
 
6147
8662
  class UniTextareaComponent {
@@ -6192,7 +8707,7 @@ class UniTextareaComponent {
6192
8707
  this.value.set(event.target.value);
6193
8708
  }
6194
8709
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6195
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniTextareaComponent, isStandalone: true, selector: "uni-textarea", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, spellcheck: { classPropertyName: "spellcheck", publicName: "spellcheck", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, 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", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8710
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.12", type: UniTextareaComponent, isStandalone: true, selector: "uni-textarea", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, dirty: { classPropertyName: "dirty", publicName: "dirty", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, minLength: { classPropertyName: "minLength", publicName: "minLength", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, autocomplete: { classPropertyName: "autocomplete", publicName: "autocomplete", isSignal: true, isRequired: false, transformFunction: null }, spellcheck: { classPropertyName: "spellcheck", publicName: "spellcheck", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, grow: { classPropertyName: "grow", publicName: "grow", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touched: "touchedChange" }, ngImport: i0, 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", dependencies: [{ kind: "component", type: UniInputBoxComponent, selector: "uni-input-box", inputs: ["disabled", "error", "minWidth", "height", "width", "fullWidth", "grow", "managedInset"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6196
8711
  }
6197
8712
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: UniTextareaComponent, decorators: [{
6198
8713
  type: Component,
@@ -6343,6 +8858,9 @@ const UNI_FORMS = [
6343
8858
  UniMultiSelectDropdownComponent,
6344
8859
  UniSearchInputComponent,
6345
8860
  UniTagInputComponent,
8861
+ UniNumberInputComponent,
8862
+ UniNumberRangeInputComponent,
8863
+ UniQuantityStepperComponent,
6346
8864
  UniSliderComponent,
6347
8865
  UniDateInputComponent,
6348
8866
  UniTimeInputComponent,
@@ -11745,5 +14263,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
11745
14263
  * Generated bundle index. Do not edit.
11746
14264
  */
11747
14265
 
11748
- 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, UniPaginatorComponent, UniPopoverComponent, UniProgressBarComponent, UniProgressGaugeComponent, 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, clearAnchorName, createAnnouncer, createListboxNavigation, dayOfWeek, daysInMonth, discreteOverlayTransition, focusableElements, formatDate, formatMonthHeading, formatTime, getFileExtension, inclusiveDayCount, isDivider, isToggleOpen, isValidDate, isoDate, joinDateTime, listboxPopupAttr, listboxPopupStyles, localeDatePlaceholder, localeDefaultHour12, localeFieldOrder, localeMonthNames, localeWeekStart, monthOf, motionSafe, newAnchorName, newListboxAnchor, parseDateText, parseTimeText, promoteListboxPopup, resolveElement, resolveFocusTarget, restoreOverlayFocus, setAnchorName, splitDateTime, spotlightStyles, supportsAnchoredPopup, timeSlots, todayIso, transformOriginFor, uniqueId, useTimer, visuallyHidden, weekdayNames };
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 };
11749
14267
  //# sourceMappingURL=uni-design-system-uni-angular.mjs.map