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.
- package/fesm2022/codexly-ui.mjs +217 -71
- package/fesm2022/codexly-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/codexly-ui.d.ts +48 -6
package/fesm2022/codexly-ui.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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 ?
|
|
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 ?
|
|
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
|
-
// ───
|
|
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.
|
|
12082
|
-
orange: c.orange.
|
|
12083
|
-
amber: c.amber.
|
|
12084
|
-
yellow: c.yellow.
|
|
12085
|
-
lime: c.lime.
|
|
12086
|
-
green: c.green.
|
|
12087
|
-
emerald: c.emerald.
|
|
12088
|
-
teal: c.teal.
|
|
12089
|
-
cyan: c.cyan.
|
|
12090
|
-
sky: c.sky.
|
|
12091
|
-
blue: c.blue.
|
|
12092
|
-
indigo: c.indigo.
|
|
12093
|
-
violet: c.violet.
|
|
12094
|
-
purple: c.purple.
|
|
12095
|
-
fuchsia: c.fuchsia.
|
|
12096
|
-
pink: c.pink.
|
|
12097
|
-
rose: c.rose.
|
|
12098
|
-
slate: c.slate.
|
|
12099
|
-
gray: c.gray.
|
|
12100
|
-
zinc: c.zinc.
|
|
12101
|
-
neutral: c.neutral.
|
|
12102
|
-
stone: c.stone.
|
|
12103
|
-
white: c.white.
|
|
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
|
-
// ───
|
|
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
|
|
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
|
-
|
|
12273
|
-
[
|
|
12274
|
-
|
|
12275
|
-
class="
|
|
12276
|
-
|
|
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
|
|
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-
|
|
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 ?? '
|
|
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-
|
|
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
|
|
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
|
-
|
|
12348
|
-
[
|
|
12349
|
-
|
|
12350
|
-
class="
|
|
12351
|
-
|
|
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
|
|
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-
|
|
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 ?? '
|
|
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-
|
|
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
|
}
|