@besaitech/ng-design-system 0.0.40 → 0.0.42

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 { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, output, signal, forwardRef, numberAttribute, viewChildren, afterRenderEffect, viewChild, ElementRef, model, Injector, afterNextRender, Injectable, TemplateRef, Directive, contentChildren, contentChild } from '@angular/core';
2
+ import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, output, signal, forwardRef, viewChild, numberAttribute, viewChildren, afterRenderEffect, ElementRef, model, Injector, afterNextRender, Injectable, TemplateRef, Directive, contentChildren, contentChild } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import * as i1 from '@angular/forms';
5
5
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule } from '@angular/forms';
@@ -48,6 +48,11 @@ const SDS_ICONS = {
48
48
  'chevron-right': '<path d="m9 18 6-6-6-6"/>',
49
49
  'chevron-left': '<path d="m15 18-6-6 6-6"/>',
50
50
  'chevron-down': '<path d="m6 9 6 6 6-6"/>',
51
+ // Stepper glyphs for sds-input-number: chevrons for the stacked spinner, plus/minus for the
52
+ // horizontal/vertical layouts.
53
+ 'chevron-up': '<path d="m18 15-6-6-6 6"/>',
54
+ plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
55
+ minus: '<path d="M5 12h14"/>',
51
56
  'chevrons-left': '<path d="m11 17-5-5 5-5"/><path d="m18 17-5-5 5-5"/>',
52
57
  'chevrons-right': '<path d="m6 17 5-5-5-5"/><path d="m13 17 5-5-5-5"/>',
53
58
  x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
@@ -155,7 +160,7 @@ const VARIANTS$2 = {
155
160
  ghost: 'bg-transparent text-ink-soft hover:bg-black/5 active:bg-black/10',
156
161
  danger: 'bg-danger text-white shadow-sm hover:brightness-95 active:brightness-90',
157
162
  };
158
- const SIZES$a = {
163
+ const SIZES$b = {
159
164
  sm: 'h-8 px-3 text-sm',
160
165
  md: 'h-10 px-4 text-sm',
161
166
  lg: 'h-12 px-5 text-base',
@@ -215,7 +220,7 @@ class SdsButton {
215
220
  BASE,
216
221
  this.shape() === 'pill' ? 'rounded-full' : 'rounded-control',
217
222
  VARIANTS$2[this.variant()],
218
- SIZES$a[size],
223
+ SIZES$b[size],
219
224
  this.iconOnly() ? ICON_ONLY[size] : '',
220
225
  this.block() ? 'w-full' : '',
221
226
  ]
@@ -267,8 +272,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
267
272
  `, styles: [".sds-spinner{width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:9999px;animation:sds-spin .6s linear infinite}@keyframes sds-spin{to{transform:rotate(360deg)}}\n"] }]
268
273
  }], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
269
274
 
270
- let nextId$b = 0;
271
- const SIZES$9 = {
275
+ let nextId$d = 0;
276
+ const SIZES$a = {
272
277
  sm: 'h-9 text-sm',
273
278
  md: 'h-10 text-sm',
274
279
  lg: 'h-12 text-base',
@@ -280,7 +285,7 @@ const SIZES$9 = {
280
285
  * sidebar search field).
281
286
  */
282
287
  class SdsInput {
283
- inputId = `sds-input-${nextId$b++}`;
288
+ inputId = `sds-input-${nextId$d++}`;
284
289
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
285
290
  /** Accessible name used when no visible [label] is provided. */
286
291
  ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
@@ -357,7 +362,7 @@ class SdsInput {
357
362
  return `${left} ${right}`;
358
363
  }, ...(ngDevMode ? [{ debugName: "fieldPadding" }] : []));
359
364
  wrapperClasses = computed(() => {
360
- const parts = [SIZES$9[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
365
+ const parts = [SIZES$a[this.size()], this.pill() ? 'rounded-full' : 'rounded-control'];
361
366
  if (this.isDisabled())
362
367
  parts.push('opacity-60 cursor-not-allowed');
363
368
  if (this.filled()) {
@@ -601,6 +606,1012 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
601
606
  `, styles: ["input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus,input:-webkit-autofill:active{transition:background-color 100000s ease-in-out 0s;-webkit-text-fill-color:currentColor;caret-color:currentColor}\n"] }]
602
607
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], trailingAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingAction", required: false }] }], trailingActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionLabel", required: false }] }], trailingActionPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionPressed", required: false }] }], trailingActionClick: [{ type: i0.Output, args: ["trailingActionClick"] }] } });
603
608
 
609
+ let nextId$c = 0;
610
+ const HEIGHTS = {
611
+ sm: 'h-9',
612
+ md: 'h-10',
613
+ lg: 'h-12',
614
+ };
615
+ const TEXT = {
616
+ sm: 'text-sm',
617
+ md: 'text-sm',
618
+ lg: 'text-base',
619
+ };
620
+ /**
621
+ * Numeric input with locale-aware formatting, optional currency, prefix/suffix affixes, min/max/step
622
+ * boundaries and spinner buttons — the design-system counterpart of PrimeNG's `InputNumber`.
623
+ *
624
+ * The value is a `number | null` bound through ControlValueAccessor, so it works with reactive forms
625
+ * or `[(ngModel)]`: `<sds-input-number [(ngModel)]="amount" mode="currency" currency="EUR" />`.
626
+ *
627
+ * The field formats **live as you type**: the currency symbol, grouping separators and prefix/suffix
628
+ * stay visible on every keystroke, and the caret is kept where it belongs. The value is clamped to
629
+ * {@link min}/{@link max} on blur and on every spinner step; ArrowUp / ArrowDown also step it.
630
+ */
631
+ class SdsInputNumber {
632
+ inputId = `sds-input-number-${nextId$c++}`;
633
+ fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
634
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
635
+ /** Accessible name used when no visible [label] is provided. */
636
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
637
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
638
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
639
+ error = input('', ...(ngDevMode ? [{ debugName: "error" }] : []));
640
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
641
+ /** `filled` is a borderless field on a muted ground; `default` is the bordered white field. */
642
+ appearance = input('default', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
643
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
644
+ /** Read-only: the value is shown and selectable but cannot be typed or stepped. */
645
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
646
+ // --- Formatting -----------------------------------------------------------
647
+ mode = input('decimal', ...(ngDevMode ? [{ debugName: "mode" }] : []));
648
+ /** BCP 47 locale for grouping/decimal symbols (e.g. `fr-FR`). Empty follows the runtime locale. */
649
+ locale = input('', ...(ngDevMode ? [{ debugName: "locale" }] : []));
650
+ /** ISO 4217 code, required when {@link mode} is `currency` (e.g. `EUR`, `USD`). */
651
+ currency = input('', ...(ngDevMode ? [{ debugName: "currency" }] : []));
652
+ currencyDisplay = input('symbol', ...(ngDevMode ? [{ debugName: "currencyDisplay" }] : []));
653
+ /** Thousands/grouping separators. */
654
+ useGrouping = input(true, ...(ngDevMode ? [{ debugName: "useGrouping", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
655
+ minFractionDigits = input(null, ...(ngDevMode ? [{ debugName: "minFractionDigits" }] : []));
656
+ maxFractionDigits = input(null, ...(ngDevMode ? [{ debugName: "maxFractionDigits" }] : []));
657
+ /** Text shown before the value (e.g. `↑ `). */
658
+ prefix = input('', ...(ngDevMode ? [{ debugName: "prefix" }] : []));
659
+ /** Text shown after the value (e.g. ` kg`, `%`). */
660
+ suffix = input('', ...(ngDevMode ? [{ debugName: "suffix" }] : []));
661
+ // --- Boundaries & spinner -------------------------------------------------
662
+ min = input(null, ...(ngDevMode ? [{ debugName: "min" }] : []));
663
+ max = input(null, ...(ngDevMode ? [{ debugName: "max" }] : []));
664
+ step = input(1, ...(ngDevMode ? [{ debugName: "step", transform: numberAttribute }] : [{ transform: numberAttribute }]));
665
+ /** Show the increment/decrement spinner buttons. */
666
+ showButtons = input(false, ...(ngDevMode ? [{ debugName: "showButtons", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
667
+ buttonLayout = input('stacked', ...(ngDevMode ? [{ debugName: "buttonLayout" }] : []));
668
+ /** Override the increment button icon (defaults: chevron-up when stacked, plus otherwise). */
669
+ incrementIcon = input(null, ...(ngDevMode ? [{ debugName: "incrementIcon" }] : []));
670
+ /** Override the decrement button icon (defaults: chevron-down when stacked, minus otherwise). */
671
+ decrementIcon = input(null, ...(ngDevMode ? [{ debugName: "decrementIcon" }] : []));
672
+ /** Allow an empty (null) value. When false, blurring an empty field falls back to {@link min} or 0. */
673
+ allowEmpty = input(true, ...(ngDevMode ? [{ debugName: "allowEmpty", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
674
+ clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
675
+ /**
676
+ * Accessible names for the icon-only controls. French defaults so existing consumers are unchanged;
677
+ * translate them in a multi-language app.
678
+ */
679
+ clearLabel = input('Effacer', ...(ngDevMode ? [{ debugName: "clearLabel" }] : []));
680
+ incrementLabel = input('Augmenter', ...(ngDevMode ? [{ debugName: "incrementLabel" }] : []));
681
+ decrementLabel = input('Diminuer', ...(ngDevMode ? [{ debugName: "decrementLabel" }] : []));
682
+ /** The committed numeric value (driven by CVA + user input). */
683
+ value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
684
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
685
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
686
+ describedBy = computed(() => {
687
+ if (this.error())
688
+ return `${this.inputId}-err`;
689
+ if (this.hint())
690
+ return `${this.inputId}-hint`;
691
+ return null;
692
+ }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
693
+ clearVisible = computed(() => this.clearable() && this.value() !== null && !this.isDisabled() && !this.readonly(), ...(ngDevMode ? [{ debugName: "clearVisible" }] : []));
694
+ incrementIconName = computed(() => this.incrementIcon() ?? (this.buttonLayout() === 'stacked' ? 'chevron-up' : 'plus'), ...(ngDevMode ? [{ debugName: "incrementIconName" }] : []));
695
+ decrementIconName = computed(() => this.decrementIcon() ?? (this.buttonLayout() === 'stacked' ? 'chevron-down' : 'minus'), ...(ngDevMode ? [{ debugName: "decrementIconName" }] : []));
696
+ canIncrement = computed(() => {
697
+ if (this.isDisabled() || this.readonly())
698
+ return false;
699
+ const max = this.max();
700
+ return max === null || (this.value() ?? this.min() ?? 0) < max;
701
+ }, ...(ngDevMode ? [{ debugName: "canIncrement" }] : []));
702
+ canDecrement = computed(() => {
703
+ if (this.isDisabled() || this.readonly())
704
+ return false;
705
+ const min = this.min();
706
+ return min === null || (this.value() ?? min ?? 0) > min;
707
+ }, ...(ngDevMode ? [{ debugName: "canDecrement" }] : []));
708
+ /** Locale grouping/decimal separators, derived from Intl so parsing matches formatting. */
709
+ separators = computed(() => {
710
+ const sample = new Intl.NumberFormat(this.locale() || undefined, {
711
+ useGrouping: true,
712
+ }).formatToParts(1234567.8);
713
+ return {
714
+ group: sample.find((p) => p.type === 'group')?.value ?? ',',
715
+ decimal: sample.find((p) => p.type === 'decimal')?.value ?? '.',
716
+ };
717
+ }, ...(ngDevMode ? [{ debugName: "separators" }] : []));
718
+ /** Base Intl options (style/currency/grouping) without any fraction-digit constraints. */
719
+ baseOptions() {
720
+ const opts = { useGrouping: this.useGrouping() };
721
+ if (this.mode() === 'currency') {
722
+ opts.style = 'currency';
723
+ opts.currency = this.currency() || 'USD';
724
+ opts.currencyDisplay = this.currencyDisplay();
725
+ }
726
+ else {
727
+ opts.style = 'decimal';
728
+ }
729
+ return opts;
730
+ }
731
+ committedOptions = computed(() => {
732
+ const opts = this.baseOptions();
733
+ const min = this.minFractionDigits();
734
+ const max = this.maxFractionDigits();
735
+ if (min !== null)
736
+ opts.minimumFractionDigits = min;
737
+ if (max !== null)
738
+ opts.maximumFractionDigits = min !== null ? Math.max(max, min) : max;
739
+ return opts;
740
+ }, ...(ngDevMode ? [{ debugName: "committedOptions" }] : []));
741
+ /** The most fraction digits the field will keep — the resolved Intl maximum (2 for USD, 0 for JPY…). */
742
+ maxFrac = computed(() => {
743
+ const fallback = this.maxFractionDigits() ?? (this.mode() === 'currency' ? 2 : 3);
744
+ try {
745
+ const resolved = new Intl.NumberFormat(this.locale() || undefined, this.committedOptions()).resolvedOptions().maximumFractionDigits;
746
+ return resolved ?? fallback;
747
+ }
748
+ catch {
749
+ return fallback;
750
+ }
751
+ }, ...(ngDevMode ? [{ debugName: "maxFrac" }] : []));
752
+ wrapperClasses = computed(() => {
753
+ const vertical = this.buttonLayout() === 'vertical' && this.showButtons();
754
+ const parts = [
755
+ 'overflow-hidden rounded-control',
756
+ vertical ? 'flex flex-col' : `flex items-stretch ${HEIGHTS[this.size()]}`,
757
+ ];
758
+ if (this.isDisabled())
759
+ parts.push('opacity-60 cursor-not-allowed');
760
+ if (this.appearance() === 'filled') {
761
+ parts.push('border-transparent bg-muted text-ink');
762
+ parts.push(this.error()
763
+ ? 'border-danger focus-within:ring-2 focus-within:ring-danger/30'
764
+ : 'focus-within:ring-2 focus-within:ring-brand-500/25');
765
+ }
766
+ else {
767
+ parts.push('bg-white text-ink');
768
+ parts.push(this.error()
769
+ ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
770
+ : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
771
+ }
772
+ return parts.join(' ');
773
+ }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
774
+ fieldCellClasses = computed(() => this.buttonLayout() === 'vertical' && this.showButtons() ? HEIGHTS[this.size()] : 'h-full', ...(ngDevMode ? [{ debugName: "fieldCellClasses" }] : []));
775
+ inputClasses = computed(() => {
776
+ const parts = [TEXT[this.size()], 'pl-3.5', this.clearVisible() ? 'pr-9' : 'pr-3.5'];
777
+ if (this.showButtons() && this.buttonLayout() !== 'stacked')
778
+ parts.push('text-center');
779
+ return parts.join(' ');
780
+ }, ...(ngDevMode ? [{ debugName: "inputClasses" }] : []));
781
+ buttonBase = 'inline-flex shrink-0 items-center justify-center text-ink-soft transition-colors ' +
782
+ 'hover:bg-muted hover:text-ink disabled:cursor-not-allowed disabled:opacity-40 select-none';
783
+ edgeButtonClasses = computed(() => this.buttonLayout() === 'vertical'
784
+ ? `${this.buttonBase} ${HEIGHTS[this.size()]} border-border`
785
+ : `${this.buttonBase} w-10 border-border`, ...(ngDevMode ? [{ debugName: "edgeButtonClasses" }] : []));
786
+ stackedButtonClasses = computed(() => this.buttonBase, ...(ngDevMode ? [{ debugName: "stackedButtonClasses" }] : []));
787
+ constructor() {
788
+ // Reflect the committed value into the field whenever it — or a formatting input — changes from
789
+ // OUTSIDE typing (initial render, writeValue, a spin while blurred, a currency/locale switch).
790
+ // While the field is the active element the input handler owns the DOM value and its caret, so this
791
+ // stands down: gating on the live `document.activeElement` (not a signal) is immune to focus-event
792
+ // timing, which is what makes live formatting safe.
793
+ effect(() => {
794
+ const el = this.fieldRef()?.nativeElement;
795
+ const v = this.value();
796
+ // Touch the formatting inputs so a currency/locale/affix change repaints a blurred field.
797
+ this.committedOptions();
798
+ this.prefix();
799
+ this.suffix();
800
+ if (!el || this.isActive(el))
801
+ return;
802
+ el.value = v === null ? '' : this.format(v);
803
+ });
804
+ }
805
+ isActive(el) {
806
+ return typeof document !== 'undefined' && document.activeElement === el;
807
+ }
808
+ /** Paint the committed value into the field imperatively (used by spin/clear/blur). */
809
+ paintCommitted() {
810
+ const el = this.fieldRef()?.nativeElement;
811
+ if (!el)
812
+ return;
813
+ const v = this.value();
814
+ el.value = v === null ? '' : this.format(v);
815
+ }
816
+ // --- Formatting / parsing helpers -----------------------------------------
817
+ /** Format a committed value with the configured min/max fraction digits (used at rest and on blur). */
818
+ format(n) {
819
+ try {
820
+ const body = new Intl.NumberFormat(this.locale() || undefined, this.committedOptions()).format(n);
821
+ return `${this.prefix()}${body}${this.suffix()}`;
822
+ }
823
+ catch {
824
+ return `${this.prefix()}${n}${this.suffix()}`;
825
+ }
826
+ }
827
+ /**
828
+ * Format a partially-typed value, keeping exactly the fraction digits the user has entered (so
829
+ * trailing zeros and a lone decimal separator survive) while still grouping the integer part and
830
+ * showing the currency symbol / affixes.
831
+ */
832
+ formatLive(p) {
833
+ const locale = this.locale() || undefined;
834
+ const fracCount = p.fracDigits.length;
835
+ const num = this.toNumber(p) ?? 0;
836
+ if (fracCount > 0 || !p.hadDecimal) {
837
+ try {
838
+ const body = new Intl.NumberFormat(locale, {
839
+ ...this.baseOptions(),
840
+ minimumFractionDigits: fracCount,
841
+ maximumFractionDigits: fracCount,
842
+ }).format(num);
843
+ return `${this.prefix()}${body}${this.suffix()}`;
844
+ }
845
+ catch {
846
+ return `${this.prefix()}${num}${this.suffix()}`;
847
+ }
848
+ }
849
+ // A lone decimal separator was just typed ("1234,"): format the integer, then splice the separator
850
+ // in right after the integer digits — which, for a trailing-symbol currency, is NOT the string end.
851
+ const dec = this.separators().decimal;
852
+ let out = '';
853
+ let inserted = false;
854
+ try {
855
+ const parts = new Intl.NumberFormat(locale, {
856
+ ...this.baseOptions(),
857
+ maximumFractionDigits: 0,
858
+ }).formatToParts(num);
859
+ parts.forEach((part, i) => {
860
+ out += part.value;
861
+ const here = part.type === 'integer' || part.type === 'group';
862
+ const next = parts[i + 1];
863
+ const nextHere = next && (next.type === 'integer' || next.type === 'group');
864
+ if (here && !nextHere && !inserted) {
865
+ out += dec;
866
+ inserted = true;
867
+ }
868
+ });
869
+ }
870
+ catch {
871
+ out = `${num}${dec}`;
872
+ inserted = true;
873
+ }
874
+ if (!inserted)
875
+ out += dec;
876
+ return `${this.prefix()}${out}${this.suffix()}`;
877
+ }
878
+ toNumber(p) {
879
+ const s = `${p.negative ? '-' : ''}${p.intDigits || '0'}${p.fracDigits ? '.' + p.fracDigits : ''}`;
880
+ const n = Number(s);
881
+ return Number.isFinite(n) ? n : null;
882
+ }
883
+ /** Pull the sign / integer / fraction fragments out of a formatted, partially-typed string. */
884
+ parseInput(raw) {
885
+ const { group, decimal } = this.separators();
886
+ let t = raw;
887
+ const prefix = this.prefix();
888
+ const suffix = this.suffix();
889
+ if (prefix && t.startsWith(prefix))
890
+ t = t.slice(prefix.length);
891
+ if (suffix && t.endsWith(suffix))
892
+ t = t.slice(0, t.length - suffix.length);
893
+ if (group)
894
+ t = t.split(group).join('');
895
+ if (decimal && decimal !== '.')
896
+ t = t.split(decimal).join('.');
897
+ t = t.replace(/[^0-9.\-]/g, '');
898
+ const negative = t.includes('-');
899
+ t = t.replace(/-/g, '');
900
+ const dot = t.indexOf('.');
901
+ const hadDecimal = dot !== -1 && this.maxFrac() > 0;
902
+ let intDigits = (hadDecimal ? t.slice(0, dot) : t.replace(/\./g, '')).replace(/\D/g, '');
903
+ let fracDigits = hadDecimal ? t.slice(dot + 1).replace(/\D/g, '') : '';
904
+ // Strip a leading run of zeros ("007" -> "7") but keep a lone "0".
905
+ intDigits = intDigits.replace(/^0+(?=\d)/, '');
906
+ fracDigits = fracDigits.slice(0, this.maxFrac());
907
+ return { negative, intDigits, fracDigits, hadDecimal };
908
+ }
909
+ /** Characters that "carry meaning" for caret placement (digits, minus, the decimal separator). */
910
+ isSignificant(ch) {
911
+ return (ch >= '0' && ch <= '9') || ch === '-' || ch === this.separators().decimal;
912
+ }
913
+ significantBefore(s, upto) {
914
+ let n = 0;
915
+ for (let i = 0; i < upto && i < s.length; i++)
916
+ if (this.isSignificant(s[i]))
917
+ n++;
918
+ return n;
919
+ }
920
+ /** The caret index in `formatted` that sits just after `target` significant characters. */
921
+ caretAfter(formatted, target) {
922
+ if (target <= 0) {
923
+ for (let i = 0; i < formatted.length; i++)
924
+ if (this.isSignificant(formatted[i]))
925
+ return i;
926
+ return formatted.length;
927
+ }
928
+ let n = 0;
929
+ for (let i = 0; i < formatted.length; i++) {
930
+ if (this.isSignificant(formatted[i]) && ++n === target)
931
+ return i + 1;
932
+ }
933
+ return formatted.length;
934
+ }
935
+ clamp(n) {
936
+ const min = this.min();
937
+ const max = this.max();
938
+ if (min !== null && n < min)
939
+ n = min;
940
+ if (max !== null && n > max)
941
+ n = max;
942
+ return n;
943
+ }
944
+ /** Trim binary-float drift (0.1 + 0.2) back to a clean decimal. */
945
+ tidy(n) {
946
+ return Math.round(n * 1e10) / 1e10;
947
+ }
948
+ // --- Interaction ----------------------------------------------------------
949
+ onInput(event) {
950
+ const el = event.target;
951
+ const raw = el.value;
952
+ const caret = el.selectionStart ?? raw.length;
953
+ const target = this.significantBefore(raw, caret);
954
+ const parsed = this.parseInput(raw);
955
+ if (parsed.intDigits === '' && parsed.fracDigits === '' && !parsed.hadDecimal) {
956
+ // Nothing numeric left — allow a lone "-" mid-entry, otherwise empty.
957
+ const text = parsed.negative ? '-' : '';
958
+ el.value = text;
959
+ el.setSelectionRange(text.length, text.length);
960
+ this.value.set(null);
961
+ this.onChange(null);
962
+ return;
963
+ }
964
+ const num = this.toNumber(parsed);
965
+ const formatted = this.formatLive(parsed);
966
+ el.value = formatted;
967
+ const next = this.caretAfter(formatted, target);
968
+ el.setSelectionRange(next, next);
969
+ this.value.set(num);
970
+ this.onChange(num);
971
+ }
972
+ onKeydown(event) {
973
+ if (event.key === 'ArrowUp') {
974
+ event.preventDefault();
975
+ this.spin(1);
976
+ }
977
+ else if (event.key === 'ArrowDown') {
978
+ event.preventDefault();
979
+ this.spin(-1);
980
+ }
981
+ }
982
+ onBlur() {
983
+ let v = this.value();
984
+ if (v === null && !this.allowEmpty())
985
+ v = this.min() ?? 0;
986
+ if (v !== null)
987
+ v = this.clamp(this.tidy(v));
988
+ // Re-render through the committed formatter (applies min-fraction padding + clamping).
989
+ this.value.set(v);
990
+ this.paintCommitted();
991
+ this.onChange(v);
992
+ this.onTouched();
993
+ }
994
+ spin(direction) {
995
+ if (this.isDisabled() || this.readonly())
996
+ return;
997
+ const base = this.value() ?? this.min() ?? 0;
998
+ const nextValue = this.clamp(this.tidy(base + direction * this.step()));
999
+ this.value.set(nextValue);
1000
+ this.onChange(nextValue);
1001
+ this.onTouched();
1002
+ // Paint the stepped value ourselves — the effect stands down while the field is focused.
1003
+ const el = this.fieldRef()?.nativeElement;
1004
+ if (el) {
1005
+ const text = this.format(nextValue);
1006
+ el.value = text;
1007
+ if (this.isActive(el))
1008
+ el.setSelectionRange(text.length, text.length);
1009
+ }
1010
+ }
1011
+ clear() {
1012
+ this.value.set(null);
1013
+ this.onChange(null);
1014
+ this.onTouched();
1015
+ const el = this.fieldRef()?.nativeElement;
1016
+ if (el)
1017
+ el.value = '';
1018
+ }
1019
+ // --- ControlValueAccessor -------------------------------------------------
1020
+ onChange = () => { };
1021
+ onTouched = () => { };
1022
+ writeValue(value) {
1023
+ this.value.set(typeof value === 'number' && Number.isFinite(value) ? value : null);
1024
+ }
1025
+ registerOnChange(fn) {
1026
+ this.onChange = fn;
1027
+ }
1028
+ registerOnTouched(fn) {
1029
+ this.onTouched = fn;
1030
+ }
1031
+ setDisabledState(isDisabled) {
1032
+ this.cvaDisabled.set(isDisabled);
1033
+ }
1034
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsInputNumber, deps: [], target: i0.ɵɵFactoryTarget.Component });
1035
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsInputNumber, isStandalone: true, selector: "sds-input-number", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, currencyDisplay: { classPropertyName: "currencyDisplay", publicName: "currencyDisplay", isSignal: true, isRequired: false, transformFunction: null }, useGrouping: { classPropertyName: "useGrouping", publicName: "useGrouping", isSignal: true, isRequired: false, transformFunction: null }, minFractionDigits: { classPropertyName: "minFractionDigits", publicName: "minFractionDigits", isSignal: true, isRequired: false, transformFunction: null }, maxFractionDigits: { classPropertyName: "maxFractionDigits", publicName: "maxFractionDigits", 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 }, 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 }, showButtons: { classPropertyName: "showButtons", publicName: "showButtons", isSignal: true, isRequired: false, transformFunction: null }, buttonLayout: { classPropertyName: "buttonLayout", publicName: "buttonLayout", isSignal: true, isRequired: false, transformFunction: null }, incrementIcon: { classPropertyName: "incrementIcon", publicName: "incrementIcon", isSignal: true, isRequired: false, transformFunction: null }, decrementIcon: { classPropertyName: "decrementIcon", publicName: "decrementIcon", isSignal: true, isRequired: false, transformFunction: null }, allowEmpty: { classPropertyName: "allowEmpty", publicName: "allowEmpty", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, incrementLabel: { classPropertyName: "incrementLabel", publicName: "incrementLabel", isSignal: true, isRequired: false, transformFunction: null }, decrementLabel: { classPropertyName: "decrementLabel", publicName: "decrementLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, providers: [
1036
+ {
1037
+ provide: NG_VALUE_ACCESSOR,
1038
+ useExisting: forwardRef(() => SdsInputNumber),
1039
+ multi: true,
1040
+ },
1041
+ ], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], ngImport: i0, template: `
1042
+ @if (label()) {
1043
+ <label [for]="inputId" class="mb-1.5 block text-sm font-medium text-ink">{{ label() }}</label>
1044
+ }
1045
+
1046
+ <div class="relative border transition-colors" [class]="wrapperClasses()">
1047
+ <!-- Horizontal: minus before the field. Vertical: plus above it. -->
1048
+ @if (showButtons() && buttonLayout() === 'horizontal') {
1049
+ <button
1050
+ type="button"
1051
+ tabindex="-1"
1052
+ [attr.aria-label]="decrementLabel()"
1053
+ [disabled]="!canDecrement()"
1054
+ class="border-r"
1055
+ [class]="edgeButtonClasses()"
1056
+ (mousedown)="$event.preventDefault()"
1057
+ (click)="spin(-1)"
1058
+ >
1059
+ <sds-icon [name]="decrementIconName()" [size]="16" />
1060
+ </button>
1061
+ }
1062
+ @if (showButtons() && buttonLayout() === 'vertical') {
1063
+ <button
1064
+ type="button"
1065
+ tabindex="-1"
1066
+ [attr.aria-label]="incrementLabel()"
1067
+ [disabled]="!canIncrement()"
1068
+ class="w-full border-b"
1069
+ [class]="edgeButtonClasses()"
1070
+ (mousedown)="$event.preventDefault()"
1071
+ (click)="spin(1)"
1072
+ >
1073
+ <sds-icon [name]="incrementIconName()" [size]="16" />
1074
+ </button>
1075
+ }
1076
+
1077
+ <div class="relative flex min-w-0 flex-1 items-center" [class]="fieldCellClasses()">
1078
+ <input
1079
+ #field
1080
+ [id]="inputId"
1081
+ type="text"
1082
+ inputmode="decimal"
1083
+ role="spinbutton"
1084
+ [placeholder]="placeholder()"
1085
+ [disabled]="isDisabled()"
1086
+ [readOnly]="readonly()"
1087
+ [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
1088
+ [attr.aria-invalid]="error() ? 'true' : null"
1089
+ [attr.aria-describedby]="describedBy()"
1090
+ [attr.aria-valuenow]="value()"
1091
+ [attr.aria-valuemin]="min()"
1092
+ [attr.aria-valuemax]="max()"
1093
+ class="w-full bg-transparent outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1094
+ [class]="inputClasses()"
1095
+ (input)="onInput($event)"
1096
+ (keydown)="onKeydown($event)"
1097
+ (blur)="onBlur()"
1098
+ />
1099
+
1100
+ @if (clearVisible()) {
1101
+ <button
1102
+ type="button"
1103
+ tabindex="-1"
1104
+ [attr.aria-label]="clearLabel()"
1105
+ class="absolute right-1.5 inline-flex h-6 w-6 items-center justify-center rounded-full opacity-60 hover:bg-black/5 hover:opacity-100"
1106
+ (mousedown)="$event.preventDefault()"
1107
+ (click)="clear()"
1108
+ >
1109
+ <sds-icon name="x" [size]="15" />
1110
+ </button>
1111
+ }
1112
+ </div>
1113
+
1114
+ <!-- Horizontal: plus after the field. Vertical: minus below it. -->
1115
+ @if (showButtons() && buttonLayout() === 'horizontal') {
1116
+ <button
1117
+ type="button"
1118
+ tabindex="-1"
1119
+ [attr.aria-label]="incrementLabel()"
1120
+ [disabled]="!canIncrement()"
1121
+ class="border-l"
1122
+ [class]="edgeButtonClasses()"
1123
+ (mousedown)="$event.preventDefault()"
1124
+ (click)="spin(1)"
1125
+ >
1126
+ <sds-icon [name]="incrementIconName()" [size]="16" />
1127
+ </button>
1128
+ }
1129
+ @if (showButtons() && buttonLayout() === 'vertical') {
1130
+ <button
1131
+ type="button"
1132
+ tabindex="-1"
1133
+ [attr.aria-label]="decrementLabel()"
1134
+ [disabled]="!canDecrement()"
1135
+ class="w-full border-t"
1136
+ [class]="edgeButtonClasses()"
1137
+ (mousedown)="$event.preventDefault()"
1138
+ (click)="spin(-1)"
1139
+ >
1140
+ <sds-icon [name]="decrementIconName()" [size]="16" />
1141
+ </button>
1142
+ }
1143
+
1144
+ <!-- Stacked: a chevron column at the trailing edge. -->
1145
+ @if (showButtons() && buttonLayout() === 'stacked') {
1146
+ <div class="flex w-9 shrink-0 flex-col divide-y divide-border border-l border-border">
1147
+ <button
1148
+ type="button"
1149
+ tabindex="-1"
1150
+ [attr.aria-label]="incrementLabel()"
1151
+ [disabled]="!canIncrement()"
1152
+ class="min-h-0 flex-1"
1153
+ [class]="stackedButtonClasses()"
1154
+ (mousedown)="$event.preventDefault()"
1155
+ (click)="spin(1)"
1156
+ >
1157
+ <sds-icon [name]="incrementIconName()" [size]="14" />
1158
+ </button>
1159
+ <button
1160
+ type="button"
1161
+ tabindex="-1"
1162
+ [attr.aria-label]="decrementLabel()"
1163
+ [disabled]="!canDecrement()"
1164
+ class="min-h-0 flex-1"
1165
+ [class]="stackedButtonClasses()"
1166
+ (mousedown)="$event.preventDefault()"
1167
+ (click)="spin(-1)"
1168
+ >
1169
+ <sds-icon [name]="decrementIconName()" [size]="14" />
1170
+ </button>
1171
+ </div>
1172
+ }
1173
+ </div>
1174
+
1175
+ @if (error()) {
1176
+ <p [id]="inputId + '-err'" class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
1177
+ } @else if (hint()) {
1178
+ <p [id]="inputId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
1179
+ }
1180
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1181
+ }
1182
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsInputNumber, decorators: [{
1183
+ type: Component,
1184
+ args: [{
1185
+ selector: 'sds-input-number',
1186
+ changeDetection: ChangeDetectionStrategy.OnPush,
1187
+ imports: [SdsIcon],
1188
+ providers: [
1189
+ {
1190
+ provide: NG_VALUE_ACCESSOR,
1191
+ useExisting: forwardRef(() => SdsInputNumber),
1192
+ multi: true,
1193
+ },
1194
+ ],
1195
+ host: { class: 'block' },
1196
+ template: `
1197
+ @if (label()) {
1198
+ <label [for]="inputId" class="mb-1.5 block text-sm font-medium text-ink">{{ label() }}</label>
1199
+ }
1200
+
1201
+ <div class="relative border transition-colors" [class]="wrapperClasses()">
1202
+ <!-- Horizontal: minus before the field. Vertical: plus above it. -->
1203
+ @if (showButtons() && buttonLayout() === 'horizontal') {
1204
+ <button
1205
+ type="button"
1206
+ tabindex="-1"
1207
+ [attr.aria-label]="decrementLabel()"
1208
+ [disabled]="!canDecrement()"
1209
+ class="border-r"
1210
+ [class]="edgeButtonClasses()"
1211
+ (mousedown)="$event.preventDefault()"
1212
+ (click)="spin(-1)"
1213
+ >
1214
+ <sds-icon [name]="decrementIconName()" [size]="16" />
1215
+ </button>
1216
+ }
1217
+ @if (showButtons() && buttonLayout() === 'vertical') {
1218
+ <button
1219
+ type="button"
1220
+ tabindex="-1"
1221
+ [attr.aria-label]="incrementLabel()"
1222
+ [disabled]="!canIncrement()"
1223
+ class="w-full border-b"
1224
+ [class]="edgeButtonClasses()"
1225
+ (mousedown)="$event.preventDefault()"
1226
+ (click)="spin(1)"
1227
+ >
1228
+ <sds-icon [name]="incrementIconName()" [size]="16" />
1229
+ </button>
1230
+ }
1231
+
1232
+ <div class="relative flex min-w-0 flex-1 items-center" [class]="fieldCellClasses()">
1233
+ <input
1234
+ #field
1235
+ [id]="inputId"
1236
+ type="text"
1237
+ inputmode="decimal"
1238
+ role="spinbutton"
1239
+ [placeholder]="placeholder()"
1240
+ [disabled]="isDisabled()"
1241
+ [readOnly]="readonly()"
1242
+ [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
1243
+ [attr.aria-invalid]="error() ? 'true' : null"
1244
+ [attr.aria-describedby]="describedBy()"
1245
+ [attr.aria-valuenow]="value()"
1246
+ [attr.aria-valuemin]="min()"
1247
+ [attr.aria-valuemax]="max()"
1248
+ class="w-full bg-transparent outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1249
+ [class]="inputClasses()"
1250
+ (input)="onInput($event)"
1251
+ (keydown)="onKeydown($event)"
1252
+ (blur)="onBlur()"
1253
+ />
1254
+
1255
+ @if (clearVisible()) {
1256
+ <button
1257
+ type="button"
1258
+ tabindex="-1"
1259
+ [attr.aria-label]="clearLabel()"
1260
+ class="absolute right-1.5 inline-flex h-6 w-6 items-center justify-center rounded-full opacity-60 hover:bg-black/5 hover:opacity-100"
1261
+ (mousedown)="$event.preventDefault()"
1262
+ (click)="clear()"
1263
+ >
1264
+ <sds-icon name="x" [size]="15" />
1265
+ </button>
1266
+ }
1267
+ </div>
1268
+
1269
+ <!-- Horizontal: plus after the field. Vertical: minus below it. -->
1270
+ @if (showButtons() && buttonLayout() === 'horizontal') {
1271
+ <button
1272
+ type="button"
1273
+ tabindex="-1"
1274
+ [attr.aria-label]="incrementLabel()"
1275
+ [disabled]="!canIncrement()"
1276
+ class="border-l"
1277
+ [class]="edgeButtonClasses()"
1278
+ (mousedown)="$event.preventDefault()"
1279
+ (click)="spin(1)"
1280
+ >
1281
+ <sds-icon [name]="incrementIconName()" [size]="16" />
1282
+ </button>
1283
+ }
1284
+ @if (showButtons() && buttonLayout() === 'vertical') {
1285
+ <button
1286
+ type="button"
1287
+ tabindex="-1"
1288
+ [attr.aria-label]="decrementLabel()"
1289
+ [disabled]="!canDecrement()"
1290
+ class="w-full border-t"
1291
+ [class]="edgeButtonClasses()"
1292
+ (mousedown)="$event.preventDefault()"
1293
+ (click)="spin(-1)"
1294
+ >
1295
+ <sds-icon [name]="decrementIconName()" [size]="16" />
1296
+ </button>
1297
+ }
1298
+
1299
+ <!-- Stacked: a chevron column at the trailing edge. -->
1300
+ @if (showButtons() && buttonLayout() === 'stacked') {
1301
+ <div class="flex w-9 shrink-0 flex-col divide-y divide-border border-l border-border">
1302
+ <button
1303
+ type="button"
1304
+ tabindex="-1"
1305
+ [attr.aria-label]="incrementLabel()"
1306
+ [disabled]="!canIncrement()"
1307
+ class="min-h-0 flex-1"
1308
+ [class]="stackedButtonClasses()"
1309
+ (mousedown)="$event.preventDefault()"
1310
+ (click)="spin(1)"
1311
+ >
1312
+ <sds-icon [name]="incrementIconName()" [size]="14" />
1313
+ </button>
1314
+ <button
1315
+ type="button"
1316
+ tabindex="-1"
1317
+ [attr.aria-label]="decrementLabel()"
1318
+ [disabled]="!canDecrement()"
1319
+ class="min-h-0 flex-1"
1320
+ [class]="stackedButtonClasses()"
1321
+ (mousedown)="$event.preventDefault()"
1322
+ (click)="spin(-1)"
1323
+ >
1324
+ <sds-icon [name]="decrementIconName()" [size]="14" />
1325
+ </button>
1326
+ </div>
1327
+ }
1328
+ </div>
1329
+
1330
+ @if (error()) {
1331
+ <p [id]="inputId + '-err'" class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
1332
+ } @else if (hint()) {
1333
+ <p [id]="inputId + '-hint'" class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
1334
+ }
1335
+ `,
1336
+ }]
1337
+ }], ctorParameters: () => [], propDecorators: { fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDisplay", required: false }] }], useGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "useGrouping", required: false }] }], minFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "minFractionDigits", required: false }] }], maxFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFractionDigits", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", 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 }] }], showButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtons", required: false }] }], buttonLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLayout", required: false }] }], incrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementIcon", required: false }] }], decrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementIcon", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }] } });
1338
+
1339
+ let nextId$b = 0;
1340
+ const SIZES$9 = {
1341
+ sm: 'min-h-9 text-sm',
1342
+ md: 'min-h-10 text-sm',
1343
+ lg: 'min-h-12 text-base',
1344
+ };
1345
+ /**
1346
+ * One language-aware text field for editing a translated value (a {@link SdsLocalizedValue} — a title,
1347
+ * label or description in several languages). Only one language is edited at a time; a compact switcher
1348
+ * sitting **inside the field** flips which translation you are editing, and a dot marks the languages that
1349
+ * already have content, so a partially-translated value is obvious at a glance.
1350
+ *
1351
+ * The active language defaults to {@link currentLanguage} when it is one of {@link languages}, otherwise the
1352
+ * first accepted language — so the field "opens on the language you are working in". Empty translations are
1353
+ * pruned from the emitted value, keeping the stored map clean.
1354
+ *
1355
+ * Implements ControlValueAccessor over the map — use with reactive forms or `[(ngModel)]`:
1356
+ * `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
1357
+ */
1358
+ class SdsLocalizedInput {
1359
+ inputId = `sds-localized-input-${nextId$b++}`;
1360
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
1361
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
1362
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
1363
+ /** The accepted language codes, in display order (e.g. `['fr','nl','en']`). */
1364
+ languages = input([], ...(ngDevMode ? [{ debugName: "languages" }] : []));
1365
+ /** Language to pre-select; used when it is one of {@link languages}, else the first accepted language. */
1366
+ currentLanguage = input(null, ...(ngDevMode ? [{ debugName: "currentLanguage" }] : []));
1367
+ multiline = input(false, ...(ngDevMode ? [{ debugName: "multiline", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1368
+ rows = input(3, ...(ngDevMode ? [{ debugName: "rows", transform: numberAttribute }] : [{ transform: numberAttribute }]));
1369
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
1370
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
1371
+ /**
1372
+ * Accessible name for the in-field language switcher, and the "no translation yet" title suffix on each
1373
+ * language button. French defaults so existing consumers are unchanged; translate them in a multi-language
1374
+ * app (the buttons are letter-only, so the title carries their meaning).
1375
+ */
1376
+ languageSwitcherLabel = input('Langue', ...(ngDevMode ? [{ debugName: "languageSwitcherLabel" }] : []));
1377
+ untranslatedLabel = input('à traduire', ...(ngDevMode ? [{ debugName: "untranslatedLabel" }] : []));
1378
+ valueMap = signal({}, ...(ngDevMode ? [{ debugName: "valueMap" }] : []));
1379
+ /** Language chosen via the switcher; null means "follow currentLanguage / the first language". */
1380
+ manual = signal(null, ...(ngDevMode ? [{ debugName: "manual" }] : []));
1381
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
1382
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
1383
+ wrapperClasses = computed(() => {
1384
+ const parts = [SIZES$9[this.size()]];
1385
+ parts.push(this.isDisabled()
1386
+ ? 'border-border opacity-60 cursor-not-allowed'
1387
+ : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
1388
+ return parts.join(' ');
1389
+ }, ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : []));
1390
+ /** The language currently being edited. */
1391
+ activeLang = computed(() => {
1392
+ const langs = this.languages();
1393
+ if (!langs.length) {
1394
+ return '';
1395
+ }
1396
+ const manual = this.manual();
1397
+ if (manual && langs.includes(manual)) {
1398
+ return manual;
1399
+ }
1400
+ const preferred = this.currentLanguage();
1401
+ return preferred && langs.includes(preferred) ? preferred : langs[0];
1402
+ }, ...(ngDevMode ? [{ debugName: "activeLang" }] : []));
1403
+ current = computed(() => this.valueMap()[this.activeLang()] ?? '', ...(ngDevMode ? [{ debugName: "current" }] : []));
1404
+ filled(lang) {
1405
+ const text = this.valueMap()[lang];
1406
+ return !!text && text.trim().length > 0;
1407
+ }
1408
+ fieldPlaceholder() {
1409
+ const base = this.placeholder();
1410
+ const lang = this.activeLang().toUpperCase();
1411
+ if (base) {
1412
+ return lang ? `${base} (${lang})` : base;
1413
+ }
1414
+ return lang ? `(${lang})` : '';
1415
+ }
1416
+ select(lang) {
1417
+ this.manual.set(lang);
1418
+ this.onTouched();
1419
+ }
1420
+ write(event) {
1421
+ const lang = this.activeLang();
1422
+ if (!lang) {
1423
+ return;
1424
+ }
1425
+ const text = event.target.value;
1426
+ const next = { ...this.valueMap() };
1427
+ if (text) {
1428
+ next[lang] = text;
1429
+ }
1430
+ else {
1431
+ delete next[lang];
1432
+ }
1433
+ this.valueMap.set(next);
1434
+ this.onChange(next);
1435
+ }
1436
+ // --- ControlValueAccessor -------------------------------------------------
1437
+ onChange = () => { };
1438
+ onTouched = () => { };
1439
+ writeValue(value) {
1440
+ this.valueMap.set(value ? { ...value } : {});
1441
+ }
1442
+ registerOnChange(fn) {
1443
+ this.onChange = fn;
1444
+ }
1445
+ registerOnTouched(fn) {
1446
+ this.onTouched = fn;
1447
+ }
1448
+ setDisabledState(isDisabled) {
1449
+ this.cvaDisabled.set(isDisabled);
1450
+ }
1451
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsLocalizedInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
1452
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsLocalizedInput, isStandalone: true, selector: "sds-localized-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, currentLanguage: { classPropertyName: "currentLanguage", publicName: "currentLanguage", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, languageSwitcherLabel: { classPropertyName: "languageSwitcherLabel", publicName: "languageSwitcherLabel", isSignal: true, isRequired: false, transformFunction: null }, untranslatedLabel: { classPropertyName: "untranslatedLabel", publicName: "untranslatedLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, providers: [
1453
+ {
1454
+ provide: NG_VALUE_ACCESSOR,
1455
+ useExisting: forwardRef(() => SdsLocalizedInput),
1456
+ multi: true,
1457
+ },
1458
+ ], ngImport: i0, template: `
1459
+ @if (label()) {
1460
+ <label [for]="inputId" class="mb-1.5 block text-sm font-medium text-ink">{{ label() }}</label>
1461
+ }
1462
+
1463
+ <div
1464
+ class="relative flex items-stretch rounded-control border bg-white text-ink transition-colors"
1465
+ [class]="wrapperClasses()"
1466
+ >
1467
+ @if (multiline()) {
1468
+ <textarea
1469
+ [id]="inputId"
1470
+ [rows]="rows()"
1471
+ [placeholder]="fieldPlaceholder()"
1472
+ [disabled]="isDisabled()"
1473
+ [value]="current()"
1474
+ (input)="write($event)"
1475
+ (blur)="onTouched()"
1476
+ class="min-w-0 flex-1 resize-y bg-transparent px-3.5 py-2 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1477
+ ></textarea>
1478
+ } @else {
1479
+ <input
1480
+ [id]="inputId"
1481
+ [placeholder]="fieldPlaceholder()"
1482
+ [disabled]="isDisabled()"
1483
+ [value]="current()"
1484
+ (input)="write($event)"
1485
+ (blur)="onTouched()"
1486
+ class="min-w-0 flex-1 bg-transparent px-3.5 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1487
+ />
1488
+ }
1489
+
1490
+ @if (languages().length > 1) {
1491
+ <div
1492
+ class="flex shrink-0 items-center gap-0.5 self-start p-1"
1493
+ [class]="multiline() ? '' : 'self-center'"
1494
+ role="tablist"
1495
+ [attr.aria-label]="languageSwitcherLabel()"
1496
+ >
1497
+ @for (lang of languages(); track lang) {
1498
+ <button
1499
+ type="button"
1500
+ role="tab"
1501
+ tabindex="-1"
1502
+ [disabled]="isDisabled()"
1503
+ (click)="select(lang)"
1504
+ [attr.aria-selected]="activeLang() === lang"
1505
+ [attr.title]="lang.toUpperCase() + (filled(lang) ? '' : ' — ' + untranslatedLabel())"
1506
+ class="flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50"
1507
+ [class]="
1508
+ activeLang() === lang
1509
+ ? 'bg-brand-50 text-brand-700'
1510
+ : 'text-ink-faint hover:bg-muted hover:text-ink-soft'
1511
+ "
1512
+ >
1513
+ {{ lang }}
1514
+ <span
1515
+ class="inline-block h-1 w-1 rounded-full"
1516
+ [class]="filled(lang) ? 'bg-success' : 'bg-current/30'"
1517
+ ></span>
1518
+ </button>
1519
+ }
1520
+ </div>
1521
+ }
1522
+ </div>
1523
+
1524
+ @if (hint()) {
1525
+ <p class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
1526
+ }
1527
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1528
+ }
1529
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsLocalizedInput, decorators: [{
1530
+ type: Component,
1531
+ args: [{
1532
+ selector: 'sds-localized-input',
1533
+ changeDetection: ChangeDetectionStrategy.OnPush,
1534
+ providers: [
1535
+ {
1536
+ provide: NG_VALUE_ACCESSOR,
1537
+ useExisting: forwardRef(() => SdsLocalizedInput),
1538
+ multi: true,
1539
+ },
1540
+ ],
1541
+ host: { class: 'block' },
1542
+ template: `
1543
+ @if (label()) {
1544
+ <label [for]="inputId" class="mb-1.5 block text-sm font-medium text-ink">{{ label() }}</label>
1545
+ }
1546
+
1547
+ <div
1548
+ class="relative flex items-stretch rounded-control border bg-white text-ink transition-colors"
1549
+ [class]="wrapperClasses()"
1550
+ >
1551
+ @if (multiline()) {
1552
+ <textarea
1553
+ [id]="inputId"
1554
+ [rows]="rows()"
1555
+ [placeholder]="fieldPlaceholder()"
1556
+ [disabled]="isDisabled()"
1557
+ [value]="current()"
1558
+ (input)="write($event)"
1559
+ (blur)="onTouched()"
1560
+ class="min-w-0 flex-1 resize-y bg-transparent px-3.5 py-2 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1561
+ ></textarea>
1562
+ } @else {
1563
+ <input
1564
+ [id]="inputId"
1565
+ [placeholder]="fieldPlaceholder()"
1566
+ [disabled]="isDisabled()"
1567
+ [value]="current()"
1568
+ (input)="write($event)"
1569
+ (blur)="onTouched()"
1570
+ class="min-w-0 flex-1 bg-transparent px-3.5 outline-none placeholder:text-current/50 disabled:cursor-not-allowed"
1571
+ />
1572
+ }
1573
+
1574
+ @if (languages().length > 1) {
1575
+ <div
1576
+ class="flex shrink-0 items-center gap-0.5 self-start p-1"
1577
+ [class]="multiline() ? '' : 'self-center'"
1578
+ role="tablist"
1579
+ [attr.aria-label]="languageSwitcherLabel()"
1580
+ >
1581
+ @for (lang of languages(); track lang) {
1582
+ <button
1583
+ type="button"
1584
+ role="tab"
1585
+ tabindex="-1"
1586
+ [disabled]="isDisabled()"
1587
+ (click)="select(lang)"
1588
+ [attr.aria-selected]="activeLang() === lang"
1589
+ [attr.title]="lang.toUpperCase() + (filled(lang) ? '' : ' — ' + untranslatedLabel())"
1590
+ class="flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50"
1591
+ [class]="
1592
+ activeLang() === lang
1593
+ ? 'bg-brand-50 text-brand-700'
1594
+ : 'text-ink-faint hover:bg-muted hover:text-ink-soft'
1595
+ "
1596
+ >
1597
+ {{ lang }}
1598
+ <span
1599
+ class="inline-block h-1 w-1 rounded-full"
1600
+ [class]="filled(lang) ? 'bg-success' : 'bg-current/30'"
1601
+ ></span>
1602
+ </button>
1603
+ }
1604
+ </div>
1605
+ }
1606
+ </div>
1607
+
1608
+ @if (hint()) {
1609
+ <p class="mt-1.5 text-xs text-ink-faint">{{ hint() }}</p>
1610
+ }
1611
+ `,
1612
+ }]
1613
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], currentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentLanguage", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], languageSwitcherLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "languageSwitcherLabel", required: false }] }], untranslatedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "untranslatedLabel", required: false }] }] } });
1614
+
604
1615
  let nextId$a = 0;
605
1616
  const SIZES$8 = {
606
1617
  md: 'h-12 w-11 text-lg',
@@ -7398,6 +8409,157 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
7398
8409
  }]
7399
8410
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], gridlines: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridlines", required: false }] }], hoverable: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverable", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], paginator: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginator", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], showCurrentPageReport: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCurrentPageReport", required: false }] }], currentPageReportTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPageReportTemplate", required: false }] }], rowsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageLabel", required: false }] }], paginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], pageLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkLabel", required: false }] }], globalFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilter", required: false }] }], globalFilterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterPlaceholder", required: false }] }], globalFilterAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterAriaLabel", required: false }] }], globalFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterFields", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], resizableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableColumns", required: false }] }], reorderableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderableColumns", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowUnselect: [{ type: i0.Output, args: ["rowUnselect"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowExpand: [{ type: i0.Output, args: ["rowExpand"] }], rowCollapse: [{ type: i0.Output, args: ["rowCollapse"] }], colReorder: [{ type: i0.Output, args: ["colReorder"] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableCell), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }] } });
7400
8411
 
8412
+ /**
8413
+ * A tiny transient-notification bus. Application code calls {@link success} / {@link error} / {@link info}
8414
+ * after an action; a single {@link SdsToastHost} mounted in the app shell renders the queue and each toast
8415
+ * auto-dismisses. Deliberately minimal — no positions, no actions-in-toast — because it exists to confirm
8416
+ * an action, not to be a notification centre.
8417
+ *
8418
+ * `providedIn: 'root'`, so injecting it anywhere shares one queue:
8419
+ *
8420
+ * ```ts
8421
+ * private readonly toasts = inject(SdsToastService);
8422
+ * this.toasts.success('Enregistré.');
8423
+ * ```
8424
+ *
8425
+ * Mount the host once (e.g. in the shell): `<sds-toast-host />`.
8426
+ */
8427
+ class SdsToastService {
8428
+ seq = 0;
8429
+ toasts = signal([], ...(ngDevMode ? [{ debugName: "toasts" }] : []));
8430
+ /** How long a toast stays, in ms, by kind. Errors linger; success/info are brief. */
8431
+ static TTL = {
8432
+ success: 3500,
8433
+ info: 3500,
8434
+ error: 6000,
8435
+ };
8436
+ success(text) {
8437
+ this.push('success', text);
8438
+ }
8439
+ error(text) {
8440
+ this.push('error', text);
8441
+ }
8442
+ info(text) {
8443
+ this.push('info', text);
8444
+ }
8445
+ dismiss(id) {
8446
+ this.toasts.update((list) => list.filter((t) => t.id !== id));
8447
+ }
8448
+ push(kind, text) {
8449
+ const id = ++this.seq;
8450
+ this.toasts.update((list) => [...list, { id, kind, text }]);
8451
+ setTimeout(() => this.dismiss(id), SdsToastService.TTL[kind]);
8452
+ }
8453
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8454
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsToastService, providedIn: 'root' });
8455
+ }
8456
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsToastService, decorators: [{
8457
+ type: Injectable,
8458
+ args: [{ providedIn: 'root' }]
8459
+ }] });
8460
+
8461
+ /**
8462
+ * Renders the {@link SdsToastService} queue, fixed bottom-centre. Mount it once, in the app shell:
8463
+ * `<sds-toast-host />`. Application code pushes toasts through the injected service.
8464
+ *
8465
+ * Accessibility: the two live regions are ALWAYS in the DOM (even when empty), because a live region must
8466
+ * pre-exist for its later content changes to be announced. Errors go to an assertive `role="alert"` region
8467
+ * so a failure interrupts; success/info go to a polite region so they queue.
8468
+ */
8469
+ class SdsToastHost {
8470
+ toasts = inject(SdsToastService);
8471
+ /**
8472
+ * Accessible name for each toast's close button. French default so existing consumers are unchanged; the
8473
+ * button is icon-only, so an app serving more than one language MUST translate it.
8474
+ */
8475
+ closeLabel = input('Fermer', ...(ngDevMode ? [{ debugName: "closeLabel" }] : []));
8476
+ errors = computed(() => this.toasts.toasts().filter((t) => t.kind === 'error'), ...(ngDevMode ? [{ debugName: "errors" }] : []));
8477
+ notices = computed(() => this.toasts.toasts().filter((t) => t.kind !== 'error'), ...(ngDevMode ? [{ debugName: "notices" }] : []));
8478
+ icon(kind) {
8479
+ return kind === 'success' ? 'check-circle' : kind === 'error' ? 'alert-circle' : 'info';
8480
+ }
8481
+ tone(kind) {
8482
+ return kind === 'success' ? 'text-success' : kind === 'error' ? 'text-danger' : 'text-accent-blue';
8483
+ }
8484
+ border(kind) {
8485
+ const accent = kind === 'success' ? 'border-l-success' : kind === 'error' ? 'border-l-danger' : 'border-l-accent-blue';
8486
+ return `border-l-4 ${accent} border-y-border border-r-border`;
8487
+ }
8488
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsToastHost, deps: [], target: i0.ɵɵFactoryTarget.Component });
8489
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsToastHost, isStandalone: true, selector: "sds-toast-host", inputs: { closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
8490
+ <div class="pointer-events-none fixed inset-x-0 bottom-6 z-[100] flex flex-col items-center gap-2 px-4">
8491
+ <div role="alert" aria-live="assertive" class="flex w-full flex-col items-center gap-2">
8492
+ @for (t of errors(); track t.id) {
8493
+ <ng-container [ngTemplateOutlet]="toast" [ngTemplateOutletContext]="{ t }" />
8494
+ }
8495
+ </div>
8496
+ <div aria-live="polite" class="flex w-full flex-col items-center gap-2">
8497
+ @for (t of notices(); track t.id) {
8498
+ <ng-container [ngTemplateOutlet]="toast" [ngTemplateOutletContext]="{ t }" />
8499
+ }
8500
+ </div>
8501
+ </div>
8502
+
8503
+ <ng-template #toast let-t="t">
8504
+ <div
8505
+ class="pointer-events-auto flex max-w-md items-start gap-2.5 rounded-card border bg-surface px-4 py-3 text-sm shadow-lg"
8506
+ [class]="border(t.kind)"
8507
+ >
8508
+ <sds-icon [name]="icon(t.kind)" [size]="18" class="mt-0.5 shrink-0" [class]="tone(t.kind)" />
8509
+ <span class="text-ink">{{ t.text }}</span>
8510
+ <button
8511
+ type="button"
8512
+ (click)="toasts.dismiss(t.id)"
8513
+ [attr.aria-label]="closeLabel()"
8514
+ class="ml-1 shrink-0 rounded p-0.5 text-ink-faint hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
8515
+ >
8516
+ <sds-icon name="x" [size]="15" />
8517
+ </button>
8518
+ </div>
8519
+ </ng-template>
8520
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8521
+ }
8522
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsToastHost, decorators: [{
8523
+ type: Component,
8524
+ args: [{
8525
+ selector: 'sds-toast-host',
8526
+ changeDetection: ChangeDetectionStrategy.OnPush,
8527
+ imports: [NgTemplateOutlet, SdsIcon],
8528
+ template: `
8529
+ <div class="pointer-events-none fixed inset-x-0 bottom-6 z-[100] flex flex-col items-center gap-2 px-4">
8530
+ <div role="alert" aria-live="assertive" class="flex w-full flex-col items-center gap-2">
8531
+ @for (t of errors(); track t.id) {
8532
+ <ng-container [ngTemplateOutlet]="toast" [ngTemplateOutletContext]="{ t }" />
8533
+ }
8534
+ </div>
8535
+ <div aria-live="polite" class="flex w-full flex-col items-center gap-2">
8536
+ @for (t of notices(); track t.id) {
8537
+ <ng-container [ngTemplateOutlet]="toast" [ngTemplateOutletContext]="{ t }" />
8538
+ }
8539
+ </div>
8540
+ </div>
8541
+
8542
+ <ng-template #toast let-t="t">
8543
+ <div
8544
+ class="pointer-events-auto flex max-w-md items-start gap-2.5 rounded-card border bg-surface px-4 py-3 text-sm shadow-lg"
8545
+ [class]="border(t.kind)"
8546
+ >
8547
+ <sds-icon [name]="icon(t.kind)" [size]="18" class="mt-0.5 shrink-0" [class]="tone(t.kind)" />
8548
+ <span class="text-ink">{{ t.text }}</span>
8549
+ <button
8550
+ type="button"
8551
+ (click)="toasts.dismiss(t.id)"
8552
+ [attr.aria-label]="closeLabel()"
8553
+ class="ml-1 shrink-0 rounded p-0.5 text-ink-faint hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
8554
+ >
8555
+ <sds-icon name="x" [size]="15" />
8556
+ </button>
8557
+ </div>
8558
+ </ng-template>
8559
+ `,
8560
+ }]
8561
+ }], propDecorators: { closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }] } });
8562
+
7401
8563
  /**
7402
8564
  * Shared collapse state for the sidebar and its nav items.
7403
8565
  *
@@ -8213,5 +9375,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8213
9375
  * Generated bundle index. Do not edit.
8214
9376
  */
8215
9377
 
8216
- export { SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsSelect, SdsShell, SdsSidebar, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsToggleSwitch, SdsTopbar, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
9378
+ export { SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsInputNumber, SdsLocalizedInput, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsSelect, SdsShell, SdsSidebar, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
8217
9379
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map