codexly-ui 0.24.0 → 0.25.1

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.
@@ -2221,13 +2221,14 @@ class ClxToastService {
2221
2221
  _injector = inject(Injector);
2222
2222
  // Each position has its own container div at z-index 9000 (above modals)
2223
2223
  _containers = new Map();
2224
+ /** Returns the toast's id — pass it to dismiss() to close it from code. */
2224
2225
  show(options) {
2225
2226
  // Toasts are a browser-only UI concern — during SSR there's no `document` to mount the
2226
2227
  // container into, and no user watching the server's own render to see it anyway. Silently
2227
2228
  // no-op instead of throwing (an uncaught ReferenceError here crashes the whole Node SSR
2228
2229
  // process, taking down every other in-flight request with it).
2229
2230
  if (typeof document === 'undefined')
2230
- return;
2231
+ return '';
2231
2232
  const position = options.position ?? TOAST_DEFAULTS.position;
2232
2233
  const container = this._getOrCreateContainer(position);
2233
2234
  const entry = {
@@ -2253,18 +2254,19 @@ class ClxToastService {
2253
2254
  const isBottom = position.startsWith('bottom');
2254
2255
  const list = container.instance.entries();
2255
2256
  container.instance.entries.set(isBottom ? [...list, entry] : [entry, ...list]);
2257
+ return entry.id;
2256
2258
  }
2257
2259
  success(message, opts = {}) {
2258
- this.show({ ...opts, type: 'green', message });
2260
+ return this.show({ ...opts, type: 'green', message });
2259
2261
  }
2260
2262
  error(message, opts = {}) {
2261
- this.show({ ...opts, type: 'red', message });
2263
+ return this.show({ ...opts, type: 'red', message });
2262
2264
  }
2263
2265
  warning(message, opts = {}) {
2264
- this.show({ ...opts, type: 'amber', message });
2266
+ return this.show({ ...opts, type: 'amber', message });
2265
2267
  }
2266
2268
  info(message, opts = {}) {
2267
- this.show({ ...opts, type: 'sky', message });
2269
+ return this.show({ ...opts, type: 'sky', message });
2268
2270
  }
2269
2271
  /** Shows a toast built straight from a live ClxNotificationItem (e.g. on SSE arrival) — the same
2270
2272
  * object already driving the notification panel/bell, no manual field mapping at the call site. */
@@ -2279,6 +2281,14 @@ class ClxToastService {
2279
2281
  timestamp: new Date(item.createdAt),
2280
2282
  });
2281
2283
  }
2284
+ /** Closes one toast, with its exit animation — e.g. a persistent prompt whose question was already
2285
+ * answered somewhere else. No-op when it's already gone. */
2286
+ dismiss(id) {
2287
+ this._containers.forEach(c => {
2288
+ if (c.instance.entries().some(e => e.id === id))
2289
+ void c.instance.requestDismiss(id);
2290
+ });
2291
+ }
2282
2292
  dismissAll() {
2283
2293
  this._containers.forEach(c => c.instance.entries.set([]));
2284
2294
  }
@@ -13795,6 +13805,172 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
13795
13805
  `, styles: ["clx-alert{display:flex;align-items:center;justify-content:center;width:100%}.clx-alert-card{will-change:transform,opacity}\n"] }]
13796
13806
  }], ctorParameters: () => [] });
13797
13807
 
13808
+ /** Per-item, per-channel on/off settings grouped in cards — notification settings ("avísame por App,
13809
+ * Correo o Push"), but any item × channel matrix fits. Each group's header has one switch per channel
13810
+ * that turns that whole column on or off; an item only shows switches on the channels it really uses
13811
+ * (an unavailable one shows a dash, so the columns stay aligned), and a locked channel stays on with
13812
+ * its reason as tooltip. On phones each item stacks its switches under its text, each with its own
13813
+ * channel label. Stateless: the host applies every (change) and feeds the new state back. */
13814
+ class ClxChannelPreferencesComponent {
13815
+ groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
13816
+ channels = input([], ...(ngDevMode ? [{ debugName: "channels" }] : /* istanbul ignore next */ []));
13817
+ color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
13818
+ variant = input('elevated', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
13819
+ change = output();
13820
+ _channelKeys = computed(() => this.channels().map(c => c.key), ...(ngDevMode ? [{ debugName: "_channelKeys" }] : /* istanbul ignore next */ []));
13821
+ stateOf(item, channel) {
13822
+ return item.channels[channel];
13823
+ }
13824
+ /** Items of the group that can be switched on this channel (available and not locked). */
13825
+ switchable(group, channel) {
13826
+ return group.items.filter(i => i.channels[channel]?.available && !i.channels[channel]?.locked);
13827
+ }
13828
+ groupHasChannel(group, channel) {
13829
+ return this._channelKeys().includes(channel) && this.switchable(group, channel).length > 0;
13830
+ }
13831
+ groupEnabled(group, channel) {
13832
+ return this.switchable(group, channel).every(i => i.channels[channel].enabled);
13833
+ }
13834
+ emitItem(group, item, channel, enabled) {
13835
+ this.change.emit({ group: group.key, item: item.key, channel, enabled });
13836
+ }
13837
+ emitGroup(group, channel, enabled) {
13838
+ this.change.emit({ group: group.key, item: null, channel, enabled });
13839
+ }
13840
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxChannelPreferencesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13841
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxChannelPreferencesComponent, isStandalone: true, selector: "clx-channel-preferences", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, channels: { classPropertyName: "channels", publicName: "channels", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change" }, ngImport: i0, template: `
13842
+ <div class="flex flex-col gap-6">
13843
+ @for (group of groups(); track group.key) {
13844
+ <clx-card [variant]="variant()" [icon]="group.icon" [headerTitle]="group.label">
13845
+ <div clxCardBody class="flex flex-col">
13846
+ <div class="hidden sm:flex items-center justify-end gap-2 pb-3 border-b border-clx-border-soft">
13847
+ @for (channel of channels(); track channel.key) {
13848
+ @if (groupHasChannel(group, channel.key)) {
13849
+ <div class="w-16 flex flex-col items-center gap-1">
13850
+ <span class="text-[11px] font-medium text-clx-text-muted">{{ channel.label }}</span>
13851
+ <label clx-switch size="sm" [color]="color()"
13852
+ [value]="groupEnabled(group, channel.key)"
13853
+ [clxTooltip]="groupEnabled(group, channel.key) ? 'Desactivar todo' : 'Activar todo'"
13854
+ clxTooltipSize="sm"
13855
+ (changed)="emitGroup(group, channel.key, $event)"></label>
13856
+ </div>
13857
+ } @else {
13858
+ <div class="w-16 text-center text-[11px] font-medium text-clx-text-faint">{{ channel.label }}</div>
13859
+ }
13860
+ }
13861
+ </div>
13862
+
13863
+ @for (item of group.items; track item.key; let last = $last) {
13864
+ <div class="flex flex-col sm:flex-row sm:items-center gap-3 py-3"
13865
+ [class.border-b]="!last" [class.border-clx-border-soft]="!last">
13866
+ <div class="flex items-start gap-3 flex-1 min-w-0">
13867
+ @if (item.icon) {
13868
+ <span clx-icon [name]="item.icon" size="sm" class="mt-0.5 text-clx-text-muted"></span>
13869
+ }
13870
+ <div class="min-w-0">
13871
+ <p class="text-sm font-medium text-clx-text-label">{{ item.label }}</p>
13872
+ @if (item.description) {
13873
+ <p class="text-xs text-clx-text-muted mt-0.5">{{ item.description }}</p>
13874
+ }
13875
+ </div>
13876
+ </div>
13877
+ <div class="flex flex-wrap sm:flex-nowrap items-center gap-x-5 gap-y-2 sm:gap-2 pl-8 sm:pl-0 shrink-0">
13878
+ @for (channel of channels(); track channel.key) {
13879
+ @let state = stateOf(item, channel.key);
13880
+ @if (state?.available) {
13881
+ <div class="sm:w-16 flex items-center sm:justify-center gap-2">
13882
+ <label clx-switch size="sm" [color]="color()"
13883
+ [value]="state!.enabled || !!state!.locked"
13884
+ [disabled]="!!state!.locked"
13885
+ [clxTooltip]="state!.locked ? (state!.lockedReason ?? 'Siempre activo') : ''"
13886
+ clxTooltipSize="sm"
13887
+ (changed)="emitItem(group, item, channel.key, $event)"></label>
13888
+ <span class="sm:hidden text-xs text-clx-text-muted">{{ channel.label }}</span>
13889
+ </div>
13890
+ } @else {
13891
+ <span class="hidden sm:block w-16 text-center text-clx-text-faint" aria-hidden="true">—</span>
13892
+ }
13893
+ }
13894
+ </div>
13895
+ </div>
13896
+ }
13897
+ </div>
13898
+ </clx-card>
13899
+ }
13900
+ </div>
13901
+ `, isInline: true, dependencies: [{ kind: "component", type: ClxCardComponent, selector: "clx-card", inputs: ["variant", "color", "padding", "shadow", "hover", "disabled", "border", "radius", "icon", "headerTitle", "headerSubtitle"] }, { kind: "directive", type: ClxCardBodyDirective, selector: "[clxCardBody]" }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSwitchComponent, selector: "label[clx-switch]", inputs: ["color", "size", "label", "value", "disabled"], outputs: ["changed"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13902
+ }
13903
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxChannelPreferencesComponent, decorators: [{
13904
+ type: Component,
13905
+ args: [{
13906
+ selector: 'clx-channel-preferences',
13907
+ standalone: true,
13908
+ imports: [ClxCardComponent, ClxCardBodyDirective, ClxIconComponent, ClxSwitchComponent, ClxTooltipDirective],
13909
+ changeDetection: ChangeDetectionStrategy.OnPush,
13910
+ template: `
13911
+ <div class="flex flex-col gap-6">
13912
+ @for (group of groups(); track group.key) {
13913
+ <clx-card [variant]="variant()" [icon]="group.icon" [headerTitle]="group.label">
13914
+ <div clxCardBody class="flex flex-col">
13915
+ <div class="hidden sm:flex items-center justify-end gap-2 pb-3 border-b border-clx-border-soft">
13916
+ @for (channel of channels(); track channel.key) {
13917
+ @if (groupHasChannel(group, channel.key)) {
13918
+ <div class="w-16 flex flex-col items-center gap-1">
13919
+ <span class="text-[11px] font-medium text-clx-text-muted">{{ channel.label }}</span>
13920
+ <label clx-switch size="sm" [color]="color()"
13921
+ [value]="groupEnabled(group, channel.key)"
13922
+ [clxTooltip]="groupEnabled(group, channel.key) ? 'Desactivar todo' : 'Activar todo'"
13923
+ clxTooltipSize="sm"
13924
+ (changed)="emitGroup(group, channel.key, $event)"></label>
13925
+ </div>
13926
+ } @else {
13927
+ <div class="w-16 text-center text-[11px] font-medium text-clx-text-faint">{{ channel.label }}</div>
13928
+ }
13929
+ }
13930
+ </div>
13931
+
13932
+ @for (item of group.items; track item.key; let last = $last) {
13933
+ <div class="flex flex-col sm:flex-row sm:items-center gap-3 py-3"
13934
+ [class.border-b]="!last" [class.border-clx-border-soft]="!last">
13935
+ <div class="flex items-start gap-3 flex-1 min-w-0">
13936
+ @if (item.icon) {
13937
+ <span clx-icon [name]="item.icon" size="sm" class="mt-0.5 text-clx-text-muted"></span>
13938
+ }
13939
+ <div class="min-w-0">
13940
+ <p class="text-sm font-medium text-clx-text-label">{{ item.label }}</p>
13941
+ @if (item.description) {
13942
+ <p class="text-xs text-clx-text-muted mt-0.5">{{ item.description }}</p>
13943
+ }
13944
+ </div>
13945
+ </div>
13946
+ <div class="flex flex-wrap sm:flex-nowrap items-center gap-x-5 gap-y-2 sm:gap-2 pl-8 sm:pl-0 shrink-0">
13947
+ @for (channel of channels(); track channel.key) {
13948
+ @let state = stateOf(item, channel.key);
13949
+ @if (state?.available) {
13950
+ <div class="sm:w-16 flex items-center sm:justify-center gap-2">
13951
+ <label clx-switch size="sm" [color]="color()"
13952
+ [value]="state!.enabled || !!state!.locked"
13953
+ [disabled]="!!state!.locked"
13954
+ [clxTooltip]="state!.locked ? (state!.lockedReason ?? 'Siempre activo') : ''"
13955
+ clxTooltipSize="sm"
13956
+ (changed)="emitItem(group, item, channel.key, $event)"></label>
13957
+ <span class="sm:hidden text-xs text-clx-text-muted">{{ channel.label }}</span>
13958
+ </div>
13959
+ } @else {
13960
+ <span class="hidden sm:block w-16 text-center text-clx-text-faint" aria-hidden="true">—</span>
13961
+ }
13962
+ }
13963
+ </div>
13964
+ </div>
13965
+ }
13966
+ </div>
13967
+ </clx-card>
13968
+ }
13969
+ </div>
13970
+ `,
13971
+ }]
13972
+ }], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], channels: [{ type: i0.Input, args: [{ isSignal: true, alias: "channels", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], change: [{ type: i0.Output, args: ["change"] }] } });
13973
+
13798
13974
  const SIZE_MAP = {
13799
13975
  sm: { row: 'py-1 px-2', text: 'text-xs', icon: 'xs', indent: 'pl-5' },
13800
13976
  md: { row: 'py-1.5 px-2', text: 'text-sm', icon: 'sm', indent: 'pl-6' },
@@ -17419,6 +17595,14 @@ const PRODUCT_TAG_VARIANT = {
17419
17595
  'limited': 'solid',
17420
17596
  'hot': 'solid',
17421
17597
  };
17598
+ /** How a tag renders without ClxProductTagService: built-in codes get their PRODUCT_TAG_* look,
17599
+ * any other code (a host tag before setTags) shows the code itself, light, in the fallback color. */
17600
+ function productTagLook(tag, fallbackColor) {
17601
+ if (!(tag in PRODUCT_TAG_LABEL))
17602
+ return { label: tag, color: fallbackColor, variant: 'light' };
17603
+ const known = tag;
17604
+ return { label: PRODUCT_TAG_LABEL[known], color: PRODUCT_TAG_COLOR[known], variant: PRODUCT_TAG_VARIANT[known] };
17605
+ }
17422
17606
 
17423
17607
  /**
17424
17608
  * The product tags configured by the host app (setTags, e.g. from its backend at startup). Once set,
@@ -17587,12 +17771,9 @@ class ClxProductComponent {
17587
17771
  addToCart = output();
17588
17772
  addToWishlist = output();
17589
17773
  viewOptions = output();
17590
- PRODUCT_TAG_LABEL = PRODUCT_TAG_LABEL;
17591
- PRODUCT_TAG_COLOR = PRODUCT_TAG_COLOR;
17592
- PRODUCT_TAG_VARIANT = PRODUCT_TAG_VARIANT;
17593
17774
  _h = computed(() => this.layout() === 'horizontal', ...(ngDevMode ? [{ debugName: "_h" }] : /* istanbul ignore next */ []));
17594
17775
  _sm = computed(() => this.size() === 'sm', ...(ngDevMode ? [{ debugName: "_sm" }] : /* istanbul ignore next */ []));
17595
- _tags = computed(() => (this.product()?.tags ?? []), ...(ngDevMode ? [{ debugName: "_tags" }] : /* istanbul ignore next */ []));
17776
+ _tags = computed(() => (this.product()?.tags ?? []).map(code => ({ code, ...productTagLook(code, this.clxColor()) })), ...(ngDevMode ? [{ debugName: "_tags" }] : /* istanbul ignore next */ []));
17596
17777
  _tagSvc = inject(ClxProductTagService);
17597
17778
  _badges = computed(() => {
17598
17779
  const p = this.product();
@@ -17733,14 +17914,14 @@ class ClxProductComponent {
17733
17914
  <clx-product-badges [badges]="_badges()" bottomRightOffset="bottom-14" />
17734
17915
  } @else if (_tags().length) {
17735
17916
  <div class="absolute top-3 left-3 flex flex-col gap-1">
17736
- @for (s of _tags(); track s) {
17917
+ @for (t of _tags(); track t.code) {
17737
17918
  <span
17738
17919
  clx-badge
17739
- [variant]="PRODUCT_TAG_VARIANT[s]"
17740
- [color]="PRODUCT_TAG_COLOR[s]"
17920
+ [variant]="t.variant"
17921
+ [color]="t.color"
17741
17922
  size="sm"
17742
17923
  shape="pill"
17743
- >{{ PRODUCT_TAG_LABEL[s] }}</span>
17924
+ >{{ t.label }}</span>
17744
17925
  }
17745
17926
  </div>
17746
17927
  }
@@ -17873,14 +18054,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
17873
18054
  <clx-product-badges [badges]="_badges()" bottomRightOffset="bottom-14" />
17874
18055
  } @else if (_tags().length) {
17875
18056
  <div class="absolute top-3 left-3 flex flex-col gap-1">
17876
- @for (s of _tags(); track s) {
18057
+ @for (t of _tags(); track t.code) {
17877
18058
  <span
17878
18059
  clx-badge
17879
- [variant]="PRODUCT_TAG_VARIANT[s]"
17880
- [color]="PRODUCT_TAG_COLOR[s]"
18060
+ [variant]="t.variant"
18061
+ [color]="t.color"
17881
18062
  size="sm"
17882
18063
  shape="pill"
17883
- >{{ PRODUCT_TAG_LABEL[s] }}</span>
18064
+ >{{ t.label }}</span>
17884
18065
  }
17885
18066
  </div>
17886
18067
  }
@@ -18145,11 +18326,8 @@ class ClxProductDetailComponent {
18145
18326
  return v?.sale_price ?? p?.product_sale_price ?? 0;
18146
18327
  }, ...(ngDevMode ? [{ debugName: "listPrice" }] : /* istanbul ignore next */ []));
18147
18328
  finalPrice = computed(() => {
18148
- const p = this.product();
18149
18329
  const list = this.listPrice();
18150
- if (!p?.has_discount)
18151
- return list;
18152
- const pct = p.discount_percentage ?? this.discount();
18330
+ const pct = this._discountPct();
18153
18331
  return pct ? list * (1 - pct / 100) : list;
18154
18332
  }, ...(ngDevMode ? [{ debugName: "finalPrice" }] : /* istanbul ignore next */ []));
18155
18333
  /** Was a hardcoded "Very Good" regardless of the actual rating (including 0.0 with no reviews
@@ -18170,36 +18348,28 @@ class ClxProductDetailComponent {
18170
18348
  return 'Regular';
18171
18349
  return 'Deficiente';
18172
18350
  }, ...(ngDevMode ? [{ debugName: "_ratingLabel" }] : /* istanbul ignore next */ []));
18173
- /** p.tags is plain string[] (ClxProductDetailData isn't coupled to ClxProductTag), so lookups
18174
- * guard against an unrecognized value instead of indexing the Record directly. */
18175
- isKnownTag(tag) {
18176
- return tag in PRODUCT_TAG_LABEL;
18177
- }
18178
- tagLabel(tag) {
18179
- return this.isKnownTag(tag) ? PRODUCT_TAG_LABEL[tag] : tag;
18180
- }
18181
- tagColor(tag) {
18182
- return this.isKnownTag(tag) ? PRODUCT_TAG_COLOR[tag] : this.clxColor();
18183
- }
18184
- tagVariant(tag) {
18185
- return this.isKnownTag(tag) ? PRODUCT_TAG_VARIANT[tag] : 'light';
18186
- }
18187
- /** discount_percentage is the primary source (matches this codebase's actual data model: the
18188
- * percentage lives on the product, the price it applies to is sale_price/product_sale_price —
18189
- * see listPrice() above). discount_price is only a fallback for a caller whose backend instead
18190
- * stores a precomputed higher reference price to derive the percentage from. */
18191
- discount = computed(() => {
18351
+ /** The built-in tags (shown until the host configures ClxProductTagService). */
18352
+ _tags = computed(() => (this.product()?.tags ?? []).map(code => ({ code, ...productTagLook(code, this.clxColor()) })), ...(ngDevMode ? [{ debugName: "_tags" }] : /* istanbul ignore next */ []));
18353
+ /** The selected variation's own discount_percentage when the host sends one, else the product's
18354
+ * (it applies on top of the list price — see listPrice() above). discount_price is only a fallback
18355
+ * for a caller whose backend instead stores a precomputed higher reference price to derive the
18356
+ * percentage from. */
18357
+ _discountPct = computed(() => {
18358
+ const v = this.activeVariation();
18359
+ if (v?.discount_percentage !== undefined)
18360
+ return v.discount_percentage;
18192
18361
  const p = this.product();
18193
18362
  if (!p?.has_discount)
18194
18363
  return 0;
18195
18364
  if (p.discount_percentage)
18196
- return Math.round(p.discount_percentage);
18365
+ return p.discount_percentage;
18197
18366
  const list = this.listPrice();
18198
18367
  if (p.discount_price && p.discount_price > list) {
18199
- return Math.round(((p.discount_price - list) / p.discount_price) * 100);
18368
+ return ((p.discount_price - list) / p.discount_price) * 100;
18200
18369
  }
18201
18370
  return 0;
18202
- }, ...(ngDevMode ? [{ debugName: "discount" }] : /* istanbul ignore next */ []));
18371
+ }, ...(ngDevMode ? [{ debugName: "_discountPct" }] : /* istanbul ignore next */ []));
18372
+ discount = computed(() => Math.round(this._discountPct()), ...(ngDevMode ? [{ debugName: "discount" }] : /* istanbul ignore next */ []));
18203
18373
  /** The host's configured tags (ClxProductTagService) for the selected variation, over the gallery —
18204
18374
  * the variation's own tags and settings when the host sends them, else the product's. */
18205
18375
  _badges = computed(() => {
@@ -18212,7 +18382,7 @@ class ClxProductDetailComponent {
18212
18382
  inStock: v.in_stock,
18213
18383
  stockQuantity: v.stock_quantity,
18214
18384
  showStockCount: true,
18215
- discountPercent: p.has_discount ? this.discount() : 0,
18385
+ discountPercent: this.discount(),
18216
18386
  hiddenSystemTags: v.hidden_system_tags ?? p.hidden_system_tags,
18217
18387
  lowStockThreshold: v.low_stock_threshold !== undefined ? v.low_stock_threshold : p.low_stock_threshold,
18218
18388
  });
@@ -18275,7 +18445,7 @@ class ClxProductDetailComponent {
18275
18445
  specsLeft(specs) { return specs.slice(0, Math.ceil(specs.length / 2)); }
18276
18446
  specsRight(specs) { return specs.slice(Math.ceil(specs.length / 2)); }
18277
18447
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18278
- 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 (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", "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 });
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 });
18279
18449
  }
18280
18450
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, decorators: [{
18281
18451
  type: Component,
@@ -18298,7 +18468,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
18298
18468
  ClxTooltipDirective,
18299
18469
  ClxListComponent,
18300
18470
  ClxListItemComponent,
18301
- ], 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 (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" }]
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" }]
18302
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 }] }] } });
18303
18473
 
18304
18474
  class ClxProductQuickViewComponent {
@@ -20375,5 +20545,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
20375
20545
  * Generated bundle index. Do not edit.
20376
20546
  */
20377
20547
 
20378
- export { CLX_ADDON_BORDER, CLX_ADDON_TEXT, CLX_ALERT_OPTIONS, CLX_ALERT_RESOLVE, CLX_BG_ADDON, CLX_BG_DISABLED, CLX_BG_ICON_WRAP, CLX_BG_SECTION, CLX_BG_SURFACE, CLX_BORDER_DEFAULT, CLX_BORDER_DISABLED, CLX_BORDER_MEDIUM, CLX_COLOR_HEX, CLX_COLOR_HEX_100, CLX_COLOR_MAP, CLX_FONT_CATALOG, CLX_MODAL_ANIM_CONFIG, CLX_MODAL_DATA, CLX_MODAL_REF, CLX_OPTION_DISABLED, CLX_PLACEHOLDER, CLX_RADIO_GROUP, CLX_RADIUS_MAP, CLX_TEXT_BODY, CLX_TEXT_DISABLED, CLX_TEXT_HEADING, CLX_TEXT_HINT, CLX_TEXT_IDLE, CLX_TEXT_INPUT, CLX_TEXT_LABEL, CLX_TEXT_OPTION, CLX_TEXT_SUBTITLE, CLX_TEXT_TITLE, CLX_THEME_CONFIG, CLX_THEME_DEFAULTS, CLX_TOAST_DEFAULTS, ClxAlertComponent, ClxAlertListComponent, ClxAlertService, ClxAnimateDirective, ClxAnimateGroupDirective, ClxAnimateService, ClxAppLayoutComponent, ClxAvatarComponent, ClxBadgeComponent, ClxBrandComponent, ClxButtonComponent, ClxButtonGroupComponent, ClxCardBodyDirective, ClxCardComponent, ClxCardExpiryInputComponent, ClxCardFooterDirective, ClxCardHeaderActionsDirective, ClxCardHeaderDirective, ClxCardNumberInputComponent, ClxCardValidators, ClxCarouselComponent, ClxCarouselDirective, ClxCartComponent, ClxCartSummaryDrawer, ClxCellDirective, ClxCheckboxComponent, ClxCollapseComponent, ClxColorPickerComponent, ClxColumnDefDirective, ClxCopyTextComponent, ClxDateRangePickerComponent, ClxDatepickerComponent, ClxDrawerComponent, ClxDrawerService, ClxEditorComponent, ClxEditorImageModalComponent, ClxEditorLinkModalComponent, ClxFabComponent, ClxFilterPanelComponent, ClxHeaderCellDirective, ClxIconComponent, ClxInputComponent, ClxListComponent, ClxListItemComponent, ClxMaintenanceBannerComponent, ClxMenuComponent, ClxMenuItemComponent, ClxMenuItemTrailingDirective, ClxModalComponent, ClxModalService, ClxNativeOverlayService, ClxNavGroupComponent, ClxNotificationComponent, ClxNumberComponent, ClxOtpComponent, ClxPageEmptyComponent, ClxPageHeaderComponent, ClxPageHeaderTitleDirective, ClxPageMaintenanceComponent, ClxPageNotFoundComponent, ClxPageServerErrorComponent, ClxPageUnauthorizedComponent, ClxPaginationComponent, ClxProductBadgesComponent, ClxProductComponent, ClxProductDetailComponent, ClxProductQuickViewComponent, ClxProductTagService, ClxProfileComponent, ClxProgressBarComponent, ClxRadioComponent, ClxRadioGroupComponent, ClxRankedListComponent, ClxRatingComponent, ClxSearchComponent, ClxSelectComponent, ClxSkeletonComponent, ClxSliderComponent, ClxSocialIconComponent, ClxSpinnerComponent, ClxStatCardComponent, ClxStepComponent, ClxStepperComponent, ClxSwitchComponent, ClxTabDirective, ClxTableActionsComponent, ClxTableComponent, ClxTableProductCellComponent, ClxTableProgressCellComponent, ClxTableStatusCellComponent, ClxTabsComponent, ClxTagComponent, ClxTextareaComponent, ClxThemeService, ClxTimelineComponent, ClxTimelineItemComponent, ClxTimepickerComponent, ClxToastComponent, ClxToastContainerComponent, ClxToastService, ClxTooltipComponent, ClxTooltipDirective, ClxTreeComponent, ClxUploadComponent, ClxWidgetCardComponent, ClxWishlistComponent, ClxWizardComponent, TIMEPICKER_SIZE_MAP, detectClxCardBrand, formatClxTime, formatClxWindow, isClxCardExpired, parseColorInput, provideCodexlyTheme, resolveColor, resolveContainerRadius, resolveRadius };
20548
+ export { CLX_ADDON_BORDER, CLX_ADDON_TEXT, CLX_ALERT_OPTIONS, CLX_ALERT_RESOLVE, CLX_BG_ADDON, CLX_BG_DISABLED, CLX_BG_ICON_WRAP, CLX_BG_SECTION, CLX_BG_SURFACE, CLX_BORDER_DEFAULT, CLX_BORDER_DISABLED, CLX_BORDER_MEDIUM, CLX_COLOR_HEX, CLX_COLOR_HEX_100, CLX_COLOR_MAP, CLX_FONT_CATALOG, CLX_MODAL_ANIM_CONFIG, CLX_MODAL_DATA, CLX_MODAL_REF, CLX_OPTION_DISABLED, CLX_PLACEHOLDER, CLX_RADIO_GROUP, CLX_RADIUS_MAP, CLX_TEXT_BODY, CLX_TEXT_DISABLED, CLX_TEXT_HEADING, CLX_TEXT_HINT, CLX_TEXT_IDLE, CLX_TEXT_INPUT, CLX_TEXT_LABEL, CLX_TEXT_OPTION, CLX_TEXT_SUBTITLE, CLX_TEXT_TITLE, CLX_THEME_CONFIG, CLX_THEME_DEFAULTS, CLX_TOAST_DEFAULTS, ClxAlertComponent, ClxAlertListComponent, ClxAlertService, ClxAnimateDirective, ClxAnimateGroupDirective, ClxAnimateService, ClxAppLayoutComponent, ClxAvatarComponent, ClxBadgeComponent, ClxBrandComponent, ClxButtonComponent, ClxButtonGroupComponent, ClxCardBodyDirective, ClxCardComponent, ClxCardExpiryInputComponent, ClxCardFooterDirective, ClxCardHeaderActionsDirective, ClxCardHeaderDirective, ClxCardNumberInputComponent, ClxCardValidators, ClxCarouselComponent, ClxCarouselDirective, ClxCartComponent, ClxCartSummaryDrawer, ClxCellDirective, ClxChannelPreferencesComponent, ClxCheckboxComponent, ClxCollapseComponent, ClxColorPickerComponent, ClxColumnDefDirective, ClxCopyTextComponent, ClxDateRangePickerComponent, ClxDatepickerComponent, ClxDrawerComponent, ClxDrawerService, ClxEditorComponent, ClxEditorImageModalComponent, ClxEditorLinkModalComponent, ClxFabComponent, ClxFilterPanelComponent, ClxHeaderCellDirective, ClxIconComponent, ClxInputComponent, ClxListComponent, ClxListItemComponent, ClxMaintenanceBannerComponent, ClxMenuComponent, ClxMenuItemComponent, ClxMenuItemTrailingDirective, ClxModalComponent, ClxModalService, ClxNativeOverlayService, ClxNavGroupComponent, ClxNotificationComponent, ClxNumberComponent, ClxOtpComponent, ClxPageEmptyComponent, ClxPageHeaderComponent, ClxPageHeaderTitleDirective, ClxPageMaintenanceComponent, ClxPageNotFoundComponent, ClxPageServerErrorComponent, ClxPageUnauthorizedComponent, ClxPaginationComponent, ClxProductBadgesComponent, ClxProductComponent, ClxProductDetailComponent, ClxProductQuickViewComponent, ClxProductTagService, ClxProfileComponent, ClxProgressBarComponent, ClxRadioComponent, ClxRadioGroupComponent, ClxRankedListComponent, ClxRatingComponent, ClxSearchComponent, ClxSelectComponent, ClxSkeletonComponent, ClxSliderComponent, ClxSocialIconComponent, ClxSpinnerComponent, ClxStatCardComponent, ClxStepComponent, ClxStepperComponent, ClxSwitchComponent, ClxTabDirective, ClxTableActionsComponent, ClxTableComponent, ClxTableProductCellComponent, ClxTableProgressCellComponent, ClxTableStatusCellComponent, ClxTabsComponent, ClxTagComponent, ClxTextareaComponent, ClxThemeService, ClxTimelineComponent, ClxTimelineItemComponent, ClxTimepickerComponent, ClxToastComponent, ClxToastContainerComponent, ClxToastService, ClxTooltipComponent, ClxTooltipDirective, ClxTreeComponent, ClxUploadComponent, ClxWidgetCardComponent, ClxWishlistComponent, ClxWizardComponent, TIMEPICKER_SIZE_MAP, detectClxCardBrand, formatClxTime, formatClxWindow, isClxCardExpired, parseColorInput, provideCodexlyTheme, resolveColor, resolveContainerRadius, resolveRadius };
20379
20549
  //# sourceMappingURL=codexly-ui.mjs.map