codexly-ui 0.12.45 → 0.12.47

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.
@@ -3,7 +3,7 @@ import { input, computed, ChangeDetectionStrategy, ViewEncapsulation, Component,
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { isPlatformBrowser, NgTemplateOutlet, DOCUMENT, NgStyle, CurrencyPipe, DecimalPipe } from '@angular/common';
5
5
  import * as i1$1 from '@angular/forms';
6
- import { NG_VALUE_ACCESSOR, FormControl, NgControl, ReactiveFormsModule, FormGroup, Validators, FormsModule } from '@angular/forms';
6
+ import { NG_VALUE_ACCESSOR, FormControl, NgControl, TouchedChangeEvent, ReactiveFormsModule, FormGroup, Validators, FormsModule } from '@angular/forms';
7
7
  import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { startWith, map, debounceTime, distinctUntilChanged, switchMap, of, catchError, filter } from 'rxjs';
9
9
  import * as i1 from '@angular/cdk/overlay';
@@ -3484,6 +3484,443 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
3484
3484
  }]
3485
3485
  }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], prefixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixIcon", required: false }] }], suffixIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixIcon", required: false }] }], prefixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixText", required: false }] }], suffixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixText", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], statusMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusMessage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }] } });
3486
3486
 
3487
+ /** Luhn checksum + brand detection are pure functions, reusable outside the component (e.g. a
3488
+ * parent FormGroup wiring its own cross-field validator) — kept separate from the component
3489
+ * class itself, same reasoning as any other framework-agnostic utility in this library. */
3490
+ const ClxCardValidators = {
3491
+ luhn(digits) {
3492
+ if (!/^\d+$/.test(digits))
3493
+ return false;
3494
+ let sum = 0;
3495
+ let shouldDouble = false;
3496
+ for (let i = digits.length - 1; i >= 0; i--) {
3497
+ let d = Number(digits[i]);
3498
+ if (shouldDouble) {
3499
+ d *= 2;
3500
+ if (d > 9)
3501
+ d -= 9;
3502
+ }
3503
+ sum += d;
3504
+ shouldDouble = !shouldDouble;
3505
+ }
3506
+ return digits.length > 0 && sum % 10 === 0;
3507
+ },
3508
+ };
3509
+ /** Prefix rules ordered so the most specific range is checked before its broader superset would
3510
+ * otherwise misclassify it (none overlap here, but future additions should preserve this). */
3511
+ function detectClxCardBrand(digits) {
3512
+ if (/^4/.test(digits))
3513
+ return 'visa';
3514
+ if (/^5[1-5]/.test(digits) || /^2(2[2-9]|[3-6]\d|7[01]|720)/.test(digits))
3515
+ return 'mastercard';
3516
+ if (/^3[47]/.test(digits))
3517
+ return 'amex';
3518
+ return 'unknown';
3519
+ }
3520
+
3521
+ const CLX_CARD_BRAND_LABEL = {
3522
+ visa: 'VISA',
3523
+ mastercard: 'MASTERCARD',
3524
+ amex: 'AMEX',
3525
+ unknown: '',
3526
+ };
3527
+ let _clxCardNumberInputIdCounter = 0;
3528
+ /** Plain text input for a card's PAN — groups digits into 4-digit blocks as the customer types,
3529
+ * detects the brand (Visa/Mastercard/Amex) by BIN prefix purely to show its label (no network
3530
+ * call, no lookup), and exposes a Luhn-valid boolean so a consuming FormGroup can wire its own
3531
+ * validator without importing anything beyond ClxCardValidators. Knows nothing about any payment
3532
+ * provider — it is a generic card-number field, reusable by any app that ever needs one. */
3533
+ class ClxCardNumberInputComponent {
3534
+ _themeSvc = inject(ClxThemeService);
3535
+ // ── Inputs ───────────────────────────────────────────────────────────────
3536
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3537
+ placeholder = input('0000 0000 0000 0000', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
3538
+ color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3539
+ _color = computed(() => this.color() ?? this._themeSvc.formControlColor('cardNumber'), ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
3540
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
3541
+ status = input('default', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
3542
+ statusMessage = input('', ...(ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []));
3543
+ size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3544
+ _size = computed(() => (this.size() ?? this._themeSvc.config().defaultSize), ...(ngDevMode ? [{ debugName: "_size" }] : /* istanbul ignore next */ []));
3545
+ /** Initial value for standalone use (without FormControl) — raw digits, no spaces. */
3546
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
3547
+ /** Disabled state for standalone use (without FormControl) */
3548
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3549
+ /** Emits the detected brand every time it changes — a consuming form can show its own brand
3550
+ * logo elsewhere in the page without re-deriving the detection itself. */
3551
+ brandChange = output();
3552
+ // ── Internal state ───────────────────────────────────────────────────────
3553
+ _inputId = `clx-card-number-input-${++_clxCardNumberInputIdCounter}`;
3554
+ /** Raw digits only (no spaces) — this is the value handed to the FormControl / ControlValueAccessor. */
3555
+ _value = signal('', ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
3556
+ _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : /* istanbul ignore next */ []));
3557
+ _cvaConnected = false;
3558
+ _cardDisabled = inject(CLX_CARD_DISABLED_CONTEXT, { optional: true });
3559
+ _disabled = computed(() => this._cvaDisabled() || this.disabled() || !!this._cardDisabled?.(), ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
3560
+ _valueEffect = effect(() => {
3561
+ const v = this.value();
3562
+ if (!this._cvaConnected)
3563
+ untracked(() => this._value.set(v));
3564
+ }, ...(ngDevMode ? [{ debugName: "_valueEffect" }] : /* istanbul ignore next */ []));
3565
+ _brandEffect = effect(() => this.brandChange.emit(this._brand()), ...(ngDevMode ? [{ debugName: "_brandEffect" }] : /* istanbul ignore next */ []));
3566
+ _onChange = () => { };
3567
+ _onTouched = () => { };
3568
+ // ── Computed ─────────────────────────────────────────────────────────────
3569
+ _brand = computed(() => detectClxCardBrand(this._value()), ...(ngDevMode ? [{ debugName: "_brand" }] : /* istanbul ignore next */ []));
3570
+ _brandLabel = computed(() => CLX_CARD_BRAND_LABEL[this._brand()], ...(ngDevMode ? [{ debugName: "_brandLabel" }] : /* istanbul ignore next */ []));
3571
+ /** Digits grouped into 4-character blocks for display only — the underlying FormControl value
3572
+ * (see writeValue/_handleInput) never contains the spaces. */
3573
+ _displayValue = computed(() => this._value().replace(/(.{4})/g, '$1 ').trim(), ...(ngDevMode ? [{ debugName: "_displayValue" }] : /* istanbul ignore next */ []));
3574
+ _sizeConfig = computed(() => INPUT_SIZE_MAP[this._size()], ...(ngDevMode ? [{ debugName: "_sizeConfig" }] : /* istanbul ignore next */ []));
3575
+ _statusCfg = computed(() => INPUT_STATUS_MAP[this.status()], ...(ngDevMode ? [{ debugName: "_statusCfg" }] : /* istanbul ignore next */ []));
3576
+ _labelClass = computed(() => `${this._sizeConfig().label} ${CLX_TEXT_LABEL} leading-none`, ...(ngDevMode ? [{ debugName: "_labelClass" }] : /* istanbul ignore next */ []));
3577
+ _hintClass = computed(() => `${this._sizeConfig().hint} ${CLX_TEXT_HINT}`, ...(ngDevMode ? [{ debugName: "_hintClass" }] : /* istanbul ignore next */ []));
3578
+ _wrapperClass = computed(() => 'relative', ...(ngDevMode ? [{ debugName: "_wrapperClass" }] : /* istanbul ignore next */ []));
3579
+ _inputClass = computed(() => {
3580
+ const size = this._sizeConfig();
3581
+ const statusCfg = this._statusCfg();
3582
+ const pl = size.padDefaultL;
3583
+ const pr = size.padRight;
3584
+ const base = `w-full ${resolveRadius(this._themeSvc.config().borderRadius)} border ${size.input} ${pl} ${pr} outline-none transition-[border-color,box-shadow] duration-200 tracking-wider`;
3585
+ if (this._disabled()) {
3586
+ return `${base} ${CLX_BORDER_DISABLED} ${CLX_BG_DISABLED} cursor-not-allowed ${CLX_TEXT_DISABLED} ${CLX_PLACEHOLDER}`;
3587
+ }
3588
+ const ring = this.status() !== 'default' ? statusCfg.ring : resolveColor(this._color()).focusDirect;
3589
+ return `${base} ${CLX_BG_SURFACE} ${statusCfg.border} ${ring} ${CLX_TEXT_INPUT} ${CLX_PLACEHOLDER}`;
3590
+ }, ...(ngDevMode ? [{ debugName: "_inputClass" }] : /* istanbul ignore next */ []));
3591
+ _suffixIconWrapCls = computed(() => {
3592
+ const size = this._sizeConfig();
3593
+ return `absolute ${size.iconRight} top-1/2 -translate-y-1/2 pointer-events-none ${CLX_TEXT_IDLE} flex items-center`;
3594
+ }, ...(ngDevMode ? [{ debugName: "_suffixIconWrapCls" }] : /* istanbul ignore next */ []));
3595
+ _brandLabelClass = computed(() => {
3596
+ const size = this._sizeConfig();
3597
+ return `absolute ${size.iconRight} top-1/2 -translate-y-1/2 pointer-events-none ${CLX_TEXT_IDLE} text-xs font-semibold tracking-wide`;
3598
+ }, ...(ngDevMode ? [{ debugName: "_brandLabelClass" }] : /* istanbul ignore next */ []));
3599
+ _statusMsgClass = computed(() => `${this._sizeConfig().hint} ${this._statusCfg().msgCls}`, ...(ngDevMode ? [{ debugName: "_statusMsgClass" }] : /* istanbul ignore next */ []));
3600
+ // ── CVA ──────────────────────────────────────────────────────────────────
3601
+ writeValue(val) {
3602
+ this._cvaConnected = true;
3603
+ this._value.set(val ?? '');
3604
+ }
3605
+ registerOnChange(fn) {
3606
+ this._onChange = fn;
3607
+ }
3608
+ registerOnTouched(fn) {
3609
+ this._onTouched = fn;
3610
+ }
3611
+ setDisabledState(disabled) {
3612
+ this._cvaDisabled.set(disabled);
3613
+ }
3614
+ // ── Handlers ─────────────────────────────────────────────────────────────
3615
+ _handleInput(event) {
3616
+ const digits = event.target.value.replace(/\D/g, '').slice(0, 19);
3617
+ this._value.set(digits);
3618
+ this._onChange(digits);
3619
+ }
3620
+ _handleBlur() {
3621
+ this._onTouched();
3622
+ }
3623
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCardNumberInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3624
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxCardNumberInputComponent, isStandalone: true, selector: "clx-card-number-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, statusMessage: { classPropertyName: "statusMessage", publicName: "statusMessage", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { brandChange: "brandChange" }, host: { classAttribute: "flex flex-col gap-1.5" }, providers: [
3625
+ {
3626
+ provide: NG_VALUE_ACCESSOR,
3627
+ useExisting: forwardRef(() => ClxCardNumberInputComponent),
3628
+ multi: true,
3629
+ },
3630
+ ], ngImport: i0, template: `
3631
+ @if (label()) {
3632
+ <label [for]="_inputId" [class]="_labelClass()">{{ label() }}</label>
3633
+ }
3634
+
3635
+ <div [class]="_wrapperClass()">
3636
+ <input
3637
+ [id]="_inputId"
3638
+ type="text"
3639
+ inputmode="numeric"
3640
+ [placeholder]="placeholder()"
3641
+ [disabled]="_disabled()"
3642
+ [value]="_displayValue()"
3643
+ [class]="_inputClass()"
3644
+ (input)="_handleInput($event)"
3645
+ (blur)="_handleBlur()"
3646
+ />
3647
+
3648
+ @if (status() !== 'default') {
3649
+ <div [class]="_suffixIconWrapCls()" [style.color]="_statusCfg().iconColor">
3650
+ <span clx-icon [name]="_statusCfg().iconName" [size]="_sizeConfig().iconSize"></span>
3651
+ </div>
3652
+ } @else if (_brand() !== 'unknown') {
3653
+ <span [class]="_brandLabelClass()">{{ _brandLabel() }}</span>
3654
+ } @else {
3655
+ <div [class]="_suffixIconWrapCls()">
3656
+ <span clx-icon name="credit_card" [size]="_sizeConfig().iconSize"></span>
3657
+ </div>
3658
+ }
3659
+ </div>
3660
+
3661
+ @if (statusMessage() && status() !== 'default') {
3662
+ <p [class]="_statusMsgClass()">{{ statusMessage() }}</p>
3663
+ } @else if (hint()) {
3664
+ <p [class]="_hintClass()">{{ hint() }}</p>
3665
+ }
3666
+ `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3667
+ }
3668
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCardNumberInputComponent, decorators: [{
3669
+ type: Component,
3670
+ args: [{
3671
+ selector: 'clx-card-number-input',
3672
+ standalone: true,
3673
+ imports: [ClxIconComponent],
3674
+ providers: [
3675
+ {
3676
+ provide: NG_VALUE_ACCESSOR,
3677
+ useExisting: forwardRef(() => ClxCardNumberInputComponent),
3678
+ multi: true,
3679
+ },
3680
+ ],
3681
+ template: `
3682
+ @if (label()) {
3683
+ <label [for]="_inputId" [class]="_labelClass()">{{ label() }}</label>
3684
+ }
3685
+
3686
+ <div [class]="_wrapperClass()">
3687
+ <input
3688
+ [id]="_inputId"
3689
+ type="text"
3690
+ inputmode="numeric"
3691
+ [placeholder]="placeholder()"
3692
+ [disabled]="_disabled()"
3693
+ [value]="_displayValue()"
3694
+ [class]="_inputClass()"
3695
+ (input)="_handleInput($event)"
3696
+ (blur)="_handleBlur()"
3697
+ />
3698
+
3699
+ @if (status() !== 'default') {
3700
+ <div [class]="_suffixIconWrapCls()" [style.color]="_statusCfg().iconColor">
3701
+ <span clx-icon [name]="_statusCfg().iconName" [size]="_sizeConfig().iconSize"></span>
3702
+ </div>
3703
+ } @else if (_brand() !== 'unknown') {
3704
+ <span [class]="_brandLabelClass()">{{ _brandLabel() }}</span>
3705
+ } @else {
3706
+ <div [class]="_suffixIconWrapCls()">
3707
+ <span clx-icon name="credit_card" [size]="_sizeConfig().iconSize"></span>
3708
+ </div>
3709
+ }
3710
+ </div>
3711
+
3712
+ @if (statusMessage() && status() !== 'default') {
3713
+ <p [class]="_statusMsgClass()">{{ statusMessage() }}</p>
3714
+ } @else if (hint()) {
3715
+ <p [class]="_hintClass()">{{ hint() }}</p>
3716
+ }
3717
+ `,
3718
+ encapsulation: ViewEncapsulation.None,
3719
+ changeDetection: ChangeDetectionStrategy.OnPush,
3720
+ host: {
3721
+ class: 'flex flex-col gap-1.5',
3722
+ },
3723
+ }]
3724
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], statusMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusMessage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], brandChange: [{ type: i0.Output, args: ["brandChange"] }] } });
3725
+
3726
+ let _clxCardExpiryInputIdCounter = 0;
3727
+ /** Plain text input for a card's expiry date in MM/YY form — auto-inserts the "/" as the customer
3728
+ * types and exposes the raw "MMYY" digits (no slash) as the FormControl value, same convention
3729
+ * Wompi's own tokenization endpoint expects for exp_month/exp_year split apart. Knows nothing
3730
+ * about any payment provider — a generic expiry field, reusable by any app that ever needs one. */
3731
+ class ClxCardExpiryInputComponent {
3732
+ _themeSvc = inject(ClxThemeService);
3733
+ // ── Inputs ───────────────────────────────────────────────────────────────
3734
+ label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
3735
+ placeholder = input('MM/AA', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
3736
+ color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3737
+ _color = computed(() => this.color() ?? this._themeSvc.formControlColor('cardExpiry'), ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
3738
+ hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
3739
+ status = input('default', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
3740
+ statusMessage = input('', ...(ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []));
3741
+ size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3742
+ _size = computed(() => (this.size() ?? this._themeSvc.config().defaultSize), ...(ngDevMode ? [{ debugName: "_size" }] : /* istanbul ignore next */ []));
3743
+ /** Initial value for standalone use (without FormControl) — raw "MMYY" digits, no slash. */
3744
+ value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
3745
+ /** Disabled state for standalone use (without FormControl) */
3746
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
3747
+ // ── Internal state ───────────────────────────────────────────────────────
3748
+ _inputId = `clx-card-expiry-input-${++_clxCardExpiryInputIdCounter}`;
3749
+ /** Raw "MMYY" digits (max 4, no slash) — this is the value handed to the FormControl. */
3750
+ _value = signal('', ...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
3751
+ _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : /* istanbul ignore next */ []));
3752
+ _cvaConnected = false;
3753
+ _cardDisabled = inject(CLX_CARD_DISABLED_CONTEXT, { optional: true });
3754
+ _disabled = computed(() => this._cvaDisabled() || this.disabled() || !!this._cardDisabled?.(), ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
3755
+ _valueEffect = effect(() => {
3756
+ const v = this.value();
3757
+ if (!this._cvaConnected)
3758
+ untracked(() => this._value.set(v));
3759
+ }, ...(ngDevMode ? [{ debugName: "_valueEffect" }] : /* istanbul ignore next */ []));
3760
+ _onChange = () => { };
3761
+ _onTouched = () => { };
3762
+ // ── Computed ─────────────────────────────────────────────────────────────
3763
+ /** "MMYY" -> "MM/YY" for display only — the underlying FormControl value never has the slash. */
3764
+ _displayValue = computed(() => {
3765
+ const v = this._value();
3766
+ return v.length > 2 ? `${v.slice(0, 2)}/${v.slice(2)}` : v;
3767
+ }, ...(ngDevMode ? [{ debugName: "_displayValue" }] : /* istanbul ignore next */ []));
3768
+ _sizeConfig = computed(() => INPUT_SIZE_MAP[this._size()], ...(ngDevMode ? [{ debugName: "_sizeConfig" }] : /* istanbul ignore next */ []));
3769
+ _statusCfg = computed(() => INPUT_STATUS_MAP[this.status()], ...(ngDevMode ? [{ debugName: "_statusCfg" }] : /* istanbul ignore next */ []));
3770
+ _labelClass = computed(() => `${this._sizeConfig().label} ${CLX_TEXT_LABEL} leading-none`, ...(ngDevMode ? [{ debugName: "_labelClass" }] : /* istanbul ignore next */ []));
3771
+ _hintClass = computed(() => `${this._sizeConfig().hint} ${CLX_TEXT_HINT}`, ...(ngDevMode ? [{ debugName: "_hintClass" }] : /* istanbul ignore next */ []));
3772
+ _wrapperClass = computed(() => 'relative', ...(ngDevMode ? [{ debugName: "_wrapperClass" }] : /* istanbul ignore next */ []));
3773
+ _inputClass = computed(() => {
3774
+ const size = this._sizeConfig();
3775
+ const statusCfg = this._statusCfg();
3776
+ const pl = size.padDefaultL;
3777
+ const hasSuffix = this.status() !== 'default';
3778
+ const pr = hasSuffix ? size.padRight : size.padDefaultR;
3779
+ const base = `w-full ${resolveRadius(this._themeSvc.config().borderRadius)} border ${size.input} ${pl} ${pr} outline-none transition-[border-color,box-shadow] duration-200`;
3780
+ if (this._disabled()) {
3781
+ return `${base} ${CLX_BORDER_DISABLED} ${CLX_BG_DISABLED} cursor-not-allowed ${CLX_TEXT_DISABLED} ${CLX_PLACEHOLDER}`;
3782
+ }
3783
+ const ring = this.status() !== 'default' ? statusCfg.ring : resolveColor(this._color()).focusDirect;
3784
+ return `${base} ${CLX_BG_SURFACE} ${statusCfg.border} ${ring} ${CLX_TEXT_INPUT} ${CLX_PLACEHOLDER}`;
3785
+ }, ...(ngDevMode ? [{ debugName: "_inputClass" }] : /* istanbul ignore next */ []));
3786
+ _suffixIconWrapCls = computed(() => {
3787
+ const size = this._sizeConfig();
3788
+ return `absolute ${size.iconRight} top-1/2 -translate-y-1/2 pointer-events-none flex items-center`;
3789
+ }, ...(ngDevMode ? [{ debugName: "_suffixIconWrapCls" }] : /* istanbul ignore next */ []));
3790
+ _statusMsgClass = computed(() => `${this._sizeConfig().hint} ${this._statusCfg().msgCls}`, ...(ngDevMode ? [{ debugName: "_statusMsgClass" }] : /* istanbul ignore next */ []));
3791
+ // ── CVA ──────────────────────────────────────────────────────────────────
3792
+ writeValue(val) {
3793
+ this._cvaConnected = true;
3794
+ this._value.set(val ?? '');
3795
+ }
3796
+ registerOnChange(fn) {
3797
+ this._onChange = fn;
3798
+ }
3799
+ registerOnTouched(fn) {
3800
+ this._onTouched = fn;
3801
+ }
3802
+ setDisabledState(disabled) {
3803
+ this._cvaDisabled.set(disabled);
3804
+ }
3805
+ // ── Handlers ─────────────────────────────────────────────────────────────
3806
+ _handleInput(event) {
3807
+ const digits = event.target.value.replace(/\D/g, '').slice(0, 4);
3808
+ this._value.set(digits);
3809
+ this._onChange(digits);
3810
+ }
3811
+ _handleBlur() {
3812
+ this._onTouched();
3813
+ }
3814
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCardExpiryInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3815
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxCardExpiryInputComponent, isStandalone: true, selector: "clx-card-expiry-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, statusMessage: { classPropertyName: "statusMessage", publicName: "statusMessage", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex flex-col gap-1.5" }, providers: [
3816
+ {
3817
+ provide: NG_VALUE_ACCESSOR,
3818
+ useExisting: forwardRef(() => ClxCardExpiryInputComponent),
3819
+ multi: true,
3820
+ },
3821
+ ], ngImport: i0, template: `
3822
+ @if (label()) {
3823
+ <label [for]="_inputId" [class]="_labelClass()">{{ label() }}</label>
3824
+ }
3825
+
3826
+ <div [class]="_wrapperClass()">
3827
+ <input
3828
+ [id]="_inputId"
3829
+ type="text"
3830
+ inputmode="numeric"
3831
+ autocomplete="cc-exp"
3832
+ [placeholder]="placeholder()"
3833
+ [disabled]="_disabled()"
3834
+ [value]="_displayValue()"
3835
+ [class]="_inputClass()"
3836
+ (input)="_handleInput($event)"
3837
+ (blur)="_handleBlur()"
3838
+ />
3839
+
3840
+ @if (status() !== 'default') {
3841
+ <div [class]="_suffixIconWrapCls()" [style.color]="_statusCfg().iconColor">
3842
+ <span clx-icon [name]="_statusCfg().iconName" [size]="_sizeConfig().iconSize"></span>
3843
+ </div>
3844
+ }
3845
+ </div>
3846
+
3847
+ @if (statusMessage() && status() !== 'default') {
3848
+ <p [class]="_statusMsgClass()">{{ statusMessage() }}</p>
3849
+ } @else if (hint()) {
3850
+ <p [class]="_hintClass()">{{ hint() }}</p>
3851
+ }
3852
+ `, isInline: true, dependencies: [{ kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3853
+ }
3854
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCardExpiryInputComponent, decorators: [{
3855
+ type: Component,
3856
+ args: [{
3857
+ selector: 'clx-card-expiry-input',
3858
+ standalone: true,
3859
+ imports: [ClxIconComponent],
3860
+ providers: [
3861
+ {
3862
+ provide: NG_VALUE_ACCESSOR,
3863
+ useExisting: forwardRef(() => ClxCardExpiryInputComponent),
3864
+ multi: true,
3865
+ },
3866
+ ],
3867
+ template: `
3868
+ @if (label()) {
3869
+ <label [for]="_inputId" [class]="_labelClass()">{{ label() }}</label>
3870
+ }
3871
+
3872
+ <div [class]="_wrapperClass()">
3873
+ <input
3874
+ [id]="_inputId"
3875
+ type="text"
3876
+ inputmode="numeric"
3877
+ autocomplete="cc-exp"
3878
+ [placeholder]="placeholder()"
3879
+ [disabled]="_disabled()"
3880
+ [value]="_displayValue()"
3881
+ [class]="_inputClass()"
3882
+ (input)="_handleInput($event)"
3883
+ (blur)="_handleBlur()"
3884
+ />
3885
+
3886
+ @if (status() !== 'default') {
3887
+ <div [class]="_suffixIconWrapCls()" [style.color]="_statusCfg().iconColor">
3888
+ <span clx-icon [name]="_statusCfg().iconName" [size]="_sizeConfig().iconSize"></span>
3889
+ </div>
3890
+ }
3891
+ </div>
3892
+
3893
+ @if (statusMessage() && status() !== 'default') {
3894
+ <p [class]="_statusMsgClass()">{{ statusMessage() }}</p>
3895
+ } @else if (hint()) {
3896
+ <p [class]="_hintClass()">{{ hint() }}</p>
3897
+ }
3898
+ `,
3899
+ encapsulation: ViewEncapsulation.None,
3900
+ changeDetection: ChangeDetectionStrategy.OnPush,
3901
+ host: {
3902
+ class: 'flex flex-col gap-1.5',
3903
+ },
3904
+ }]
3905
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], statusMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusMessage", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
3906
+
3907
+ /** True when MM/YY (2-digit year) refers to a month that has already fully elapsed relative to
3908
+ * the system's current date — the expiry month itself is still valid through its last day. */
3909
+ function isClxCardExpired(month, twoDigitYear) {
3910
+ const m = Number(month);
3911
+ const y = Number(twoDigitYear);
3912
+ if (!Number.isInteger(m) || m < 1 || m > 12 || !Number.isInteger(y))
3913
+ return true;
3914
+ const now = new Date();
3915
+ const currentYear = now.getFullYear() % 100;
3916
+ const currentMonth = now.getMonth() + 1;
3917
+ if (y < currentYear)
3918
+ return true;
3919
+ if (y === currentYear && m < currentMonth)
3920
+ return true;
3921
+ return false;
3922
+ }
3923
+
3487
3924
  const OTP_SIZE_MAP = {
3488
3925
  xs: { box: 'w-8 h-8 text-sm', text: 'text-sm', gap: 'gap-1.5' },
3489
3926
  sm: { box: 'w-10 h-10 text-base', text: 'text-base', gap: 'gap-2' },
@@ -4421,6 +4858,11 @@ class ClxSelectComponent {
4421
4858
  _asyncLoading = signal(false, ...(ngDevMode ? [{ debugName: "_asyncLoading" }] : /* istanbul ignore next */ []));
4422
4859
  _asyncQuery = signal('', ...(ngDevMode ? [{ debugName: "_asyncQuery" }] : /* istanbul ignore next */ []));
4423
4860
  _wasTouched = signal(false, ...(ngDevMode ? [{ debugName: "_wasTouched" }] : /* istanbul ignore next */ []));
4861
+ // Mirrors this._ngControl.control.touched reactively — that flag is plain mutable state on the
4862
+ // AbstractControl, not a signal, so a form.markAllAsTouched() call (e.g. from a failed submit)
4863
+ // would never be noticed by a computed() reading it directly. control.events emits a
4864
+ // TouchedChangeEvent for exactly that call, which is what actually drives this.
4865
+ _controlTouched = signal(false, ...(ngDevMode ? [{ debugName: "_controlTouched" }] : /* istanbul ignore next */ []));
4424
4866
  _selectedVals = signal([], ...(ngDevMode ? [{ debugName: "_selectedVals" }] : /* istanbul ignore next */ []));
4425
4867
  _cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "_cvaDisabled" }] : /* istanbul ignore next */ []));
4426
4868
  _cardDisabled = inject(CLX_CARD_DISABLED_CONTEXT, { optional: true });
@@ -4437,6 +4879,18 @@ class ClxSelectComponent {
4437
4879
  this._searchQuery = toSignal(this._searchControl.valueChanges.pipe(startWith(''), map(v => v ?? '')), { initialValue: '' });
4438
4880
  if (this._ngControl)
4439
4881
  this._ngControl.valueAccessor = this;
4882
+ // See _controlTouched's own doc comment — control.touched itself isn't observable, but
4883
+ // control.events (Angular 18+) emits a TouchedChangeEvent whenever markAsTouched/
4884
+ // markAllAsTouched runs, which is exactly the case (a failed form submit) that needs to make
4885
+ // this field's error visible even though the user never opened this dropdown themselves.
4886
+ const control = this._ngControl?.control;
4887
+ if (control) {
4888
+ this._controlTouched.set(control.touched);
4889
+ control.events.subscribe(event => {
4890
+ if (event instanceof TouchedChangeEvent)
4891
+ this._controlTouched.set(event.touched);
4892
+ });
4893
+ }
4440
4894
  effect(() => {
4441
4895
  const family = this._displayFontFamily();
4442
4896
  if (family)
@@ -4472,7 +4926,12 @@ class ClxSelectComponent {
4472
4926
  _hasValue = computed(() => this._selectedVals().length > 0, ...(ngDevMode ? [{ debugName: "_hasValue" }] : /* istanbul ignore next */ []));
4473
4927
  _isInvalid = computed(() => {
4474
4928
  this._selectedVals(); // re-evaluate control.invalid (imperatively updated by Angular Forms) on each selection change
4475
- return this._wasTouched() && (this._ngControl?.control?.invalid ?? false);
4929
+ // Either signal is enough: the user actually opening/closing this dropdown (_wasTouched), or
4930
+ // Angular Forms marking the control touched programmatically (_controlTouched — set by e.g.
4931
+ // form.markAllAsTouched() on a failed submit, which never fires this component's own
4932
+ // _closePanel(), so _wasTouched alone would stay false forever for a field the user never
4933
+ // interacted with before trying to submit).
4934
+ return (this._wasTouched() || this._controlTouched()) && (this._ngControl?.control?.invalid ?? false);
4476
4935
  }, ...(ngDevMode ? [{ debugName: "_isInvalid" }] : /* istanbul ignore next */ []));
4477
4936
  _selectedOptions = computed(() => {
4478
4937
  const vals = this._selectedVals();
@@ -18218,5 +18677,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
18218
18677
  * Generated bundle index. Do not edit.
18219
18678
  */
18220
18679
 
18221
- export { CLX_ADDON_BORDER, CLX_ADDON_TEXT, CLX_ALERT_OPTIONS, CLX_ALERT_RESOLVE, CLX_BG_ADDON, CLX_BG_DISABLED, CLX_BG_ICON_WRAP, CLX_BG_SECTION, CLX_BG_SURFACE, CLX_BORDER_DEFAULT, CLX_BORDER_DISABLED, CLX_BORDER_MEDIUM, CLX_COLOR_HEX, CLX_COLOR_HEX_100, CLX_COLOR_MAP, CLX_FONT_CATALOG, CLX_MODAL_ANIM_CONFIG, CLX_MODAL_DATA, CLX_MODAL_REF, CLX_OPTION_DISABLED, CLX_PLACEHOLDER, CLX_RADIO_GROUP, CLX_RADIUS_MAP, CLX_TEXT_BODY, CLX_TEXT_DISABLED, CLX_TEXT_HEADING, CLX_TEXT_HINT, CLX_TEXT_IDLE, CLX_TEXT_INPUT, CLX_TEXT_LABEL, CLX_TEXT_OPTION, CLX_TEXT_SUBTITLE, CLX_TEXT_TITLE, CLX_THEME_CONFIG, CLX_THEME_DEFAULTS, CLX_TOAST_DEFAULTS, ClxAlertComponent, ClxAlertService, ClxAnimateDirective, ClxAnimateGroupDirective, ClxAnimateService, ClxAppLayoutComponent, ClxAvatarComponent, ClxBadgeComponent, ClxBrandComponent, ClxButtonComponent, ClxButtonGroupComponent, ClxCardBodyDirective, ClxCardComponent, ClxCardFooterDirective, ClxCardHeaderActionsDirective, ClxCardHeaderDirective, ClxCarouselComponent, ClxCarouselDirective, ClxCartComponent, ClxCartSummaryDrawer, ClxCellDirective, ClxCheckboxComponent, ClxCollapseComponent, ClxColorPickerComponent, ClxColumnDefDirective, ClxDateRangePickerComponent, ClxDatepickerComponent, ClxDrawerComponent, ClxDrawerService, ClxEditorComponent, ClxEditorImageModalComponent, ClxEditorLinkModalComponent, ClxFabComponent, ClxFilterPanelComponent, ClxHeaderCellDirective, ClxIconComponent, ClxInputComponent, ClxListComponent, ClxListItemComponent, ClxMenuComponent, ClxMenuItemComponent, ClxMenuItemTrailingDirective, ClxModalComponent, ClxModalService, ClxNativeOverlayService, ClxNavGroupComponent, ClxNotificationComponent, ClxNumberComponent, ClxOtpComponent, ClxPageEmptyComponent, ClxPageHeaderComponent, ClxPageHeaderTitleDirective, ClxPageNotFoundComponent, ClxPageServerErrorComponent, ClxPageUnauthorizedComponent, ClxPaginationComponent, ClxProductComponent, ClxProductDetailComponent, ClxProductQuickViewComponent, ClxProfileComponent, ClxProgressBarComponent, ClxRadioComponent, ClxRadioGroupComponent, ClxRatingComponent, ClxSearchComponent, ClxSelectComponent, ClxSkeletonComponent, ClxSliderComponent, ClxSocialIconComponent, ClxSpinnerComponent, ClxStatCardComponent, ClxStepComponent, ClxStepperComponent, ClxSwitchComponent, ClxTabDirective, ClxTableActionsComponent, ClxTableComponent, ClxTabsComponent, ClxTagComponent, ClxTextareaComponent, ClxThemeService, ClxTimelineComponent, ClxTimelineItemComponent, ClxTimepickerComponent, ClxToastComponent, ClxToastContainerComponent, ClxToastService, ClxTooltipComponent, ClxTooltipDirective, ClxTreeComponent, ClxUploadComponent, ClxWishlistComponent, ClxWizardComponent, TIMEPICKER_SIZE_MAP, parseColorInput, provideCodexlyTheme, resolveColor, resolveContainerRadius, resolveRadius };
18680
+ export { CLX_ADDON_BORDER, CLX_ADDON_TEXT, CLX_ALERT_OPTIONS, CLX_ALERT_RESOLVE, CLX_BG_ADDON, CLX_BG_DISABLED, CLX_BG_ICON_WRAP, CLX_BG_SECTION, CLX_BG_SURFACE, CLX_BORDER_DEFAULT, CLX_BORDER_DISABLED, CLX_BORDER_MEDIUM, CLX_COLOR_HEX, CLX_COLOR_HEX_100, CLX_COLOR_MAP, CLX_FONT_CATALOG, CLX_MODAL_ANIM_CONFIG, CLX_MODAL_DATA, CLX_MODAL_REF, CLX_OPTION_DISABLED, CLX_PLACEHOLDER, CLX_RADIO_GROUP, CLX_RADIUS_MAP, CLX_TEXT_BODY, CLX_TEXT_DISABLED, CLX_TEXT_HEADING, CLX_TEXT_HINT, CLX_TEXT_IDLE, CLX_TEXT_INPUT, CLX_TEXT_LABEL, CLX_TEXT_OPTION, CLX_TEXT_SUBTITLE, CLX_TEXT_TITLE, CLX_THEME_CONFIG, CLX_THEME_DEFAULTS, CLX_TOAST_DEFAULTS, ClxAlertComponent, ClxAlertService, ClxAnimateDirective, ClxAnimateGroupDirective, ClxAnimateService, ClxAppLayoutComponent, ClxAvatarComponent, ClxBadgeComponent, ClxBrandComponent, ClxButtonComponent, ClxButtonGroupComponent, ClxCardBodyDirective, ClxCardComponent, ClxCardExpiryInputComponent, ClxCardFooterDirective, ClxCardHeaderActionsDirective, ClxCardHeaderDirective, ClxCardNumberInputComponent, ClxCardValidators, ClxCarouselComponent, ClxCarouselDirective, ClxCartComponent, ClxCartSummaryDrawer, ClxCellDirective, ClxCheckboxComponent, ClxCollapseComponent, ClxColorPickerComponent, ClxColumnDefDirective, ClxDateRangePickerComponent, ClxDatepickerComponent, ClxDrawerComponent, ClxDrawerService, ClxEditorComponent, ClxEditorImageModalComponent, ClxEditorLinkModalComponent, ClxFabComponent, ClxFilterPanelComponent, ClxHeaderCellDirective, ClxIconComponent, ClxInputComponent, ClxListComponent, ClxListItemComponent, ClxMenuComponent, ClxMenuItemComponent, ClxMenuItemTrailingDirective, ClxModalComponent, ClxModalService, ClxNativeOverlayService, ClxNavGroupComponent, ClxNotificationComponent, ClxNumberComponent, ClxOtpComponent, ClxPageEmptyComponent, ClxPageHeaderComponent, ClxPageHeaderTitleDirective, ClxPageNotFoundComponent, ClxPageServerErrorComponent, ClxPageUnauthorizedComponent, ClxPaginationComponent, ClxProductComponent, ClxProductDetailComponent, ClxProductQuickViewComponent, ClxProfileComponent, ClxProgressBarComponent, ClxRadioComponent, ClxRadioGroupComponent, ClxRatingComponent, ClxSearchComponent, ClxSelectComponent, ClxSkeletonComponent, ClxSliderComponent, ClxSocialIconComponent, ClxSpinnerComponent, ClxStatCardComponent, ClxStepComponent, ClxStepperComponent, ClxSwitchComponent, ClxTabDirective, ClxTableActionsComponent, ClxTableComponent, ClxTabsComponent, ClxTagComponent, ClxTextareaComponent, ClxThemeService, ClxTimelineComponent, ClxTimelineItemComponent, ClxTimepickerComponent, ClxToastComponent, ClxToastContainerComponent, ClxToastService, ClxTooltipComponent, ClxTooltipDirective, ClxTreeComponent, ClxUploadComponent, ClxWishlistComponent, ClxWizardComponent, TIMEPICKER_SIZE_MAP, detectClxCardBrand, isClxCardExpired, parseColorInput, provideCodexlyTheme, resolveColor, resolveContainerRadius, resolveRadius };
18222
18681
  //# sourceMappingURL=codexly-ui.mjs.map