codexly-ui 0.19.6 → 0.19.8
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.
- package/fesm2022/codexly-ui.mjs +177 -145
- package/fesm2022/codexly-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/codexly-ui.d.ts +18 -4
package/fesm2022/codexly-ui.mjs
CHANGED
|
@@ -1822,7 +1822,9 @@ class ClxCollapseComponent {
|
|
|
1822
1822
|
dividerThickness = input(2, ...(ngDevMode ? [{ debugName: "dividerThickness" }] : /* istanbul ignore next */ []));
|
|
1823
1823
|
/** Two-way bindable: [(expanded)]="..." */
|
|
1824
1824
|
expanded = model(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
1825
|
-
|
|
1825
|
+
/** Explicit [color] → theme collapseColor → primaryColor (same fallback chain as clx-input's
|
|
1826
|
+
* formControls.input). */
|
|
1827
|
+
_color = computed(() => this.color() ?? this._themeSvc.config().collapseColor ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "_color" }] : /* istanbul ignore next */ []));
|
|
1826
1828
|
_size = computed(() => (this.size() ?? this._themeSvc.config().defaultSize), ...(ngDevMode ? [{ debugName: "_size" }] : /* istanbul ignore next */ []));
|
|
1827
1829
|
_sizeConfig = computed(() => COLLAPSE_SIZE_MAP[this._size()], ...(ngDevMode ? [{ debugName: "_sizeConfig" }] : /* istanbul ignore next */ []));
|
|
1828
1830
|
_iconCls = computed(() => {
|
|
@@ -11852,6 +11854,8 @@ class ClxProfileComponent {
|
|
|
11852
11854
|
color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
11853
11855
|
menuItems = input([], ...(ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []));
|
|
11854
11856
|
signOutLabel = input('Cerrar sesión', ...(ngDevMode ? [{ debugName: "signOutLabel" }] : /* istanbul ignore next */ []));
|
|
11857
|
+
/** Small line under the sign-out item — e.g. the app version. Hidden when empty. */
|
|
11858
|
+
footerText = input('', ...(ngDevMode ? [{ debugName: "footerText" }] : /* istanbul ignore next */ []));
|
|
11855
11859
|
// ── Outputs ─────────────────────────────────────────────────────────────────
|
|
11856
11860
|
menuAction = output();
|
|
11857
11861
|
signOut = output();
|
|
@@ -11901,7 +11905,7 @@ class ClxProfileComponent {
|
|
|
11901
11905
|
this._close();
|
|
11902
11906
|
}
|
|
11903
11907
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProfileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11904
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxProfileComponent, isStandalone: true, selector: "clx-profile", inputs: { firstName: { classPropertyName: "firstName", publicName: "firstName", isSignal: true, isRequired: false, transformFunction: null }, lastName: { classPropertyName: "lastName", publicName: "lastName", isSignal: true, isRequired: false, transformFunction: null }, username: { classPropertyName: "username", publicName: "username", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, avatarSrc: { classPropertyName: "avatarSrc", publicName: "avatarSrc", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, menuItems: { classPropertyName: "menuItems", publicName: "menuItems", isSignal: true, isRequired: false, transformFunction: null }, signOutLabel: { classPropertyName: "signOutLabel", publicName: "signOutLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuAction: "menuAction", signOut: "signOut" }, host: { classAttribute: "relative inline-flex items-center" }, ngImport: i0, template: `
|
|
11908
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxProfileComponent, isStandalone: true, selector: "clx-profile", inputs: { firstName: { classPropertyName: "firstName", publicName: "firstName", isSignal: true, isRequired: false, transformFunction: null }, lastName: { classPropertyName: "lastName", publicName: "lastName", isSignal: true, isRequired: false, transformFunction: null }, username: { classPropertyName: "username", publicName: "username", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null }, avatarSrc: { classPropertyName: "avatarSrc", publicName: "avatarSrc", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, menuItems: { classPropertyName: "menuItems", publicName: "menuItems", isSignal: true, isRequired: false, transformFunction: null }, signOutLabel: { classPropertyName: "signOutLabel", publicName: "signOutLabel", isSignal: true, isRequired: false, transformFunction: null }, footerText: { classPropertyName: "footerText", publicName: "footerText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuAction: "menuAction", signOut: "signOut" }, host: { classAttribute: "relative inline-flex items-center" }, ngImport: i0, template: `
|
|
11905
11909
|
<!-- ── Trigger ─────────────────────────────────────────────────────────── -->
|
|
11906
11910
|
<button
|
|
11907
11911
|
#origin="cdkOverlayOrigin"
|
|
@@ -11968,6 +11972,13 @@ class ClxProfileComponent {
|
|
|
11968
11972
|
(itemClick)="_onSignOut()" />
|
|
11969
11973
|
</div>
|
|
11970
11974
|
|
|
11975
|
+
<!-- App version / build — small, below sign out -->
|
|
11976
|
+
@if (footerText()) {
|
|
11977
|
+
<div class="border-t border-clx-border-soft px-4 py-2 text-center text-[11px] text-clx-text-subtle select-text">
|
|
11978
|
+
{{ footerText() }}
|
|
11979
|
+
</div>
|
|
11980
|
+
}
|
|
11981
|
+
|
|
11971
11982
|
</div>
|
|
11972
11983
|
</ng-template>
|
|
11973
11984
|
`, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: ClxAvatarComponent, selector: "clx-avatar", inputs: ["src", "initials", "iconName", "color", "size", "shape", "alt"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned"] }, { kind: "component", type: ClxMenuItemComponent, selector: "clx-menu-item", inputs: ["label", "icon", "active", "style", "nested", "collapsed", "routerLink", "routerLinkActiveOptions"], outputs: ["itemClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
@@ -12045,6 +12056,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
12045
12056
|
(itemClick)="_onSignOut()" />
|
|
12046
12057
|
</div>
|
|
12047
12058
|
|
|
12059
|
+
<!-- App version / build — small, below sign out -->
|
|
12060
|
+
@if (footerText()) {
|
|
12061
|
+
<div class="border-t border-clx-border-soft px-4 py-2 text-center text-[11px] text-clx-text-subtle select-text">
|
|
12062
|
+
{{ footerText() }}
|
|
12063
|
+
</div>
|
|
12064
|
+
}
|
|
12065
|
+
|
|
12048
12066
|
</div>
|
|
12049
12067
|
</ng-template>
|
|
12050
12068
|
`,
|
|
@@ -12052,7 +12070,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
12052
12070
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
12053
12071
|
host: { class: 'relative inline-flex items-center' },
|
|
12054
12072
|
}]
|
|
12055
|
-
}], propDecorators: { firstName: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstName", required: false }] }], lastName: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastName", required: false }] }], username: [{ type: i0.Input, args: [{ isSignal: true, alias: "username", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], avatarSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarSrc", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], menuItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuItems", required: false }] }], signOutLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "signOutLabel", required: false }] }], menuAction: [{ type: i0.Output, args: ["menuAction"] }], signOut: [{ type: i0.Output, args: ["signOut"] }] } });
|
|
12073
|
+
}], propDecorators: { firstName: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstName", required: false }] }], lastName: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastName", required: false }] }], username: [{ type: i0.Input, args: [{ isSignal: true, alias: "username", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], avatarSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarSrc", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], menuItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuItems", required: false }] }], signOutLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "signOutLabel", required: false }] }], footerText: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerText", required: false }] }], menuAction: [{ type: i0.Output, args: ["menuAction"] }], signOut: [{ type: i0.Output, args: ["signOut"] }] } });
|
|
12056
12074
|
|
|
12057
12075
|
const DRAWER_SIZE_MAP = {
|
|
12058
12076
|
sm: 'w-[320px]',
|
|
@@ -14624,6 +14642,14 @@ class ClxCarouselComponent {
|
|
|
14624
14642
|
loop = input(true, ...(ngDevMode ? [{ debugName: "loop" }] : /* istanbul ignore next */ []));
|
|
14625
14643
|
aspectRatio = input('16/9', ...(ngDevMode ? [{ debugName: "aspectRatio" }] : /* istanbul ignore next */ []));
|
|
14626
14644
|
transparent = input(false, ...(ngDevMode ? [{ debugName: "transparent" }] : /* istanbul ignore next */ []));
|
|
14645
|
+
/** false = square corners (e.g. a full-bleed page hero). */
|
|
14646
|
+
rounded = input(true, ...(ngDevMode ? [{ debugName: "rounded" }] : /* istanbul ignore next */ []));
|
|
14647
|
+
/** Autoplay waits while the pointer is over the carousel (and always while the tab is hidden). */
|
|
14648
|
+
pauseOnHover = input(true, ...(ngDevMode ? [{ debugName: "pauseOnHover" }] : /* istanbul ignore next */ []));
|
|
14649
|
+
/** 'desktop' hides the arrows below md — touch users swipe. */
|
|
14650
|
+
arrows = input('always', ...(ngDevMode ? [{ debugName: "arrows" }] : /* istanbul ignore next */ []));
|
|
14651
|
+
_hovering = signal(false, ...(ngDevMode ? [{ debugName: "_hovering" }] : /* istanbul ignore next */ []));
|
|
14652
|
+
_arrowVisibility = computed(() => (this.arrows() === 'desktop' ? 'hidden md:block' : ''), ...(ngDevMode ? [{ debugName: "_arrowVisibility" }] : /* istanbul ignore next */ []));
|
|
14627
14653
|
currentPageIndex = signal(0, ...(ngDevMode ? [{ debugName: "currentPageIndex" }] : /* istanbul ignore next */ []));
|
|
14628
14654
|
_slides = contentChildren(ClxCarouselDirective, ...(ngDevMode ? [{ debugName: "_slides" }] : /* istanbul ignore next */ []));
|
|
14629
14655
|
_autoPlayTimer = null;
|
|
@@ -14631,10 +14657,12 @@ class ClxCarouselComponent {
|
|
|
14631
14657
|
_zone;
|
|
14632
14658
|
_aspectClass = computed(() => CAROUSEL_ASPECT_RATIO_MAP[this.aspectRatio()] ?? '', ...(ngDevMode ? [{ debugName: "_aspectClass" }] : /* istanbul ignore next */ []));
|
|
14633
14659
|
_viewportClass = computed(() => {
|
|
14634
|
-
const
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14660
|
+
const classes = ['relative w-full h-full overflow-hidden', this._aspectClass()];
|
|
14661
|
+
if (this.rounded())
|
|
14662
|
+
classes.push('rounded-2xl');
|
|
14663
|
+
if (!this.transparent())
|
|
14664
|
+
classes.push('bg-white');
|
|
14665
|
+
return classes.filter(Boolean).join(' ');
|
|
14638
14666
|
}, ...(ngDevMode ? [{ debugName: "_viewportClass" }] : /* istanbul ignore next */ []));
|
|
14639
14667
|
_dotItems = computed(() => {
|
|
14640
14668
|
const active = this.currentPageIndex();
|
|
@@ -14654,15 +14682,12 @@ class ClxCarouselComponent {
|
|
|
14654
14682
|
effect(() => {
|
|
14655
14683
|
const active = this.autoPlay();
|
|
14656
14684
|
const ms = this.interval();
|
|
14685
|
+
// Sole place the timer starts (ngAfterContentInit used to start a second one too).
|
|
14657
14686
|
this._clearTimer();
|
|
14658
14687
|
if (active)
|
|
14659
14688
|
this._startTimer(ms);
|
|
14660
14689
|
});
|
|
14661
14690
|
}
|
|
14662
|
-
ngAfterContentInit() {
|
|
14663
|
-
if (this.autoPlay())
|
|
14664
|
-
this._startTimer(this.interval());
|
|
14665
|
-
}
|
|
14666
14691
|
ngOnDestroy() {
|
|
14667
14692
|
this._clearTimer();
|
|
14668
14693
|
}
|
|
@@ -14712,8 +14737,13 @@ class ClxCarouselComponent {
|
|
|
14712
14737
|
this.prev();
|
|
14713
14738
|
}
|
|
14714
14739
|
_startTimer(ms) {
|
|
14740
|
+
this._clearTimer();
|
|
14715
14741
|
this._zone.runOutsideAngular(() => {
|
|
14716
14742
|
this._autoPlayTimer = setInterval(() => {
|
|
14743
|
+
if (this.pauseOnHover() && this._hovering())
|
|
14744
|
+
return;
|
|
14745
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden')
|
|
14746
|
+
return;
|
|
14717
14747
|
this._zone.run(() => this.next());
|
|
14718
14748
|
}, ms);
|
|
14719
14749
|
});
|
|
@@ -14725,144 +14755,146 @@ class ClxCarouselComponent {
|
|
|
14725
14755
|
}
|
|
14726
14756
|
}
|
|
14727
14757
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14728
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxCarouselComponent, isStandalone: true, selector: "clx-carousel", inputs: { clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, autoPlay: { classPropertyName: "autoPlay", publicName: "autoPlay", isSignal: true, isRequired: false, transformFunction: null }, interval: { classPropertyName: "interval", publicName: "interval", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, transparent: { classPropertyName: "transparent", publicName: "transparent", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "_slides", predicate: ClxCarouselDirective, isSignal: true }], ngImport: i0, template: `
|
|
14729
|
-
<!-- Viewport -->
|
|
14730
|
-
<div class="
|
|
14731
|
-
|
|
14732
|
-
(
|
|
14733
|
-
(
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
14790
|
-
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
14758
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxCarouselComponent, isStandalone: true, selector: "clx-carousel", inputs: { clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, autoPlay: { classPropertyName: "autoPlay", publicName: "autoPlay", isSignal: true, isRequired: false, transformFunction: null }, interval: { classPropertyName: "interval", publicName: "interval", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, transparent: { classPropertyName: "transparent", publicName: "transparent", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, pauseOnHover: { classPropertyName: "pauseOnHover", publicName: "pauseOnHover", isSignal: true, isRequired: false, transformFunction: null }, arrows: { classPropertyName: "arrows", publicName: "arrows", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "_slides", predicate: ClxCarouselDirective, isSignal: true }], ngImport: i0, template: `
|
|
14759
|
+
<!-- Viewport -->
|
|
14760
|
+
<div [class]="_viewportClass()"
|
|
14761
|
+
(touchstart)="_onTouchStart($event)"
|
|
14762
|
+
(touchend)="_onTouchEnd($event)"
|
|
14763
|
+
(mouseenter)="_hovering.set(true)"
|
|
14764
|
+
(mouseleave)="_hovering.set(false)">
|
|
14765
|
+
|
|
14766
|
+
<!-- Track -->
|
|
14767
|
+
<div class="flex h-full transition-transform duration-500 ease-in-out will-change-transform"
|
|
14768
|
+
[style.transform]="'translateX(-' + currentPageIndex() * 100 + '%)'">
|
|
14769
|
+
@for (slide of _slides(); track slide) {
|
|
14770
|
+
<div class="flex-none w-full h-full">
|
|
14771
|
+
<ng-container [ngTemplateOutlet]="slide.templateRef" />
|
|
14772
|
+
</div>
|
|
14773
|
+
}
|
|
14774
|
+
</div>
|
|
14775
|
+
|
|
14776
|
+
<!-- Arrow: Previous — variant="translucent" (a solid white pill, the old color="white",
|
|
14777
|
+
reads as invisible on a light product photo or a light theme surface) stays legible
|
|
14778
|
+
against any image, light or dark, without needing to know the photo's own colors. -->
|
|
14779
|
+
@if (_slides().length > 1 && arrows() !== 'never') {
|
|
14780
|
+
<div [class]="'absolute left-3 top-1/2 -translate-y-1/2 z-10 ' + _arrowVisibility()">
|
|
14781
|
+
<button clx-button
|
|
14782
|
+
type="button"
|
|
14783
|
+
variant="translucent"
|
|
14784
|
+
shape="circle"
|
|
14785
|
+
size="sm"
|
|
14786
|
+
[iconOnly]="true"
|
|
14787
|
+
class="shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none"
|
|
14788
|
+
[disabled]="!loop() && currentPageIndex() === 0"
|
|
14789
|
+
(click)="prev()">
|
|
14790
|
+
<span clx-icon name="chevron_left" size="sm"></span>
|
|
14791
|
+
</button>
|
|
14792
|
+
</div>
|
|
14793
|
+
|
|
14794
|
+
<!-- Arrow: Next -->
|
|
14795
|
+
<div [class]="'absolute right-3 top-1/2 -translate-y-1/2 z-10 ' + _arrowVisibility()">
|
|
14796
|
+
<button clx-button
|
|
14797
|
+
type="button"
|
|
14798
|
+
variant="translucent"
|
|
14799
|
+
shape="circle"
|
|
14800
|
+
size="sm"
|
|
14801
|
+
[iconOnly]="true"
|
|
14802
|
+
class="shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none"
|
|
14803
|
+
[disabled]="!loop() && currentPageIndex() === _slides().length - 1"
|
|
14804
|
+
(click)="next()">
|
|
14805
|
+
<span clx-icon name="chevron_right" size="sm"></span>
|
|
14806
|
+
</button>
|
|
14807
|
+
</div>
|
|
14808
|
+
}
|
|
14809
|
+
|
|
14810
|
+
<!-- Dots -->
|
|
14811
|
+
@if (_slides().length > 1) {
|
|
14812
|
+
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1.5">
|
|
14813
|
+
@for (dot of _dotItems(); track dot.idx) {
|
|
14814
|
+
<button
|
|
14815
|
+
type="button"
|
|
14816
|
+
class="rounded-full transition-[width,background-color] duration-300 focus:outline-none cursor-pointer"
|
|
14817
|
+
[class]="dot.cls"
|
|
14818
|
+
(click)="goTo(dot.idx)"
|
|
14819
|
+
[attr.aria-label]="'Slide ' + (dot.idx + 1)">
|
|
14820
|
+
</button>
|
|
14821
|
+
}
|
|
14822
|
+
</div>
|
|
14823
|
+
}
|
|
14824
|
+
</div>
|
|
14794
14825
|
`, isInline: true, styles: ["clx-carousel{display:block;width:100%;height:100%}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
14795
14826
|
}
|
|
14796
14827
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxCarouselComponent, decorators: [{
|
|
14797
14828
|
type: Component,
|
|
14798
|
-
args: [{ selector: 'clx-carousel', standalone: true, imports: [NgTemplateOutlet, ClxButtonComponent, ClxIconComponent], template: `
|
|
14799
|
-
<!-- Viewport -->
|
|
14800
|
-
<div class="
|
|
14801
|
-
|
|
14802
|
-
(
|
|
14803
|
-
(
|
|
14804
|
-
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
|
|
14809
|
-
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14814
|
-
|
|
14815
|
-
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14819
|
-
|
|
14820
|
-
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14829
|
+
args: [{ selector: 'clx-carousel', standalone: true, imports: [NgTemplateOutlet, ClxButtonComponent, ClxIconComponent], template: `
|
|
14830
|
+
<!-- Viewport -->
|
|
14831
|
+
<div [class]="_viewportClass()"
|
|
14832
|
+
(touchstart)="_onTouchStart($event)"
|
|
14833
|
+
(touchend)="_onTouchEnd($event)"
|
|
14834
|
+
(mouseenter)="_hovering.set(true)"
|
|
14835
|
+
(mouseleave)="_hovering.set(false)">
|
|
14836
|
+
|
|
14837
|
+
<!-- Track -->
|
|
14838
|
+
<div class="flex h-full transition-transform duration-500 ease-in-out will-change-transform"
|
|
14839
|
+
[style.transform]="'translateX(-' + currentPageIndex() * 100 + '%)'">
|
|
14840
|
+
@for (slide of _slides(); track slide) {
|
|
14841
|
+
<div class="flex-none w-full h-full">
|
|
14842
|
+
<ng-container [ngTemplateOutlet]="slide.templateRef" />
|
|
14843
|
+
</div>
|
|
14844
|
+
}
|
|
14845
|
+
</div>
|
|
14846
|
+
|
|
14847
|
+
<!-- Arrow: Previous — variant="translucent" (a solid white pill, the old color="white",
|
|
14848
|
+
reads as invisible on a light product photo or a light theme surface) stays legible
|
|
14849
|
+
against any image, light or dark, without needing to know the photo's own colors. -->
|
|
14850
|
+
@if (_slides().length > 1 && arrows() !== 'never') {
|
|
14851
|
+
<div [class]="'absolute left-3 top-1/2 -translate-y-1/2 z-10 ' + _arrowVisibility()">
|
|
14852
|
+
<button clx-button
|
|
14853
|
+
type="button"
|
|
14854
|
+
variant="translucent"
|
|
14855
|
+
shape="circle"
|
|
14856
|
+
size="sm"
|
|
14857
|
+
[iconOnly]="true"
|
|
14858
|
+
class="shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none"
|
|
14859
|
+
[disabled]="!loop() && currentPageIndex() === 0"
|
|
14860
|
+
(click)="prev()">
|
|
14861
|
+
<span clx-icon name="chevron_left" size="sm"></span>
|
|
14862
|
+
</button>
|
|
14863
|
+
</div>
|
|
14864
|
+
|
|
14865
|
+
<!-- Arrow: Next -->
|
|
14866
|
+
<div [class]="'absolute right-3 top-1/2 -translate-y-1/2 z-10 ' + _arrowVisibility()">
|
|
14867
|
+
<button clx-button
|
|
14868
|
+
type="button"
|
|
14869
|
+
variant="translucent"
|
|
14870
|
+
shape="circle"
|
|
14871
|
+
size="sm"
|
|
14872
|
+
[iconOnly]="true"
|
|
14873
|
+
class="shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none"
|
|
14874
|
+
[disabled]="!loop() && currentPageIndex() === _slides().length - 1"
|
|
14875
|
+
(click)="next()">
|
|
14876
|
+
<span clx-icon name="chevron_right" size="sm"></span>
|
|
14877
|
+
</button>
|
|
14878
|
+
</div>
|
|
14879
|
+
}
|
|
14880
|
+
|
|
14881
|
+
<!-- Dots -->
|
|
14882
|
+
@if (_slides().length > 1) {
|
|
14883
|
+
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1.5">
|
|
14884
|
+
@for (dot of _dotItems(); track dot.idx) {
|
|
14885
|
+
<button
|
|
14886
|
+
type="button"
|
|
14887
|
+
class="rounded-full transition-[width,background-color] duration-300 focus:outline-none cursor-pointer"
|
|
14888
|
+
[class]="dot.cls"
|
|
14889
|
+
(click)="goTo(dot.idx)"
|
|
14890
|
+
[attr.aria-label]="'Slide ' + (dot.idx + 1)">
|
|
14891
|
+
</button>
|
|
14892
|
+
}
|
|
14893
|
+
</div>
|
|
14894
|
+
}
|
|
14895
|
+
</div>
|
|
14864
14896
|
`, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["clx-carousel{display:block;width:100%;height:100%}\n"] }]
|
|
14865
|
-
}], ctorParameters: () => [], propDecorators: { clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], autoPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoPlay", required: false }] }], interval: [{ type: i0.Input, args: [{ isSignal: true, alias: "interval", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], transparent: [{ type: i0.Input, args: [{ isSignal: true, alias: "transparent", required: false }] }], _slides: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ClxCarouselDirective), { isSignal: true }] }] } });
|
|
14897
|
+
}], ctorParameters: () => [], propDecorators: { clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], autoPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoPlay", required: false }] }], interval: [{ type: i0.Input, args: [{ isSignal: true, alias: "interval", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], transparent: [{ type: i0.Input, args: [{ isSignal: true, alias: "transparent", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], pauseOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseOnHover", required: false }] }], arrows: [{ type: i0.Input, args: [{ isSignal: true, alias: "arrows", required: false }] }], _slides: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ClxCarouselDirective), { isSignal: true }] }] } });
|
|
14866
14898
|
|
|
14867
14899
|
class ClxAnimateDirective {
|
|
14868
14900
|
// ─── Inputs ───────────────────────────────────────────────────────────────
|
|
@@ -17672,7 +17704,7 @@ class ClxProductDetailComponent {
|
|
|
17672
17704
|
specsLeft(specs) { return specs.slice(0, Math.ceil(specs.length / 2)); }
|
|
17673
17705
|
specsRight(specs) { return specs.slice(Math.ceil(specs.length / 2)); }
|
|
17674
17706
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17675
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxProductDetailComponent, isStandalone: true, selector: "clx-product-detail", inputs: { product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, initialVariationId: { classPropertyName: "initialVariationId", publicName: "initialVariationId", isSignal: true, isRequired: false, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowColor: { classPropertyName: "buyNowColor", publicName: "buyNowColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowTextColor: { classPropertyName: "buyNowTextColor", publicName: "buyNowTextColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartColor: { classPropertyName: "addToCartColor", publicName: "addToCartColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartTextColor: { classPropertyName: "addToCartTextColor", publicName: "addToCartTextColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewColor: { classPropertyName: "leaveReviewColor", publicName: "leaveReviewColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewTextColor: { classPropertyName: "leaveReviewTextColor", publicName: "leaveReviewTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistColor: { classPropertyName: "wishlistColor", publicName: "wishlistColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistTextColor: { classPropertyName: "wishlistTextColor", publicName: "wishlistTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistVariant: { classPropertyName: "wishlistVariant", publicName: "wishlistVariant", isSignal: true, isRequired: false, transformFunction: null }, stockBadgeColor: { classPropertyName: "stockBadgeColor", publicName: "stockBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, discountBadgeColor: { classPropertyName: "discountBadgeColor", publicName: "discountBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, quantityColor: { classPropertyName: "quantityColor", publicName: "quantityColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonColor: { classPropertyName: "specButtonColor", publicName: "specButtonColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonTextColor: { classPropertyName: "specButtonTextColor", publicName: "specButtonTextColor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addToCart: "addToCart", buyNow: "buyNow", wishlist: "wishlist", variationChange: "variationChange", leaveReview: "leaveReview" }, viewQueries: [{ propertyName: "_carousel", first: true, predicate: ClxCarouselComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u2550\u2550 SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (loading()) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\n <div class=\"space-y-3\">\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\n <div class=\"flex gap-2\">\n @for (t of [1,2,3,4]; track t) {\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\n }\n </div>\n </div>\n <div class=\"space-y-4 pt-2\">\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n </div>\n </div>\n}\n\n<!-- \u2550\u2550 CONTENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (!loading() && product(); as p) {\n @let v = activeVariation();\n <div class=\"space-y-8\">\n\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\n purchase info column, especially once a product has specs/variants to list; a fixed\n max-width on the gallery alone (tried first) just left dead space beside it instead of\n letting the info column actually grow into it. -->\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\n\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\n header. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\n <div class=\"relative aspect-4/3\">\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <ng-template clxSlide>\n <div class=\"w-full h-full p-9\">\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\n </div>\n </ng-template>\n }\n </clx-carousel>\n </div>\n @if (v && !v.in_stock) {\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\n </div>\n }\n <div class=\"absolute top-3 right-3 z-10\">\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\n (click)=\"wishlist.emit()\">\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\n </button>\n </div>\n </div>\n <div class=\"flex flex-wrap gap-2\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <img [src]=\"img\" [alt]=\"p.product_name\"\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\n [class]=\"_carousel()?.currentPageIndex() === i\n ? _thumbBorderClass() + ' opacity-100'\n : 'border-transparent opacity-60 hover:opacity-90'\"\n loading=\"lazy\"\n (click)=\"_carousel()?.goTo(i)\" />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\n\n <!-- Stock status -->\n @if (v) {\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\n @if (v.in_stock) {\n @if (v.stock_quantity != null) {\n {{ v.stock_quantity }} en stock\n } @else {\n En stock\n }\n } @else {\n Sin stock\n }\n </span>\n }\n\n <!-- Tags (st_products.tags) -->\n <div class=\"flex flex-wrap gap-1\">\n @for (tag of p.tags; track tag) {\n <span clx-badge [variant]=\"tagVariant(tag)\" [color]=\"tagColor(tag)\" size=\"sm\" shape=\"pill\">\n {{ tagLabel(tag) }}\n </span>\n }\n </div>\n\n <!-- Title -->\n <div>\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\n @if (p.product_detail) {\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\n }\n </div>\n\n <!-- Rating -->\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\n </div>\n\n <!-- Price block -->\n <div>\n <div class=\"flex items-baseline gap-3\">\n <span class=\"text-2xl font-black text-clx-text-label\">\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n @if (p.has_discount && discount() > 0) {\n <span class=\"text-base text-clx-text-subtle line-through\">\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\n -{{ discount() }}%\n </span>\n }\n </div>\n </div>\n\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\n @for (specType of p.spec_types; track specType.id) {\n @if (specType.input_type === 'color') {\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button\n type=\"button\"\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\n [style.background-color]=\"opt.value\"\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\n [clxTooltip]=\"opt.label ?? opt.value\"\n clxTooltipPosition=\"top\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n </button>\n }\n }\n </clx-button-group>\n } @else {\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button clx-button\n type=\"button\"\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\n [color]=\"_specButtonColor()\"\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\n size=\"sm\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n {{ opt.label ?? opt.value }}\n </button>\n }\n }\n </clx-button-group>\n }\n }\n\n <!-- Quantity -->\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\n </clx-number>\n\n <!-- CTA -->\n @if (v?.in_stock && !isSelectionComplete()) {\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\n }\n <div class=\"flex gap-3\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\n </div>\n\n </div>\n </div>\n\n @if (!compact()) {\n <!-- \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\n\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\n <ng-template [clxTabPanel]=\"'general'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsLeft(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsRight(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\n <ng-template [clxTabPanel]=\"'specs'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsLeft(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsRight(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\n <ng-template [clxTabPanel]=\"'description'\">\n <div class=\"space-y-5\">\n @for (block of p.descriptions; track block.id) {\n <div>\n @if (block.title) {\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\n }\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\n </div>\n }\n </div>\n </ng-template>\n\n </clx-tabs>\n\n <!-- \u2500\u2500 Reviews \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\n\n <div class=\"space-y-3\">\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\n <div class=\"flex items-end gap-3\">\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <div class=\"pb-1 space-y-1\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\n </div>\n </div>\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\n <div class=\"pt-2 space-y-2\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\n (click)=\"leaveReview.emit()\">\n Dejar una rese\u00F1a\n </button>\n </div>\n </div>\n\n <div class=\"lg:col-span-2 space-y-4\">\n @for (review of p.reviews ?? []; track review.id) {\n <div [class]=\"_reviewCardClass()\">\n <div class=\"flex items-center gap-3 px-4 py-3\">\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\n <span class=\"text-sm font-bold text-clx-text-muted\">\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\n </div>\n @if (review.is_verified) {\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\n }\n </div>\n <div class=\"px-4 pb-4 space-y-2\">\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\n @if (review.title) {\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\n }\n </div>\n @if (review.body) {\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\n }\n <div class=\"flex items-center gap-3 pt-1\">\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\n </div>\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxButtonGroupComponent, selector: "clx-button-group", inputs: ["label", "shape", "attached", "orientation"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSkeletonComponent, selector: "clx-skeleton", inputs: ["variant", "size", "width", "height"] }, { kind: "component", type: ClxRatingComponent, selector: "clx-rating", inputs: ["value", "max", "color"] }, { kind: "component", type: ClxCarouselComponent, selector: "clx-carousel", inputs: ["clxColor", "autoPlay", "interval", "loop", "aspectRatio", "transparent"] }, { kind: "directive", type: ClxCarouselDirective, selector: "[clxSlide]" }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }, { kind: "directive", type: ClxTabDirective, selector: "[clxTabPanel]", inputs: ["clxTabPanel"] }, { kind: "component", type: ClxNumberComponent, selector: "clx-number", inputs: ["variant", "size", "color", "label", "placeholder", "hint", "prefixIcon", "min", "max", "step", "value", "disabled"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }, { kind: "component", type: ClxListComponent, selector: "clx-list", inputs: ["color", "variant", "size", "numbered"] }, { kind: "component", type: ClxListItemComponent, selector: "clx-list-item", inputs: ["label", "sublabel", "wrapSublabel", "icon", "meta", "metaColor", "disabled"] }, { kind: "pipe", type: CurrencyPipe, name: "currency" }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
17707
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxProductDetailComponent, isStandalone: true, selector: "clx-product-detail", inputs: { product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, initialVariationId: { classPropertyName: "initialVariationId", publicName: "initialVariationId", isSignal: true, isRequired: false, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowColor: { classPropertyName: "buyNowColor", publicName: "buyNowColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowTextColor: { classPropertyName: "buyNowTextColor", publicName: "buyNowTextColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartColor: { classPropertyName: "addToCartColor", publicName: "addToCartColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartTextColor: { classPropertyName: "addToCartTextColor", publicName: "addToCartTextColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewColor: { classPropertyName: "leaveReviewColor", publicName: "leaveReviewColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewTextColor: { classPropertyName: "leaveReviewTextColor", publicName: "leaveReviewTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistColor: { classPropertyName: "wishlistColor", publicName: "wishlistColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistTextColor: { classPropertyName: "wishlistTextColor", publicName: "wishlistTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistVariant: { classPropertyName: "wishlistVariant", publicName: "wishlistVariant", isSignal: true, isRequired: false, transformFunction: null }, stockBadgeColor: { classPropertyName: "stockBadgeColor", publicName: "stockBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, discountBadgeColor: { classPropertyName: "discountBadgeColor", publicName: "discountBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, quantityColor: { classPropertyName: "quantityColor", publicName: "quantityColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonColor: { classPropertyName: "specButtonColor", publicName: "specButtonColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonTextColor: { classPropertyName: "specButtonTextColor", publicName: "specButtonTextColor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addToCart: "addToCart", buyNow: "buyNow", wishlist: "wishlist", variationChange: "variationChange", leaveReview: "leaveReview" }, viewQueries: [{ propertyName: "_carousel", first: true, predicate: ClxCarouselComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u2550\u2550 SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (loading()) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\n <div class=\"space-y-3\">\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\n <div class=\"flex gap-2\">\n @for (t of [1,2,3,4]; track t) {\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\n }\n </div>\n </div>\n <div class=\"space-y-4 pt-2\">\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n </div>\n </div>\n}\n\n<!-- \u2550\u2550 CONTENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (!loading() && product(); as p) {\n @let v = activeVariation();\n <div class=\"space-y-8\">\n\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\n purchase info column, especially once a product has specs/variants to list; a fixed\n max-width on the gallery alone (tried first) just left dead space beside it instead of\n letting the info column actually grow into it. -->\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\n\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\n header. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\n <div class=\"relative aspect-4/3\">\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <ng-template clxSlide>\n <div class=\"w-full h-full p-9\">\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\n </div>\n </ng-template>\n }\n </clx-carousel>\n </div>\n @if (v && !v.in_stock) {\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\n </div>\n }\n <div class=\"absolute top-3 right-3 z-10\">\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\n (click)=\"wishlist.emit()\">\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\n </button>\n </div>\n </div>\n <div class=\"flex flex-wrap gap-2\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <img [src]=\"img\" [alt]=\"p.product_name\"\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\n [class]=\"_carousel()?.currentPageIndex() === i\n ? _thumbBorderClass() + ' opacity-100'\n : 'border-transparent opacity-60 hover:opacity-90'\"\n loading=\"lazy\"\n (click)=\"_carousel()?.goTo(i)\" />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\n\n <!-- Stock status -->\n @if (v) {\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\n @if (v.in_stock) {\n @if (v.stock_quantity != null) {\n {{ v.stock_quantity }} en stock\n } @else {\n En stock\n }\n } @else {\n Sin stock\n }\n </span>\n }\n\n <!-- Tags (st_products.tags) -->\n <div class=\"flex flex-wrap gap-1\">\n @for (tag of p.tags; track tag) {\n <span clx-badge [variant]=\"tagVariant(tag)\" [color]=\"tagColor(tag)\" size=\"sm\" shape=\"pill\">\n {{ tagLabel(tag) }}\n </span>\n }\n </div>\n\n <!-- Title -->\n <div>\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\n @if (p.product_detail) {\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\n }\n </div>\n\n <!-- Rating -->\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\n </div>\n\n <!-- Price block -->\n <div>\n <div class=\"flex items-baseline gap-3\">\n <span class=\"text-2xl font-black text-clx-text-label\">\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n @if (p.has_discount && discount() > 0) {\n <span class=\"text-base text-clx-text-subtle line-through\">\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\n -{{ discount() }}%\n </span>\n }\n </div>\n </div>\n\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\n @for (specType of p.spec_types; track specType.id) {\n @if (specType.input_type === 'color') {\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button\n type=\"button\"\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\n [style.background-color]=\"opt.value\"\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\n [clxTooltip]=\"opt.label ?? opt.value\"\n clxTooltipPosition=\"top\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n </button>\n }\n }\n </clx-button-group>\n } @else {\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button clx-button\n type=\"button\"\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\n [color]=\"_specButtonColor()\"\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\n size=\"sm\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n {{ opt.label ?? opt.value }}\n </button>\n }\n }\n </clx-button-group>\n }\n }\n\n <!-- Quantity -->\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\n </clx-number>\n\n <!-- CTA -->\n @if (v?.in_stock && !isSelectionComplete()) {\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\n }\n <div class=\"flex gap-3\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\n </div>\n\n </div>\n </div>\n\n @if (!compact()) {\n <!-- \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\n\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\n <ng-template [clxTabPanel]=\"'general'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsLeft(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsRight(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\n <ng-template [clxTabPanel]=\"'specs'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsLeft(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsRight(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\n <ng-template [clxTabPanel]=\"'description'\">\n <div class=\"space-y-5\">\n @for (block of p.descriptions; track block.id) {\n <div>\n @if (block.title) {\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\n }\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\n </div>\n }\n </div>\n </ng-template>\n\n </clx-tabs>\n\n <!-- \u2500\u2500 Reviews \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\n\n <div class=\"space-y-3\">\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\n <div class=\"flex items-end gap-3\">\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <div class=\"pb-1 space-y-1\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\n </div>\n </div>\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\n <div class=\"pt-2 space-y-2\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\n (click)=\"leaveReview.emit()\">\n Dejar una rese\u00F1a\n </button>\n </div>\n </div>\n\n <div class=\"lg:col-span-2 space-y-4\">\n @for (review of p.reviews ?? []; track review.id) {\n <div [class]=\"_reviewCardClass()\">\n <div class=\"flex items-center gap-3 px-4 py-3\">\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\n <span class=\"text-sm font-bold text-clx-text-muted\">\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\n </div>\n @if (review.is_verified) {\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\n }\n </div>\n <div class=\"px-4 pb-4 space-y-2\">\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\n @if (review.title) {\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\n }\n </div>\n @if (review.body) {\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\n }\n <div class=\"flex items-center gap-3 pt-1\">\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\n </div>\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxButtonGroupComponent, selector: "clx-button-group", inputs: ["label", "shape", "attached", "orientation"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSkeletonComponent, selector: "clx-skeleton", inputs: ["variant", "size", "width", "height"] }, { kind: "component", type: ClxRatingComponent, selector: "clx-rating", inputs: ["value", "max", "color"] }, { kind: "component", type: ClxCarouselComponent, selector: "clx-carousel", inputs: ["clxColor", "autoPlay", "interval", "loop", "aspectRatio", "transparent", "rounded", "pauseOnHover", "arrows"] }, { kind: "directive", type: ClxCarouselDirective, selector: "[clxSlide]" }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }, { kind: "directive", type: ClxTabDirective, selector: "[clxTabPanel]", inputs: ["clxTabPanel"] }, { kind: "component", type: ClxNumberComponent, selector: "clx-number", inputs: ["variant", "size", "color", "label", "placeholder", "hint", "prefixIcon", "min", "max", "step", "value", "disabled"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }, { kind: "component", type: ClxListComponent, selector: "clx-list", inputs: ["color", "variant", "size", "numbered"] }, { kind: "component", type: ClxListItemComponent, selector: "clx-list-item", inputs: ["label", "sublabel", "wrapSublabel", "icon", "meta", "metaColor", "disabled"] }, { kind: "pipe", type: CurrencyPipe, name: "currency" }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
17676
17708
|
}
|
|
17677
17709
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, decorators: [{
|
|
17678
17710
|
type: Component,
|