codexly-ui 0.11.8 → 0.12.0

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.
@@ -627,6 +627,16 @@ class ClxThemeService {
627
627
  bordered: t?.bordered ?? true,
628
628
  };
629
629
  }, ...(ngDevMode ? [{ debugName: "paginationStyle" }] : /* istanbul ignore next */ []));
630
+ /** Resolved style for ClxTabsComponent — activeTextColor and underlineColor each fall back to
631
+ * primaryColor independently when unset. Per-instance inputs on clx-tabs still take
632
+ * precedence over this theme-wide default. */
633
+ tabsStyle = computed(() => {
634
+ const t = this._config().tabs;
635
+ return {
636
+ activeTextColor: t?.activeTextColor ?? this._config().primaryColor,
637
+ underlineColor: t?.underlineColor ?? this._config().primaryColor,
638
+ };
639
+ }, ...(ngDevMode ? [{ debugName: "tabsStyle" }] : /* istanbul ignore next */ []));
630
640
  /** Resolved style for ClxNotificationComponent — severity colors fall back to built-in
631
641
  * green/amber/sky/red, unreadColor to primaryColor, borderColor to listBorderColor then
632
642
  * primaryColor, radius to the app-wide borderRadius. Per-instance inputs on clx-notification
@@ -9600,20 +9610,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
9600
9610
  class ClxTabsComponent {
9601
9611
  _themeSvc = inject(ClxThemeService);
9602
9612
  tabs = input.required(...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
9613
+ /** Legacy single-color override — still the shared fallback for both activeTextColor and
9614
+ * underlineColor when neither of those is set, so existing callers keep working unchanged. */
9603
9615
  clxColor = input(undefined, ...(ngDevMode ? [{ debugName: "clxColor" }] : /* istanbul ignore next */ []));
9616
+ /** Active tab's text/icon/badge color — falls back to clxColor, then the theme's
9617
+ * tabs.activeTextColor (then primaryColor), when unset. */
9618
+ activeTextColor = input(undefined, ...(ngDevMode ? [{ debugName: "activeTextColor" }] : /* istanbul ignore next */ []));
9619
+ /** Active tab's bottom underline color — independent of activeTextColor, falls back to
9620
+ * clxColor, then the theme's tabs.underlineColor (then primaryColor), when unset. */
9621
+ underlineColor = input(undefined, ...(ngDevMode ? [{ debugName: "underlineColor" }] : /* istanbul ignore next */ []));
9604
9622
  activeTab = model('', ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
9605
- _clxColor = computed(() => this.clxColor() ?? this._themeSvc.config().primaryColor, ...(ngDevMode ? [{ debugName: "_clxColor" }] : /* istanbul ignore next */ []));
9623
+ _style = this._themeSvc.tabsStyle;
9624
+ _activeTextColor = computed(() => this.activeTextColor() ?? this.clxColor() ?? this._style().activeTextColor, ...(ngDevMode ? [{ debugName: "_activeTextColor" }] : /* istanbul ignore next */ []));
9625
+ _underlineColorResolved = computed(() => this.underlineColor() ?? this.clxColor() ?? this._style().underlineColor, ...(ngDevMode ? [{ debugName: "_underlineColorResolved" }] : /* istanbul ignore next */ []));
9606
9626
  _panels = contentChildren(ClxTabDirective, ...(ngDevMode ? [{ debugName: "_panels" }] : /* istanbul ignore next */ []));
9607
- _underlineClass = computed(() => resolveColor(this._clxColor()).bg, ...(ngDevMode ? [{ debugName: "_underlineClass" }] : /* istanbul ignore next */ []));
9627
+ _underlineClass = computed(() => resolveColor(this._underlineColorResolved()).bg, ...(ngDevMode ? [{ debugName: "_underlineClass" }] : /* istanbul ignore next */ []));
9608
9628
  _badgeClass(tabId) {
9609
9629
  const active = this.activeTab() === tabId;
9610
- const t = resolveColor(this._clxColor());
9630
+ const t = resolveColor(this._activeTextColor());
9611
9631
  return active
9612
9632
  ? `ml-0.5 px-1.5 py-0.5 rounded-full text-[10px] font-bold leading-none tabular-nums ${t.bgSubtle} ${t.textSubtle}`
9613
9633
  : 'ml-0.5 px-1.5 py-0.5 rounded-full text-[10px] font-bold leading-none tabular-nums bg-clx-surface-2 text-clx-text-subtle';
9614
9634
  }
9615
9635
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9616
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTabsComponent, isStandalone: true, selector: "clx-tabs", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: true, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeTab: "activeTabChange" }, queries: [{ propertyName: "_panels", predicate: ClxTabDirective, isSignal: true }], ngImport: i0, template: `
9636
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxTabsComponent, isStandalone: true, selector: "clx-tabs", inputs: { tabs: { classPropertyName: "tabs", publicName: "tabs", isSignal: true, isRequired: true, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, activeTextColor: { classPropertyName: "activeTextColor", publicName: "activeTextColor", isSignal: true, isRequired: false, transformFunction: null }, underlineColor: { classPropertyName: "underlineColor", publicName: "underlineColor", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeTab: "activeTabChange" }, queries: [{ propertyName: "_panels", predicate: ClxTabDirective, isSignal: true }], ngImport: i0, template: `
9617
9637
  <!-- Tab bar -->
9618
9638
  <div class="flex flex-wrap items-end border-b border-clx-border gap-3" role="tablist">
9619
9639
  @for (tab of tabs(); track tab.id) {
@@ -9622,7 +9642,7 @@ class ClxTabsComponent {
9622
9642
  type="button"
9623
9643
  role="tab"
9624
9644
  variant="ghost"
9625
- [color]="activeTab() === tab.id ? _clxColor() : 'slate'"
9645
+ [color]="activeTab() === tab.id ? _activeTextColor() : 'slate'"
9626
9646
  size="md"
9627
9647
  shape="rounded"
9628
9648
  [disabled]="tab.disabled ?? false"
@@ -9666,7 +9686,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
9666
9686
  type="button"
9667
9687
  role="tab"
9668
9688
  variant="ghost"
9669
- [color]="activeTab() === tab.id ? _clxColor() : 'slate'"
9689
+ [color]="activeTab() === tab.id ? _activeTextColor() : 'slate'"
9670
9690
  size="md"
9671
9691
  shape="rounded"
9672
9692
  [disabled]="tab.disabled ?? false"
@@ -9698,7 +9718,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
9698
9718
  }
9699
9719
  </div>
9700
9720
  `, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["clx-tabs{display:block}\n"] }]
9701
- }], propDecorators: { tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: true }] }], clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], activeTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTab", required: false }] }, { type: i0.Output, args: ["activeTabChange"] }], _panels: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ClxTabDirective), { isSignal: true }] }] } });
9721
+ }], propDecorators: { tabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabs", required: true }] }], clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], activeTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTextColor", required: false }] }], underlineColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "underlineColor", required: false }] }], activeTab: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeTab", required: false }] }, { type: i0.Output, args: ["activeTabChange"] }], _panels: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => ClxTabDirective), { isSignal: true }] }] } });
9702
9722
 
9703
9723
  class ClxWizardComponent {
9704
9724
  _themeSvc = inject(ClxThemeService);
@@ -9922,7 +9942,7 @@ class ClxWizardComponent {
9922
9942
  </div>
9923
9943
  </div>
9924
9944
  </div>
9925
- `, isInline: true, styles: ["@keyframes clx-wizard-fade-slide{0%{opacity:0;transform:translate(10px)}to{opacity:1;transform:translate(0)}}.clx-wizard-step-enter{animation:clx-wizard-fade-slide .22s cubic-bezier(.4,0,.2,1) both}@keyframes clx-wizard-fill{0%{width:0%}to{width:100%}}.clx-wizard-connector-fill{animation:clx-wizard-fill .3s ease both}clx-step[data-active=false]{display:none}\n"], dependencies: [{ kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTab"], outputs: ["activeTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9945
+ `, isInline: true, styles: ["@keyframes clx-wizard-fade-slide{0%{opacity:0;transform:translate(10px)}to{opacity:1;transform:translate(0)}}.clx-wizard-step-enter{animation:clx-wizard-fade-slide .22s cubic-bezier(.4,0,.2,1) both}@keyframes clx-wizard-fill{0%{width:0%}to{width:100%}}.clx-wizard-connector-fill{animation:clx-wizard-fill .3s ease both}clx-step[data-active=false]{display:none}\n"], dependencies: [{ kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
9926
9946
  }
9927
9947
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxWizardComponent, decorators: [{
9928
9948
  type: Component,
@@ -11601,7 +11621,7 @@ class ClxNotificationComponent {
11601
11621
  }
11602
11622
  </div>
11603
11623
  </ng-template>
11604
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxPageEmptyComponent, selector: "clx-page-empty", inputs: ["icon", "heading", "description", "ctaLabel", "ctaIcon", "ctaColor"], outputs: ["cta"] }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTab"], outputs: ["activeTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11624
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: OverlayModule }, { kind: "directive", type: i1.CdkConnectedOverlay, selector: "[cdk-connected-overlay], [connected-overlay], [cdkConnectedOverlay]", inputs: ["cdkConnectedOverlayOrigin", "cdkConnectedOverlayPositions", "cdkConnectedOverlayPositionStrategy", "cdkConnectedOverlayOffsetX", "cdkConnectedOverlayOffsetY", "cdkConnectedOverlayWidth", "cdkConnectedOverlayHeight", "cdkConnectedOverlayMinWidth", "cdkConnectedOverlayMinHeight", "cdkConnectedOverlayBackdropClass", "cdkConnectedOverlayPanelClass", "cdkConnectedOverlayViewportMargin", "cdkConnectedOverlayScrollStrategy", "cdkConnectedOverlayOpen", "cdkConnectedOverlayDisableClose", "cdkConnectedOverlayTransformOriginOn", "cdkConnectedOverlayHasBackdrop", "cdkConnectedOverlayLockPosition", "cdkConnectedOverlayFlexibleDimensions", "cdkConnectedOverlayGrowAfterOpen", "cdkConnectedOverlayPush", "cdkConnectedOverlayDisposeOnNavigation"], outputs: ["backdropClick", "positionChange", "attach", "detach", "overlayKeydown", "overlayOutsideClick"], exportAs: ["cdkConnectedOverlay"] }, { kind: "directive", type: i1.CdkOverlayOrigin, selector: "[cdk-overlay-origin], [overlay-origin], [cdkOverlayOrigin]", exportAs: ["cdkOverlayOrigin"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxPageEmptyComponent, selector: "clx-page-empty", inputs: ["icon", "heading", "description", "ctaLabel", "ctaIcon", "ctaColor"], outputs: ["cta"] }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
11605
11625
  }
11606
11626
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxNotificationComponent, decorators: [{
11607
11627
  type: Component,
@@ -12076,33 +12096,85 @@ const TOAST_DEFAULT_ICON = {
12076
12096
  white: 'info',
12077
12097
  };
12078
12098
  const c = CLX_COLOR_MAP;
12079
- // ─── Border color per type ─────────────────────────────────────────────────────
12099
+ // ─── Card background (tinted surface) per type ────────────────────────────────
12100
+ const TOAST_BG_MAP = {
12101
+ red: c.red.bgSubtle,
12102
+ orange: c.orange.bgSubtle,
12103
+ amber: c.amber.bgSubtle,
12104
+ yellow: c.yellow.bgSubtle,
12105
+ lime: c.lime.bgSubtle,
12106
+ green: c.green.bgSubtle,
12107
+ emerald: c.emerald.bgSubtle,
12108
+ teal: c.teal.bgSubtle,
12109
+ cyan: c.cyan.bgSubtle,
12110
+ sky: c.sky.bgSubtle,
12111
+ blue: c.blue.bgSubtle,
12112
+ indigo: c.indigo.bgSubtle,
12113
+ violet: c.violet.bgSubtle,
12114
+ purple: c.purple.bgSubtle,
12115
+ fuchsia: c.fuchsia.bgSubtle,
12116
+ pink: c.pink.bgSubtle,
12117
+ rose: c.rose.bgSubtle,
12118
+ slate: c.slate.bgSubtle,
12119
+ gray: c.gray.bgSubtle,
12120
+ zinc: c.zinc.bgSubtle,
12121
+ neutral: c.neutral.bgSubtle,
12122
+ stone: c.stone.bgSubtle,
12123
+ white: c.white.bgSubtle,
12124
+ };
12125
+ // ─── Card border color per type ───────────────────────────────────────────────
12080
12126
  const TOAST_BORDER_MAP = {
12081
- red: c.red.borderBase,
12082
- orange: c.orange.borderBase,
12083
- amber: c.amber.borderBase,
12084
- yellow: c.yellow.borderBase,
12085
- lime: c.lime.borderBase,
12086
- green: c.green.borderBase,
12087
- emerald: c.emerald.borderBase,
12088
- teal: c.teal.borderBase,
12089
- cyan: c.cyan.borderBase,
12090
- sky: c.sky.borderBase,
12091
- blue: c.blue.borderBase,
12092
- indigo: c.indigo.borderBase,
12093
- violet: c.violet.borderBase,
12094
- purple: c.purple.borderBase,
12095
- fuchsia: c.fuchsia.borderBase,
12096
- pink: c.pink.borderBase,
12097
- rose: c.rose.borderBase,
12098
- slate: c.slate.borderBase,
12099
- gray: c.gray.borderBase,
12100
- zinc: c.zinc.borderBase,
12101
- neutral: c.neutral.borderBase,
12102
- stone: c.stone.borderBase,
12103
- white: c.white.borderBase,
12127
+ red: c.red.borderLight,
12128
+ orange: c.orange.borderLight,
12129
+ amber: c.amber.borderLight,
12130
+ yellow: c.yellow.borderLight,
12131
+ lime: c.lime.borderLight,
12132
+ green: c.green.borderLight,
12133
+ emerald: c.emerald.borderLight,
12134
+ teal: c.teal.borderLight,
12135
+ cyan: c.cyan.borderLight,
12136
+ sky: c.sky.borderLight,
12137
+ blue: c.blue.borderLight,
12138
+ indigo: c.indigo.borderLight,
12139
+ violet: c.violet.borderLight,
12140
+ purple: c.purple.borderLight,
12141
+ fuchsia: c.fuchsia.borderLight,
12142
+ pink: c.pink.borderLight,
12143
+ rose: c.rose.borderLight,
12144
+ slate: c.slate.borderLight,
12145
+ gray: c.gray.borderLight,
12146
+ zinc: c.zinc.borderLight,
12147
+ neutral: c.neutral.borderLight,
12148
+ stone: c.stone.borderLight,
12149
+ white: c.white.borderLight,
12150
+ };
12151
+ // ─── Icon disc background per type (solid, paired with white icon glyph) ─────
12152
+ const TOAST_ICON_BG_MAP = {
12153
+ red: c.red.bgBase,
12154
+ orange: c.orange.bgBase,
12155
+ amber: c.amber.bgBase,
12156
+ yellow: c.yellow.bgBase,
12157
+ lime: c.lime.bgBase,
12158
+ green: c.green.bgBase,
12159
+ emerald: c.emerald.bgBase,
12160
+ teal: c.teal.bgBase,
12161
+ cyan: c.cyan.bgBase,
12162
+ sky: c.sky.bgBase,
12163
+ blue: c.blue.bgBase,
12164
+ indigo: c.indigo.bgBase,
12165
+ violet: c.violet.bgBase,
12166
+ purple: c.purple.bgBase,
12167
+ fuchsia: c.fuchsia.bgBase,
12168
+ pink: c.pink.bgBase,
12169
+ rose: c.rose.bgBase,
12170
+ slate: c.slate.bgBase,
12171
+ gray: c.gray.bgBase,
12172
+ zinc: c.zinc.bgBase,
12173
+ neutral: c.neutral.bgBase,
12174
+ stone: c.stone.bgBase,
12175
+ white: c.white.bgBase,
12104
12176
  };
12105
- // ─── Icon / title text color per type ─────────────────────────────────────────
12177
+ // ─── Title / accent text color per type ───────────────────────────────────────
12106
12178
  const TOAST_COLOR_MAP = {
12107
12179
  red: c.red.textBase,
12108
12180
  orange: c.orange.textBase,
@@ -12181,11 +12253,29 @@ class ClxToastComponent {
12181
12253
  _destroyed = false;
12182
12254
  // ─── Derived visuals ──────────────────────────────────────────────────────
12183
12255
  _icon = computed(() => this.entry().icon ?? TOAST_DEFAULT_ICON[this.entry().type], ...(ngDevMode ? [{ debugName: "_icon" }] : /* istanbul ignore next */ []));
12256
+ _bgClass = computed(() => TOAST_BG_MAP[this.entry().type], ...(ngDevMode ? [{ debugName: "_bgClass" }] : /* istanbul ignore next */ []));
12184
12257
  _borderClass = computed(() => TOAST_BORDER_MAP[this.entry().type], ...(ngDevMode ? [{ debugName: "_borderClass" }] : /* istanbul ignore next */ []));
12258
+ _iconBgClass = computed(() => TOAST_ICON_BG_MAP[this.entry().type], ...(ngDevMode ? [{ debugName: "_iconBgClass" }] : /* istanbul ignore next */ []));
12185
12259
  _colorClass = computed(() => TOAST_COLOR_MAP[this.entry().type], ...(ngDevMode ? [{ debugName: "_colorClass" }] : /* istanbul ignore next */ []));
12186
12260
  _progressColor = computed(() => TOAST_PROGRESS_MAP[this.entry().type], ...(ngDevMode ? [{ debugName: "_progressColor" }] : /* istanbul ignore next */ []));
12187
12261
  _buttonColor = computed(() => this.entry().type, ...(ngDevMode ? [{ debugName: "_buttonColor" }] : /* istanbul ignore next */ []));
12188
12262
  _actions = computed(() => this.entry().actions ?? (this.entry().action ? [this.entry().action] : []), ...(ngDevMode ? [{ debugName: "_actions" }] : /* istanbul ignore next */ []));
12263
+ _relativeTime = computed(() => {
12264
+ const ts = this.entry().timestamp;
12265
+ if (!ts)
12266
+ return '';
12267
+ const diffMs = Date.now() - ts.getTime();
12268
+ const min = Math.round(diffMs / 60000);
12269
+ if (min < 1)
12270
+ return 'ahora';
12271
+ if (min < 60)
12272
+ return `hace ${min} min`;
12273
+ const hr = Math.round(min / 60);
12274
+ if (hr < 24)
12275
+ return `hace ${hr} h`;
12276
+ const day = Math.round(hr / 24);
12277
+ return `hace ${day} d`;
12278
+ }, ...(ngDevMode ? [{ debugName: "_relativeTime" }] : /* istanbul ignore next */ []));
12189
12279
  get _card() {
12190
12280
  return this._el.nativeElement.querySelector('.clx-toast-card');
12191
12281
  }
@@ -12262,40 +12352,57 @@ class ClxToastComponent {
12262
12352
  <!-- Toast card -->
12263
12353
  <div
12264
12354
  #card
12265
- class="clx-toast-card relative flex items-start gap-3 w-full bg-clx-surface backdrop-blur-md shadow-lg rounded-lg border-2 px-4 py-3.5 overflow-hidden"
12266
- [class]="_borderClass()"
12355
+ class="clx-toast-card relative flex items-start gap-3.5 w-full backdrop-blur-md shadow-lg rounded-xl border px-4 py-3.5 overflow-hidden"
12356
+ [class]="_bgClass() + ' ' + _borderClass()"
12267
12357
  (mouseenter)="_onMouseEnter()"
12268
12358
  (mouseleave)="_onMouseLeave()"
12269
12359
  >
12270
- <!-- Icon -->
12360
+ <!-- Icon disc -->
12271
12361
  <span
12272
- clx-icon
12273
- [name]="_icon()"
12274
- size="sm"
12275
- class="shrink-0 mt-0.5"
12276
- [class]="_colorClass()"
12277
- ></span>
12362
+ class="shrink-0 w-8 h-8 rounded-full flex items-center justify-center"
12363
+ [class]="_iconBgClass()"
12364
+ >
12365
+ <span clx-icon [name]="_icon()" size="sm" class="text-white"></span>
12366
+ </span>
12278
12367
 
12279
12368
  <!-- Content -->
12280
- <div class="flex-1 min-w-0">
12369
+ <div class="flex-1 min-w-0 pt-0.5">
12370
+ @if (entry().category) {
12371
+ <div class="flex items-center gap-1.5 mb-0.5">
12372
+ <span class="text-[11px] font-bold uppercase tracking-wide" [class]="_colorClass()">
12373
+ {{ entry().category }}
12374
+ </span>
12375
+ @if (entry().timestamp) {
12376
+ <span class="w-0.5 h-0.5 rounded-full bg-clx-text-subtle opacity-60"></span>
12377
+ <span class="text-[11px] text-clx-text-subtle">{{ _relativeTime() }}</span>
12378
+ }
12379
+ </div>
12380
+ }
12281
12381
  @if (entry().title) {
12282
- <p class="text-sm font-semibold leading-snug mb-0.5" [class]="_colorClass()">
12382
+ <p class="text-sm font-semibold leading-snug mb-0.5 flex items-center gap-1.5 flex-wrap" [class]="_colorClass()">
12283
12383
  {{ entry().title }}
12384
+ @if (entry().badgeLabel) {
12385
+ <span
12386
+ class="text-[10px] font-bold uppercase tracking-wide px-1.5 py-0.5 rounded text-white"
12387
+ [class]="_iconBgClass()"
12388
+ >
12389
+ {{ entry().badgeLabel }}
12390
+ </span>
12391
+ }
12284
12392
  </p>
12285
12393
  }
12286
- <p class="text-sm text-clx-text-muted leading-snug wrap-break-word">
12394
+ <p class="text-sm text-clx-text leading-snug wrap-break-word opacity-80">
12287
12395
  {{ entry().message }}
12288
12396
  </p>
12289
12397
  @if (_actions().length) {
12290
- <div class="flex items-center gap-3 mt-2">
12291
- @for (a of _actions(); track a.label) {
12398
+ <div class="flex items-center gap-2 mt-2.5">
12399
+ @for (a of _actions(); track a.label; let first = $first) {
12292
12400
  <button
12293
12401
  clx-button
12294
12402
  type="button"
12295
- [variant]="a.variant ?? 'link'"
12403
+ [variant]="(a.variant ?? (first ? 'primary' : 'ghost')) === 'primary' ? 'solid' : 'ghost'"
12296
12404
  [color]="_buttonColor()"
12297
12405
  size="xs"
12298
- [class]="a.variant && a.variant !== 'link' ? '' : '-ml-1 h-auto p-0'"
12299
12406
  (click)="_onAction(a)"
12300
12407
  >
12301
12408
  {{ a.label }}
@@ -12320,9 +12427,9 @@ class ClxToastComponent {
12320
12427
 
12321
12428
  <!-- Progress bar -->
12322
12429
  @if (entry().showProgress && entry().duration > 0) {
12323
- <div class="absolute bottom-0 left-0 right-0 h-0.5 bg-clx-surface-2">
12430
+ <div class="absolute bottom-0 left-px right-px h-0.5 overflow-hidden rounded-b-[11px]">
12324
12431
  <div
12325
- class="h-full w-full origin-left transition-none"
12432
+ class="h-full w-full origin-left transition-none opacity-45"
12326
12433
  [class]="_progressColor()"
12327
12434
  [ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }"
12328
12435
  ></div>
@@ -12337,40 +12444,57 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
12337
12444
  <!-- Toast card -->
12338
12445
  <div
12339
12446
  #card
12340
- class="clx-toast-card relative flex items-start gap-3 w-full bg-clx-surface backdrop-blur-md shadow-lg rounded-lg border-2 px-4 py-3.5 overflow-hidden"
12341
- [class]="_borderClass()"
12447
+ class="clx-toast-card relative flex items-start gap-3.5 w-full backdrop-blur-md shadow-lg rounded-xl border px-4 py-3.5 overflow-hidden"
12448
+ [class]="_bgClass() + ' ' + _borderClass()"
12342
12449
  (mouseenter)="_onMouseEnter()"
12343
12450
  (mouseleave)="_onMouseLeave()"
12344
12451
  >
12345
- <!-- Icon -->
12452
+ <!-- Icon disc -->
12346
12453
  <span
12347
- clx-icon
12348
- [name]="_icon()"
12349
- size="sm"
12350
- class="shrink-0 mt-0.5"
12351
- [class]="_colorClass()"
12352
- ></span>
12454
+ class="shrink-0 w-8 h-8 rounded-full flex items-center justify-center"
12455
+ [class]="_iconBgClass()"
12456
+ >
12457
+ <span clx-icon [name]="_icon()" size="sm" class="text-white"></span>
12458
+ </span>
12353
12459
 
12354
12460
  <!-- Content -->
12355
- <div class="flex-1 min-w-0">
12461
+ <div class="flex-1 min-w-0 pt-0.5">
12462
+ @if (entry().category) {
12463
+ <div class="flex items-center gap-1.5 mb-0.5">
12464
+ <span class="text-[11px] font-bold uppercase tracking-wide" [class]="_colorClass()">
12465
+ {{ entry().category }}
12466
+ </span>
12467
+ @if (entry().timestamp) {
12468
+ <span class="w-0.5 h-0.5 rounded-full bg-clx-text-subtle opacity-60"></span>
12469
+ <span class="text-[11px] text-clx-text-subtle">{{ _relativeTime() }}</span>
12470
+ }
12471
+ </div>
12472
+ }
12356
12473
  @if (entry().title) {
12357
- <p class="text-sm font-semibold leading-snug mb-0.5" [class]="_colorClass()">
12474
+ <p class="text-sm font-semibold leading-snug mb-0.5 flex items-center gap-1.5 flex-wrap" [class]="_colorClass()">
12358
12475
  {{ entry().title }}
12476
+ @if (entry().badgeLabel) {
12477
+ <span
12478
+ class="text-[10px] font-bold uppercase tracking-wide px-1.5 py-0.5 rounded text-white"
12479
+ [class]="_iconBgClass()"
12480
+ >
12481
+ {{ entry().badgeLabel }}
12482
+ </span>
12483
+ }
12359
12484
  </p>
12360
12485
  }
12361
- <p class="text-sm text-clx-text-muted leading-snug wrap-break-word">
12486
+ <p class="text-sm text-clx-text leading-snug wrap-break-word opacity-80">
12362
12487
  {{ entry().message }}
12363
12488
  </p>
12364
12489
  @if (_actions().length) {
12365
- <div class="flex items-center gap-3 mt-2">
12366
- @for (a of _actions(); track a.label) {
12490
+ <div class="flex items-center gap-2 mt-2.5">
12491
+ @for (a of _actions(); track a.label; let first = $first) {
12367
12492
  <button
12368
12493
  clx-button
12369
12494
  type="button"
12370
- [variant]="a.variant ?? 'link'"
12495
+ [variant]="(a.variant ?? (first ? 'primary' : 'ghost')) === 'primary' ? 'solid' : 'ghost'"
12371
12496
  [color]="_buttonColor()"
12372
12497
  size="xs"
12373
- [class]="a.variant && a.variant !== 'link' ? '' : '-ml-1 h-auto p-0'"
12374
12498
  (click)="_onAction(a)"
12375
12499
  >
12376
12500
  {{ a.label }}
@@ -12395,9 +12519,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
12395
12519
 
12396
12520
  <!-- Progress bar -->
12397
12521
  @if (entry().showProgress && entry().duration > 0) {
12398
- <div class="absolute bottom-0 left-0 right-0 h-0.5 bg-clx-surface-2">
12522
+ <div class="absolute bottom-0 left-px right-px h-0.5 overflow-hidden rounded-b-[11px]">
12399
12523
  <div
12400
- class="h-full w-full origin-left transition-none"
12524
+ class="h-full w-full origin-left transition-none opacity-45"
12401
12525
  [class]="_progressColor()"
12402
12526
  [ngStyle]="{ transform: 'scaleX(' + (_progressPct() / 100) + ')' }"
12403
12527
  ></div>
@@ -15695,7 +15819,7 @@ class ClxProductDetailComponent {
15695
15819
  specsLeft(specs) { return specs.slice(0, Math.ceil(specs.length / 2)); }
15696
15820
  specsRight(specs) { return specs.slice(Math.ceil(specs.length / 2)); }
15697
15821
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15698
- 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 }, actionColor: { classPropertyName: "actionColor", publicName: "actionColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistColor: { classPropertyName: "wishlistColor", publicName: "wishlistColor", 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 }, specButtonColor: { classPropertyName: "specButtonColor", publicName: "specButtonColor", isSignal: true, isRequired: false, transformFunction: null }, quantityColor: { classPropertyName: "quantityColor", publicName: "quantityColor", 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 } }, outputs: { addToCart: "addToCart", buyNow: "buyNow", wishlist: "wishlist", variationChange: "variationChange" }, 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 <div class=\"grid grid-cols-1 md:grid-cols-2 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-5 sm:p-12.5 md:p-5 lg:p-12.5\">\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\n </div>\n </ng-template>\n }\n </clx-carousel>\n </div>\n @if (v && !v.in_stock) {\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\n </div>\n }\n <div class=\"absolute top-3 right-3 z-10\">\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" 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 ? 'border-' + clxColor() + '-500 opacity-100'\n : 'border-transparent opacity-60 hover:opacity-90'\"\n loading=\"lazy\"\n (click)=\"_carousel()?.goTo(i)\" />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass + ' space-y-2.5'\">\n\n <!-- Stock status -->\n @if (v) {\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\n @if (v.in_stock) {\n @if (v.stock_quantity != null) {\n {{ v.stock_quantity }} en stock\n } @else {\n En stock\n }\n } @else {\n Sin stock\n }\n </span>\n }\n\n <!-- Tags (st_products.tags) -->\n <div class=\"flex flex-wrap gap-1\">\n @for (tag of p.tags; track tag) {\n <span clx-badge variant=\"light\" [color]=\"clxColor()\" size=\"sm\" shape=\"pill\">{{ tag }}</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 && p.discount_price) {\n <span class=\"text-base text-clx-text-subtle line-through\">\n {{ p.discount_price | 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()\" 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=\"Quantity:\" 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]=\"_actionColor()\" 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\" [color]=\"_actionColor()\" 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\">Customer Reviews</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 }} verified ratings</p>\n </div>\n </div>\n <p class=\"text-sm font-semibold\" [class]=\"'text-' + clxColor() + '-600'\">Very Good</p>\n <div class=\"pt-2 space-y-2\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_actionColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\">\n Leave Us a Review\n </button>\n <button clx-button type=\"button\" variant=\"light\" [color]=\"_actionColor()\" size=\"md\" [block]=\"true\" icon=\"flag\">\n Report abuse\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=\"rounded-2xl overflow-hidden shadow-sm\">\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\">Helpful?</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", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxButtonGroupComponent, selector: "clx-button-group", inputs: ["label", "shape", "attached", "orientation"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSkeletonComponent, selector: "clx-skeleton", inputs: ["variant", "size", "width", "height"] }, { kind: "component", type: ClxRatingComponent, selector: "clx-rating", inputs: ["value", "max", "color"] }, { kind: "component", type: ClxCarouselComponent, selector: "clx-carousel", inputs: ["clxColor", "autoPlay", "interval", "loop", "aspectRatio", "transparent"] }, { kind: "directive", type: ClxCarouselDirective, selector: "[clxSlide]" }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "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 });
15822
+ 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 }, actionColor: { classPropertyName: "actionColor", publicName: "actionColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistColor: { classPropertyName: "wishlistColor", publicName: "wishlistColor", 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 }, specButtonColor: { classPropertyName: "specButtonColor", publicName: "specButtonColor", isSignal: true, isRequired: false, transformFunction: null }, quantityColor: { classPropertyName: "quantityColor", publicName: "quantityColor", 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 } }, outputs: { addToCart: "addToCart", buyNow: "buyNow", wishlist: "wishlist", variationChange: "variationChange" }, 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 <div class=\"grid grid-cols-1 md:grid-cols-2 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-5 sm:p-12.5 md:p-5 lg:p-12.5\">\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\n </div>\n </ng-template>\n }\n </clx-carousel>\n </div>\n @if (v && !v.in_stock) {\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\n </div>\n }\n <div class=\"absolute top-3 right-3 z-10\">\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" 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 ? 'border-' + clxColor() + '-500 opacity-100'\n : 'border-transparent opacity-60 hover:opacity-90'\"\n loading=\"lazy\"\n (click)=\"_carousel()?.goTo(i)\" />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass + ' space-y-2.5'\">\n\n <!-- Stock status -->\n @if (v) {\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\n @if (v.in_stock) {\n @if (v.stock_quantity != null) {\n {{ v.stock_quantity }} en stock\n } @else {\n En stock\n }\n } @else {\n Sin stock\n }\n </span>\n }\n\n <!-- Tags (st_products.tags) -->\n <div class=\"flex flex-wrap gap-1\">\n @for (tag of p.tags; track tag) {\n <span clx-badge variant=\"light\" [color]=\"clxColor()\" size=\"sm\" shape=\"pill\">{{ tag }}</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 && p.discount_price) {\n <span class=\"text-base text-clx-text-subtle line-through\">\n {{ p.discount_price | 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()\" 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=\"Quantity:\" 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]=\"_actionColor()\" 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\" [color]=\"_actionColor()\" 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\">Customer Reviews</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 }} verified ratings</p>\n </div>\n </div>\n <p class=\"text-sm font-semibold\" [class]=\"'text-' + clxColor() + '-600'\">Very Good</p>\n <div class=\"pt-2 space-y-2\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_actionColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\">\n Leave Us a Review\n </button>\n <button clx-button type=\"button\" variant=\"light\" [color]=\"_actionColor()\" size=\"md\" [block]=\"true\" icon=\"flag\">\n Report abuse\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=\"rounded-2xl overflow-hidden shadow-sm\">\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\">Helpful?</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", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxButtonGroupComponent, selector: "clx-button-group", inputs: ["label", "shape", "attached", "orientation"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSkeletonComponent, selector: "clx-skeleton", inputs: ["variant", "size", "width", "height"] }, { kind: "component", type: ClxRatingComponent, selector: "clx-rating", inputs: ["value", "max", "color"] }, { kind: "component", type: ClxCarouselComponent, selector: "clx-carousel", inputs: ["clxColor", "autoPlay", "interval", "loop", "aspectRatio", "transparent"] }, { kind: "directive", type: ClxCarouselDirective, selector: "[clxSlide]" }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }, { kind: "directive", type: ClxTabDirective, selector: "[clxTabPanel]", inputs: ["clxTabPanel"] }, { kind: "component", type: ClxNumberComponent, selector: "clx-number", inputs: ["variant", "size", "color", "label", "placeholder", "hint", "prefixIcon", "min", "max", "step", "value", "disabled"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }, { kind: "component", type: ClxListComponent, selector: "clx-list", inputs: ["color", "variant", "size", "numbered"] }, { kind: "component", type: ClxListItemComponent, selector: "clx-list-item", inputs: ["label", "sublabel", "wrapSublabel", "icon", "meta", "metaColor", "disabled"] }, { kind: "pipe", type: CurrencyPipe, name: "currency" }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
15699
15823
  }
15700
15824
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, decorators: [{
15701
15825
  type: Component,
@@ -17390,6 +17514,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
17390
17514
  args: [{ providedIn: 'root' }]
17391
17515
  }] });
17392
17516
 
17517
+ const NOTIFICATION_SEVERITY_TO_TOAST_TYPE = {
17518
+ success: 'green',
17519
+ warning: 'amber',
17520
+ info: 'sky',
17521
+ error: 'red',
17522
+ };
17393
17523
  class ClxToastService {
17394
17524
  _appRef = inject(ApplicationRef);
17395
17525
  _injector = inject(Injector);
@@ -17410,6 +17540,9 @@ class ClxToastService {
17410
17540
  message: options.message,
17411
17541
  title: options.title,
17412
17542
  icon: options.icon,
17543
+ category: options.category,
17544
+ timestamp: options.timestamp,
17545
+ badgeLabel: options.badgeLabel,
17413
17546
  position,
17414
17547
  duration: options.duration ?? TOAST_DEFAULTS.duration,
17415
17548
  showProgress: options.showProgress ?? TOAST_DEFAULTS.showProgress,
@@ -17437,6 +17570,19 @@ class ClxToastService {
17437
17570
  info(message, opts = {}) {
17438
17571
  this.show({ ...opts, type: 'sky', message });
17439
17572
  }
17573
+ /** Shows a toast built straight from a live ClxNotificationItem (e.g. on SSE arrival) — the same
17574
+ * object already driving the notification panel/bell, no manual field mapping at the call site. */
17575
+ fromNotification(item, opts = {}) {
17576
+ this.show({
17577
+ ...opts,
17578
+ type: NOTIFICATION_SEVERITY_TO_TOAST_TYPE[item.severity],
17579
+ icon: item.icon,
17580
+ title: item.title,
17581
+ message: item.detail ?? '',
17582
+ category: item.category,
17583
+ timestamp: new Date(item.createdAt),
17584
+ });
17585
+ }
17440
17586
  dismissAll() {
17441
17587
  this._containers.forEach(c => c.instance.entries.set([]));
17442
17588
  }