codexly-ui 0.25.1 → 0.25.2
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
CHANGED
|
@@ -18203,6 +18203,11 @@ class ClxProductDetailComponent {
|
|
|
18203
18203
|
/** Hides the tabs (Características/Especificaciones/Descripción) and the Reviews section —
|
|
18204
18204
|
* for contexts like a quick-view modal where only the gallery + spec selector + CTA matter. */
|
|
18205
18205
|
compact = input(false, ...(ngDevMode ? [{ debugName: "compact" }] : /* istanbul ignore next */ []));
|
|
18206
|
+
/** False = a catalog view with nothing to buy (e.g. an ERP looking a product up to share it):
|
|
18207
|
+
* hides favorites, rating, quantity, "Comprar ahora" / "Agrega al carrito" and the Reviews
|
|
18208
|
+
* section, keeping the gallery, the spec selectors and the tabs. Whatever the host projects with
|
|
18209
|
+
* [clxProductDetailActions] renders where the purchase buttons were, in either mode. */
|
|
18210
|
+
purchasable = input(true, ...(ngDevMode ? [{ debugName: "purchasable" }] : /* istanbul ignore next */ []));
|
|
18206
18211
|
/** Same ClxCardVariant concept as clx-card: 'elevated' (border + shadow, the default the
|
|
18207
18212
|
* gallery/info panels and each review card have always rendered with), 'outlined' (border, no
|
|
18208
18213
|
* shadow), 'flat' (neither). The border itself always reads cardBorderColor() regardless of
|
|
@@ -18445,7 +18450,7 @@ class ClxProductDetailComponent {
|
|
|
18445
18450
|
specsLeft(specs) { return specs.slice(0, Math.ceil(specs.length / 2)); }
|
|
18446
18451
|
specsRight(specs) { return specs.slice(Math.ceil(specs.length / 2)); }
|
|
18447
18452
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18448
|
-
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 (_tagSvc.configured()) {\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\n } @else 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 + tags \u2014 over the gallery instead when the host configured its tags -->\n @if (v && !_tagSvc.configured()) {\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\" [class.hidden]=\"_tagSvc.configured()\">\n @for (t of _tags(); track t.code) {\n <span clx-badge [variant]=\"t.variant\" [color]=\"t.color\" size=\"sm\" shape=\"pill\">\n {{ t.label }}\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 (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", "textColor"] }, { kind: "component", type: ClxProductBadgesComponent, selector: "clx-product-badges", inputs: ["badges", "size", "topRightOffset", "bottomRightOffset"] }, { 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 });
|
|
18453
|
+
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 }, purchasable: { classPropertyName: "purchasable", publicName: "purchasable", 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 -->\r\n@if (loading()) {\r\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\r\n <div class=\"space-y-3\">\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\r\n <div class=\"flex gap-2\">\r\n @for (t of [1,2,3,4]; track t) {\r\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"space-y-4 pt-2\">\r\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n </div>\r\n </div>\r\n}\r\n\r\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 -->\r\n@if (!loading() && product(); as p) {\r\n @let v = activeVariation();\r\n <div class=\"space-y-8\">\r\n\r\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\r\n purchase info column, especially once a product has specs/variants to list; a fixed\r\n max-width on the gallery alone (tried first) just left dead space beside it instead of\r\n letting the info column actually grow into it. -->\r\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\r\n\r\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\r\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\r\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\r\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\r\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\r\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\r\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\r\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 -->\r\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\r\n <div class=\"relative aspect-4/3\">\r\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\r\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <ng-template clxSlide>\r\n <div class=\"w-full h-full p-9\">\r\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\r\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\r\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\r\n </div>\r\n </ng-template>\r\n }\r\n </clx-carousel>\r\n </div>\r\n @if (_tagSvc.configured()) {\r\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\r\n } @else if (v && !v.in_stock) {\r\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\r\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\r\n </div>\r\n }\r\n @if (purchasable()) {\r\n <div class=\"absolute top-3 right-3 z-10\">\r\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\r\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\r\n (click)=\"wishlist.emit()\">\r\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <img [src]=\"img\" [alt]=\"p.product_name\"\r\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\r\n [class]=\"_carousel()?.currentPageIndex() === i\r\n ? _thumbBorderClass() + ' opacity-100'\r\n : 'border-transparent opacity-60 hover:opacity-90'\"\r\n loading=\"lazy\"\r\n (click)=\"_carousel()?.goTo(i)\" />\r\n }\r\n </div>\r\n </div>\r\n\r\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 -->\r\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\r\n\r\n <!-- Stock status + tags \u2014 over the gallery instead when the host configured its tags -->\r\n @if (v && !_tagSvc.configured()) {\r\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\r\n @if (v.in_stock) {\r\n @if (v.stock_quantity != null) {\r\n {{ v.stock_quantity }} en stock\r\n } @else {\r\n En stock\r\n }\r\n } @else {\r\n Sin stock\r\n }\r\n </span>\r\n }\r\n\r\n <!-- Tags (st_products.tags) -->\r\n <div class=\"flex flex-wrap gap-1\" [class.hidden]=\"_tagSvc.configured()\">\r\n @for (t of _tags(); track t.code) {\r\n <span clx-badge [variant]=\"t.variant\" [color]=\"t.color\" size=\"sm\" shape=\"pill\">\r\n {{ t.label }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Title -->\r\n <div>\r\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\r\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\r\n @if (p.product_detail) {\r\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\r\n }\r\n </div>\r\n\r\n <!-- Rating -->\r\n @if (purchasable()) {\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\r\n </div>\r\n }\r\n\r\n <!-- Price block -->\r\n <div>\r\n <div class=\"flex items-baseline gap-3\">\r\n <span class=\"text-2xl font-black text-clx-text-label\">\r\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n @if (discount() > 0) {\r\n <span class=\"text-base text-clx-text-subtle line-through\">\r\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\r\n -{{ discount() }}%\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\r\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\r\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\r\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\r\n @for (specType of p.spec_types; track specType.id) {\r\n @if (specType.input_type === 'color') {\r\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button\r\n type=\"button\"\r\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\r\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\r\n [style.background-color]=\"opt.value\"\r\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\r\n [clxTooltip]=\"opt.label ?? opt.value\"\r\n clxTooltipPosition=\"top\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n } @else {\r\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button clx-button\r\n type=\"button\"\r\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\r\n [color]=\"_specButtonColor()\"\r\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\r\n size=\"sm\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n {{ opt.label ?? opt.value }}\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n }\r\n }\r\n\r\n @if (purchasable()) {\r\n <!-- Quantity -->\r\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\r\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\r\n </clx-number>\r\n\r\n <!-- CTA -->\r\n @if (v?.in_stock && !isSelectionComplete()) {\r\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\r\n }\r\n <div class=\"flex gap-3\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\r\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\r\n </div>\r\n }\r\n\r\n <!-- Host actions (e.g. an ERP's share options) -->\r\n <ng-content select=\"[clxProductDetailActions]\" />\r\n\r\n </div>\r\n </div>\r\n\r\n @if (!compact()) {\r\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 -->\r\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\r\n\r\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\r\n <ng-template [clxTabPanel]=\"'general'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsLeft(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsRight(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\r\n <ng-template [clxTabPanel]=\"'specs'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsLeft(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsRight(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\r\n <ng-template [clxTabPanel]=\"'description'\">\r\n <div class=\"space-y-5\">\r\n @for (block of p.descriptions; track block.id) {\r\n <div>\r\n @if (block.title) {\r\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\r\n }\r\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n </clx-tabs>\r\n\r\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 -->\r\n @if (purchasable()) {\r\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\r\n\r\n <div class=\"space-y-3\">\r\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\r\n <div class=\"flex items-end gap-3\">\r\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <div class=\"pb-1 space-y-1\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\r\n </div>\r\n </div>\r\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\r\n <div class=\"pt-2 space-y-2\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\r\n (click)=\"leaveReview.emit()\">\r\n Dejar una rese\u00F1a\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"lg:col-span-2 space-y-4\">\r\n @for (review of p.reviews ?? []; track review.id) {\r\n <div [class]=\"_reviewCardClass()\">\r\n <div class=\"flex items-center gap-3 px-4 py-3\">\r\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\r\n <span class=\"text-sm font-bold text-clx-text-muted\">\r\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\r\n </span>\r\n </div>\r\n <div class=\"flex-1 min-w-0\">\r\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\r\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\r\n </div>\r\n @if (review.is_verified) {\r\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\r\n }\r\n </div>\r\n <div class=\"px-4 pb-4 space-y-2\">\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\r\n @if (review.title) {\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\r\n }\r\n </div>\r\n @if (review.body) {\r\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\r\n }\r\n <div class=\"flex items-center gap-3 pt-1\">\r\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n }\r\n }\r\n\r\n </div>\r\n}\r\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", "textColor"] }, { kind: "component", type: ClxProductBadgesComponent, selector: "clx-product-badges", inputs: ["badges", "size", "topRightOffset", "bottomRightOffset"] }, { 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 });
|
|
18449
18454
|
}
|
|
18450
18455
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, decorators: [{
|
|
18451
18456
|
type: Component,
|
|
@@ -18468,8 +18473,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
18468
18473
|
ClxTooltipDirective,
|
|
18469
18474
|
ClxListComponent,
|
|
18470
18475
|
ClxListItemComponent,
|
|
18471
|
-
], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, 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 (_tagSvc.configured()) {\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\n } @else 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 + tags \u2014 over the gallery instead when the host configured its tags -->\n @if (v && !_tagSvc.configured()) {\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\" [class.hidden]=\"_tagSvc.configured()\">\n @for (t of _tags(); track t.code) {\n <span clx-badge [variant]=\"t.variant\" [color]=\"t.color\" size=\"sm\" shape=\"pill\">\n {{ t.label }}\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 (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" }]
|
|
18472
|
-
}], ctorParameters: () => [], propDecorators: { product: [{ type: i0.Input, args: [{ isSignal: true, alias: "product", required: false }] }], initialVariationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialVariationId", required: false }] }], clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], buyNowColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowColor", required: false }] }], buyNowTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowTextColor", required: false }] }], addToCartColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartColor", required: false }] }], addToCartTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartTextColor", required: false }] }], leaveReviewColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewColor", required: false }] }], leaveReviewTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewTextColor", required: false }] }], wishlistColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistColor", required: false }] }], wishlistTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistTextColor", required: false }] }], wishlistVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistVariant", required: false }] }], stockBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "stockBadgeColor", required: false }] }], discountBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "discountBadgeColor", required: false }] }], quantityColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "quantityColor", required: false }] }], specButtonColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonColor", required: false }] }], specButtonTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonTextColor", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], addToCart: [{ type: i0.Output, args: ["addToCart"] }], buyNow: [{ type: i0.Output, args: ["buyNow"] }], wishlist: [{ type: i0.Output, args: ["wishlist"] }], variationChange: [{ type: i0.Output, args: ["variationChange"] }], leaveReview: [{ type: i0.Output, args: ["leaveReview"] }], _carousel: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ClxCarouselComponent), { isSignal: true }] }] } });
|
|
18476
|
+
], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, 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 -->\r\n@if (loading()) {\r\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\r\n <div class=\"space-y-3\">\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\r\n <div class=\"flex gap-2\">\r\n @for (t of [1,2,3,4]; track t) {\r\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"space-y-4 pt-2\">\r\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n </div>\r\n </div>\r\n}\r\n\r\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 -->\r\n@if (!loading() && product(); as p) {\r\n @let v = activeVariation();\r\n <div class=\"space-y-8\">\r\n\r\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\r\n purchase info column, especially once a product has specs/variants to list; a fixed\r\n max-width on the gallery alone (tried first) just left dead space beside it instead of\r\n letting the info column actually grow into it. -->\r\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\r\n\r\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\r\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\r\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\r\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\r\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\r\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\r\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\r\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 -->\r\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\r\n <div class=\"relative aspect-4/3\">\r\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\r\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <ng-template clxSlide>\r\n <div class=\"w-full h-full p-9\">\r\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\r\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\r\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\r\n </div>\r\n </ng-template>\r\n }\r\n </clx-carousel>\r\n </div>\r\n @if (_tagSvc.configured()) {\r\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\r\n } @else if (v && !v.in_stock) {\r\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\r\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\r\n </div>\r\n }\r\n @if (purchasable()) {\r\n <div class=\"absolute top-3 right-3 z-10\">\r\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\r\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\r\n (click)=\"wishlist.emit()\">\r\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <img [src]=\"img\" [alt]=\"p.product_name\"\r\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\r\n [class]=\"_carousel()?.currentPageIndex() === i\r\n ? _thumbBorderClass() + ' opacity-100'\r\n : 'border-transparent opacity-60 hover:opacity-90'\"\r\n loading=\"lazy\"\r\n (click)=\"_carousel()?.goTo(i)\" />\r\n }\r\n </div>\r\n </div>\r\n\r\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 -->\r\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\r\n\r\n <!-- Stock status + tags \u2014 over the gallery instead when the host configured its tags -->\r\n @if (v && !_tagSvc.configured()) {\r\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\r\n @if (v.in_stock) {\r\n @if (v.stock_quantity != null) {\r\n {{ v.stock_quantity }} en stock\r\n } @else {\r\n En stock\r\n }\r\n } @else {\r\n Sin stock\r\n }\r\n </span>\r\n }\r\n\r\n <!-- Tags (st_products.tags) -->\r\n <div class=\"flex flex-wrap gap-1\" [class.hidden]=\"_tagSvc.configured()\">\r\n @for (t of _tags(); track t.code) {\r\n <span clx-badge [variant]=\"t.variant\" [color]=\"t.color\" size=\"sm\" shape=\"pill\">\r\n {{ t.label }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Title -->\r\n <div>\r\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\r\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\r\n @if (p.product_detail) {\r\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\r\n }\r\n </div>\r\n\r\n <!-- Rating -->\r\n @if (purchasable()) {\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\r\n </div>\r\n }\r\n\r\n <!-- Price block -->\r\n <div>\r\n <div class=\"flex items-baseline gap-3\">\r\n <span class=\"text-2xl font-black text-clx-text-label\">\r\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n @if (discount() > 0) {\r\n <span class=\"text-base text-clx-text-subtle line-through\">\r\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\r\n -{{ discount() }}%\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\r\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\r\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\r\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\r\n @for (specType of p.spec_types; track specType.id) {\r\n @if (specType.input_type === 'color') {\r\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button\r\n type=\"button\"\r\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\r\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\r\n [style.background-color]=\"opt.value\"\r\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\r\n [clxTooltip]=\"opt.label ?? opt.value\"\r\n clxTooltipPosition=\"top\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n } @else {\r\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button clx-button\r\n type=\"button\"\r\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\r\n [color]=\"_specButtonColor()\"\r\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\r\n size=\"sm\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n {{ opt.label ?? opt.value }}\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n }\r\n }\r\n\r\n @if (purchasable()) {\r\n <!-- Quantity -->\r\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\r\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\r\n </clx-number>\r\n\r\n <!-- CTA -->\r\n @if (v?.in_stock && !isSelectionComplete()) {\r\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\r\n }\r\n <div class=\"flex gap-3\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\r\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\r\n </div>\r\n }\r\n\r\n <!-- Host actions (e.g. an ERP's share options) -->\r\n <ng-content select=\"[clxProductDetailActions]\" />\r\n\r\n </div>\r\n </div>\r\n\r\n @if (!compact()) {\r\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 -->\r\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\r\n\r\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\r\n <ng-template [clxTabPanel]=\"'general'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsLeft(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsRight(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\r\n <ng-template [clxTabPanel]=\"'specs'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsLeft(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsRight(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\r\n <ng-template [clxTabPanel]=\"'description'\">\r\n <div class=\"space-y-5\">\r\n @for (block of p.descriptions; track block.id) {\r\n <div>\r\n @if (block.title) {\r\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\r\n }\r\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n </clx-tabs>\r\n\r\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 -->\r\n @if (purchasable()) {\r\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\r\n\r\n <div class=\"space-y-3\">\r\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\r\n <div class=\"flex items-end gap-3\">\r\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <div class=\"pb-1 space-y-1\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\r\n </div>\r\n </div>\r\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\r\n <div class=\"pt-2 space-y-2\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\r\n (click)=\"leaveReview.emit()\">\r\n Dejar una rese\u00F1a\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"lg:col-span-2 space-y-4\">\r\n @for (review of p.reviews ?? []; track review.id) {\r\n <div [class]=\"_reviewCardClass()\">\r\n <div class=\"flex items-center gap-3 px-4 py-3\">\r\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\r\n <span class=\"text-sm font-bold text-clx-text-muted\">\r\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\r\n </span>\r\n </div>\r\n <div class=\"flex-1 min-w-0\">\r\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\r\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\r\n </div>\r\n @if (review.is_verified) {\r\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\r\n }\r\n </div>\r\n <div class=\"px-4 pb-4 space-y-2\">\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\r\n @if (review.title) {\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\r\n }\r\n </div>\r\n @if (review.body) {\r\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\r\n }\r\n <div class=\"flex items-center gap-3 pt-1\">\r\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n }\r\n }\r\n\r\n </div>\r\n}\r\n" }]
|
|
18477
|
+
}], ctorParameters: () => [], propDecorators: { product: [{ type: i0.Input, args: [{ isSignal: true, alias: "product", required: false }] }], initialVariationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialVariationId", required: false }] }], clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], buyNowColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowColor", required: false }] }], buyNowTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowTextColor", required: false }] }], addToCartColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartColor", required: false }] }], addToCartTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartTextColor", required: false }] }], leaveReviewColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewColor", required: false }] }], leaveReviewTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewTextColor", required: false }] }], wishlistColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistColor", required: false }] }], wishlistTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistTextColor", required: false }] }], wishlistVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistVariant", required: false }] }], stockBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "stockBadgeColor", required: false }] }], discountBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "discountBadgeColor", required: false }] }], quantityColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "quantityColor", required: false }] }], specButtonColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonColor", required: false }] }], specButtonTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonTextColor", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], purchasable: [{ type: i0.Input, args: [{ isSignal: true, alias: "purchasable", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], addToCart: [{ type: i0.Output, args: ["addToCart"] }], buyNow: [{ type: i0.Output, args: ["buyNow"] }], wishlist: [{ type: i0.Output, args: ["wishlist"] }], variationChange: [{ type: i0.Output, args: ["variationChange"] }], leaveReview: [{ type: i0.Output, args: ["leaveReview"] }], _carousel: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ClxCarouselComponent), { isSignal: true }] }] } });
|
|
18473
18478
|
|
|
18474
18479
|
class ClxProductQuickViewComponent {
|
|
18475
18480
|
ref = inject(CLX_MODAL_REF, { optional: true });
|
|
@@ -18499,7 +18504,7 @@ class ClxProductQuickViewComponent {
|
|
|
18499
18504
|
(buyNow)="_onBuyNow($event)">
|
|
18500
18505
|
</clx-product-detail>
|
|
18501
18506
|
</clx-modal>
|
|
18502
|
-
`, isInline: true, dependencies: [{ kind: "component", type: ClxModalComponent, selector: "clx-modal", inputs: ["size", "showClose", "confirmButton", "cancelButton", "showCancelButton"], outputs: ["confirmClick", "cancelClick"] }, { kind: "component", type: ClxProductDetailComponent, selector: "clx-product-detail", inputs: ["product", "initialVariationId", "clxColor", "buyNowColor", "buyNowTextColor", "addToCartColor", "addToCartTextColor", "leaveReviewColor", "leaveReviewTextColor", "wishlistColor", "wishlistTextColor", "wishlistVariant", "stockBadgeColor", "discountBadgeColor", "quantityColor", "specButtonColor", "specButtonTextColor", "loading", "currency", "compact", "variant"], outputs: ["addToCart", "buyNow", "wishlist", "variationChange", "leaveReview"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18507
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ClxModalComponent, selector: "clx-modal", inputs: ["size", "showClose", "confirmButton", "cancelButton", "showCancelButton"], outputs: ["confirmClick", "cancelClick"] }, { kind: "component", type: ClxProductDetailComponent, selector: "clx-product-detail", inputs: ["product", "initialVariationId", "clxColor", "buyNowColor", "buyNowTextColor", "addToCartColor", "addToCartTextColor", "leaveReviewColor", "leaveReviewTextColor", "wishlistColor", "wishlistTextColor", "wishlistVariant", "stockBadgeColor", "discountBadgeColor", "quantityColor", "specButtonColor", "specButtonTextColor", "loading", "currency", "compact", "purchasable", "variant"], outputs: ["addToCart", "buyNow", "wishlist", "variationChange", "leaveReview"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18503
18508
|
}
|
|
18504
18509
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductQuickViewComponent, decorators: [{
|
|
18505
18510
|
type: Component,
|