codexly-ui 0.25.0 → 0.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/codexly-ui.mjs +210 -36
- package/fesm2022/codexly-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/codexly-ui.d.ts +90 -20
package/fesm2022/codexly-ui.mjs
CHANGED
|
@@ -2221,13 +2221,14 @@ class ClxToastService {
|
|
|
2221
2221
|
_injector = inject(Injector);
|
|
2222
2222
|
// Each position has its own container div at z-index 9000 (above modals)
|
|
2223
2223
|
_containers = new Map();
|
|
2224
|
+
/** Returns the toast's id — pass it to dismiss() to close it from code. */
|
|
2224
2225
|
show(options) {
|
|
2225
2226
|
// Toasts are a browser-only UI concern — during SSR there's no `document` to mount the
|
|
2226
2227
|
// container into, and no user watching the server's own render to see it anyway. Silently
|
|
2227
2228
|
// no-op instead of throwing (an uncaught ReferenceError here crashes the whole Node SSR
|
|
2228
2229
|
// process, taking down every other in-flight request with it).
|
|
2229
2230
|
if (typeof document === 'undefined')
|
|
2230
|
-
return;
|
|
2231
|
+
return '';
|
|
2231
2232
|
const position = options.position ?? TOAST_DEFAULTS.position;
|
|
2232
2233
|
const container = this._getOrCreateContainer(position);
|
|
2233
2234
|
const entry = {
|
|
@@ -2253,18 +2254,19 @@ class ClxToastService {
|
|
|
2253
2254
|
const isBottom = position.startsWith('bottom');
|
|
2254
2255
|
const list = container.instance.entries();
|
|
2255
2256
|
container.instance.entries.set(isBottom ? [...list, entry] : [entry, ...list]);
|
|
2257
|
+
return entry.id;
|
|
2256
2258
|
}
|
|
2257
2259
|
success(message, opts = {}) {
|
|
2258
|
-
this.show({ ...opts, type: 'green', message });
|
|
2260
|
+
return this.show({ ...opts, type: 'green', message });
|
|
2259
2261
|
}
|
|
2260
2262
|
error(message, opts = {}) {
|
|
2261
|
-
this.show({ ...opts, type: 'red', message });
|
|
2263
|
+
return this.show({ ...opts, type: 'red', message });
|
|
2262
2264
|
}
|
|
2263
2265
|
warning(message, opts = {}) {
|
|
2264
|
-
this.show({ ...opts, type: 'amber', message });
|
|
2266
|
+
return this.show({ ...opts, type: 'amber', message });
|
|
2265
2267
|
}
|
|
2266
2268
|
info(message, opts = {}) {
|
|
2267
|
-
this.show({ ...opts, type: 'sky', message });
|
|
2269
|
+
return this.show({ ...opts, type: 'sky', message });
|
|
2268
2270
|
}
|
|
2269
2271
|
/** Shows a toast built straight from a live ClxNotificationItem (e.g. on SSE arrival) — the same
|
|
2270
2272
|
* object already driving the notification panel/bell, no manual field mapping at the call site. */
|
|
@@ -2279,6 +2281,14 @@ class ClxToastService {
|
|
|
2279
2281
|
timestamp: new Date(item.createdAt),
|
|
2280
2282
|
});
|
|
2281
2283
|
}
|
|
2284
|
+
/** Closes one toast, with its exit animation — e.g. a persistent prompt whose question was already
|
|
2285
|
+
* answered somewhere else. No-op when it's already gone. */
|
|
2286
|
+
dismiss(id) {
|
|
2287
|
+
this._containers.forEach(c => {
|
|
2288
|
+
if (c.instance.entries().some(e => e.id === id))
|
|
2289
|
+
void c.instance.requestDismiss(id);
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2282
2292
|
dismissAll() {
|
|
2283
2293
|
this._containers.forEach(c => c.instance.entries.set([]));
|
|
2284
2294
|
}
|
|
@@ -13795,6 +13805,172 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
13795
13805
|
`, styles: ["clx-alert{display:flex;align-items:center;justify-content:center;width:100%}.clx-alert-card{will-change:transform,opacity}\n"] }]
|
|
13796
13806
|
}], ctorParameters: () => [] });
|
|
13797
13807
|
|
|
13808
|
+
/** Per-item, per-channel on/off settings grouped in cards — notification settings ("avísame por App,
|
|
13809
|
+
* Correo o Push"), but any item × channel matrix fits. Each group's header has one switch per channel
|
|
13810
|
+
* that turns that whole column on or off; an item only shows switches on the channels it really uses
|
|
13811
|
+
* (an unavailable one shows a dash, so the columns stay aligned), and a locked channel stays on with
|
|
13812
|
+
* its reason as tooltip. On phones each item stacks its switches under its text, each with its own
|
|
13813
|
+
* channel label. Stateless: the host applies every (change) and feeds the new state back. */
|
|
13814
|
+
class ClxChannelPreferencesComponent {
|
|
13815
|
+
groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
13816
|
+
channels = input([], ...(ngDevMode ? [{ debugName: "channels" }] : /* istanbul ignore next */ []));
|
|
13817
|
+
color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
13818
|
+
variant = input('elevated', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
13819
|
+
change = output();
|
|
13820
|
+
_channelKeys = computed(() => this.channels().map(c => c.key), ...(ngDevMode ? [{ debugName: "_channelKeys" }] : /* istanbul ignore next */ []));
|
|
13821
|
+
stateOf(item, channel) {
|
|
13822
|
+
return item.channels[channel];
|
|
13823
|
+
}
|
|
13824
|
+
/** Items of the group that can be switched on this channel (available and not locked). */
|
|
13825
|
+
switchable(group, channel) {
|
|
13826
|
+
return group.items.filter(i => i.channels[channel]?.available && !i.channels[channel]?.locked);
|
|
13827
|
+
}
|
|
13828
|
+
groupHasChannel(group, channel) {
|
|
13829
|
+
return this._channelKeys().includes(channel) && this.switchable(group, channel).length > 0;
|
|
13830
|
+
}
|
|
13831
|
+
groupEnabled(group, channel) {
|
|
13832
|
+
return this.switchable(group, channel).every(i => i.channels[channel].enabled);
|
|
13833
|
+
}
|
|
13834
|
+
emitItem(group, item, channel, enabled) {
|
|
13835
|
+
this.change.emit({ group: group.key, item: item.key, channel, enabled });
|
|
13836
|
+
}
|
|
13837
|
+
emitGroup(group, channel, enabled) {
|
|
13838
|
+
this.change.emit({ group: group.key, item: null, channel, enabled });
|
|
13839
|
+
}
|
|
13840
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxChannelPreferencesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13841
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxChannelPreferencesComponent, isStandalone: true, selector: "clx-channel-preferences", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, channels: { classPropertyName: "channels", publicName: "channels", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change" }, ngImport: i0, template: `
|
|
13842
|
+
<div class="flex flex-col gap-6">
|
|
13843
|
+
@for (group of groups(); track group.key) {
|
|
13844
|
+
<clx-card [variant]="variant()" [icon]="group.icon" [headerTitle]="group.label">
|
|
13845
|
+
<div clxCardBody class="flex flex-col">
|
|
13846
|
+
<div class="hidden sm:flex items-center justify-end gap-2 pb-3 border-b border-clx-border-soft">
|
|
13847
|
+
@for (channel of channels(); track channel.key) {
|
|
13848
|
+
@if (groupHasChannel(group, channel.key)) {
|
|
13849
|
+
<div class="w-16 flex flex-col items-center gap-1">
|
|
13850
|
+
<span class="text-[11px] font-medium text-clx-text-muted">{{ channel.label }}</span>
|
|
13851
|
+
<label clx-switch size="sm" [color]="color()"
|
|
13852
|
+
[value]="groupEnabled(group, channel.key)"
|
|
13853
|
+
[clxTooltip]="groupEnabled(group, channel.key) ? 'Desactivar todo' : 'Activar todo'"
|
|
13854
|
+
clxTooltipSize="sm"
|
|
13855
|
+
(changed)="emitGroup(group, channel.key, $event)"></label>
|
|
13856
|
+
</div>
|
|
13857
|
+
} @else {
|
|
13858
|
+
<div class="w-16 text-center text-[11px] font-medium text-clx-text-faint">{{ channel.label }}</div>
|
|
13859
|
+
}
|
|
13860
|
+
}
|
|
13861
|
+
</div>
|
|
13862
|
+
|
|
13863
|
+
@for (item of group.items; track item.key; let last = $last) {
|
|
13864
|
+
<div class="flex flex-col sm:flex-row sm:items-center gap-3 py-3"
|
|
13865
|
+
[class.border-b]="!last" [class.border-clx-border-soft]="!last">
|
|
13866
|
+
<div class="flex items-start gap-3 flex-1 min-w-0">
|
|
13867
|
+
@if (item.icon) {
|
|
13868
|
+
<span clx-icon [name]="item.icon" size="sm" class="mt-0.5 text-clx-text-muted"></span>
|
|
13869
|
+
}
|
|
13870
|
+
<div class="min-w-0">
|
|
13871
|
+
<p class="text-sm font-medium text-clx-text-label">{{ item.label }}</p>
|
|
13872
|
+
@if (item.description) {
|
|
13873
|
+
<p class="text-xs text-clx-text-muted mt-0.5">{{ item.description }}</p>
|
|
13874
|
+
}
|
|
13875
|
+
</div>
|
|
13876
|
+
</div>
|
|
13877
|
+
<div class="flex flex-wrap sm:flex-nowrap items-center gap-x-5 gap-y-2 sm:gap-2 pl-8 sm:pl-0 shrink-0">
|
|
13878
|
+
@for (channel of channels(); track channel.key) {
|
|
13879
|
+
@let state = stateOf(item, channel.key);
|
|
13880
|
+
@if (state?.available) {
|
|
13881
|
+
<div class="sm:w-16 flex items-center sm:justify-center gap-2">
|
|
13882
|
+
<label clx-switch size="sm" [color]="color()"
|
|
13883
|
+
[value]="state!.enabled || !!state!.locked"
|
|
13884
|
+
[disabled]="!!state!.locked"
|
|
13885
|
+
[clxTooltip]="state!.locked ? (state!.lockedReason ?? 'Siempre activo') : ''"
|
|
13886
|
+
clxTooltipSize="sm"
|
|
13887
|
+
(changed)="emitItem(group, item, channel.key, $event)"></label>
|
|
13888
|
+
<span class="sm:hidden text-xs text-clx-text-muted">{{ channel.label }}</span>
|
|
13889
|
+
</div>
|
|
13890
|
+
} @else {
|
|
13891
|
+
<span class="hidden sm:block w-16 text-center text-clx-text-faint" aria-hidden="true">—</span>
|
|
13892
|
+
}
|
|
13893
|
+
}
|
|
13894
|
+
</div>
|
|
13895
|
+
</div>
|
|
13896
|
+
}
|
|
13897
|
+
</div>
|
|
13898
|
+
</clx-card>
|
|
13899
|
+
}
|
|
13900
|
+
</div>
|
|
13901
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ClxCardComponent, selector: "clx-card", inputs: ["variant", "color", "padding", "shadow", "hover", "disabled", "border", "radius", "icon", "headerTitle", "headerSubtitle"] }, { kind: "directive", type: ClxCardBodyDirective, selector: "[clxCardBody]" }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSwitchComponent, selector: "label[clx-switch]", inputs: ["color", "size", "label", "value", "disabled"], outputs: ["changed"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13902
|
+
}
|
|
13903
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxChannelPreferencesComponent, decorators: [{
|
|
13904
|
+
type: Component,
|
|
13905
|
+
args: [{
|
|
13906
|
+
selector: 'clx-channel-preferences',
|
|
13907
|
+
standalone: true,
|
|
13908
|
+
imports: [ClxCardComponent, ClxCardBodyDirective, ClxIconComponent, ClxSwitchComponent, ClxTooltipDirective],
|
|
13909
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
13910
|
+
template: `
|
|
13911
|
+
<div class="flex flex-col gap-6">
|
|
13912
|
+
@for (group of groups(); track group.key) {
|
|
13913
|
+
<clx-card [variant]="variant()" [icon]="group.icon" [headerTitle]="group.label">
|
|
13914
|
+
<div clxCardBody class="flex flex-col">
|
|
13915
|
+
<div class="hidden sm:flex items-center justify-end gap-2 pb-3 border-b border-clx-border-soft">
|
|
13916
|
+
@for (channel of channels(); track channel.key) {
|
|
13917
|
+
@if (groupHasChannel(group, channel.key)) {
|
|
13918
|
+
<div class="w-16 flex flex-col items-center gap-1">
|
|
13919
|
+
<span class="text-[11px] font-medium text-clx-text-muted">{{ channel.label }}</span>
|
|
13920
|
+
<label clx-switch size="sm" [color]="color()"
|
|
13921
|
+
[value]="groupEnabled(group, channel.key)"
|
|
13922
|
+
[clxTooltip]="groupEnabled(group, channel.key) ? 'Desactivar todo' : 'Activar todo'"
|
|
13923
|
+
clxTooltipSize="sm"
|
|
13924
|
+
(changed)="emitGroup(group, channel.key, $event)"></label>
|
|
13925
|
+
</div>
|
|
13926
|
+
} @else {
|
|
13927
|
+
<div class="w-16 text-center text-[11px] font-medium text-clx-text-faint">{{ channel.label }}</div>
|
|
13928
|
+
}
|
|
13929
|
+
}
|
|
13930
|
+
</div>
|
|
13931
|
+
|
|
13932
|
+
@for (item of group.items; track item.key; let last = $last) {
|
|
13933
|
+
<div class="flex flex-col sm:flex-row sm:items-center gap-3 py-3"
|
|
13934
|
+
[class.border-b]="!last" [class.border-clx-border-soft]="!last">
|
|
13935
|
+
<div class="flex items-start gap-3 flex-1 min-w-0">
|
|
13936
|
+
@if (item.icon) {
|
|
13937
|
+
<span clx-icon [name]="item.icon" size="sm" class="mt-0.5 text-clx-text-muted"></span>
|
|
13938
|
+
}
|
|
13939
|
+
<div class="min-w-0">
|
|
13940
|
+
<p class="text-sm font-medium text-clx-text-label">{{ item.label }}</p>
|
|
13941
|
+
@if (item.description) {
|
|
13942
|
+
<p class="text-xs text-clx-text-muted mt-0.5">{{ item.description }}</p>
|
|
13943
|
+
}
|
|
13944
|
+
</div>
|
|
13945
|
+
</div>
|
|
13946
|
+
<div class="flex flex-wrap sm:flex-nowrap items-center gap-x-5 gap-y-2 sm:gap-2 pl-8 sm:pl-0 shrink-0">
|
|
13947
|
+
@for (channel of channels(); track channel.key) {
|
|
13948
|
+
@let state = stateOf(item, channel.key);
|
|
13949
|
+
@if (state?.available) {
|
|
13950
|
+
<div class="sm:w-16 flex items-center sm:justify-center gap-2">
|
|
13951
|
+
<label clx-switch size="sm" [color]="color()"
|
|
13952
|
+
[value]="state!.enabled || !!state!.locked"
|
|
13953
|
+
[disabled]="!!state!.locked"
|
|
13954
|
+
[clxTooltip]="state!.locked ? (state!.lockedReason ?? 'Siempre activo') : ''"
|
|
13955
|
+
clxTooltipSize="sm"
|
|
13956
|
+
(changed)="emitItem(group, item, channel.key, $event)"></label>
|
|
13957
|
+
<span class="sm:hidden text-xs text-clx-text-muted">{{ channel.label }}</span>
|
|
13958
|
+
</div>
|
|
13959
|
+
} @else {
|
|
13960
|
+
<span class="hidden sm:block w-16 text-center text-clx-text-faint" aria-hidden="true">—</span>
|
|
13961
|
+
}
|
|
13962
|
+
}
|
|
13963
|
+
</div>
|
|
13964
|
+
</div>
|
|
13965
|
+
}
|
|
13966
|
+
</div>
|
|
13967
|
+
</clx-card>
|
|
13968
|
+
}
|
|
13969
|
+
</div>
|
|
13970
|
+
`,
|
|
13971
|
+
}]
|
|
13972
|
+
}], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], channels: [{ type: i0.Input, args: [{ isSignal: true, alias: "channels", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], change: [{ type: i0.Output, args: ["change"] }] } });
|
|
13973
|
+
|
|
13798
13974
|
const SIZE_MAP = {
|
|
13799
13975
|
sm: { row: 'py-1 px-2', text: 'text-xs', icon: 'xs', indent: 'pl-5' },
|
|
13800
13976
|
md: { row: 'py-1.5 px-2', text: 'text-sm', icon: 'sm', indent: 'pl-6' },
|
|
@@ -17419,6 +17595,14 @@ const PRODUCT_TAG_VARIANT = {
|
|
|
17419
17595
|
'limited': 'solid',
|
|
17420
17596
|
'hot': 'solid',
|
|
17421
17597
|
};
|
|
17598
|
+
/** How a tag renders without ClxProductTagService: built-in codes get their PRODUCT_TAG_* look,
|
|
17599
|
+
* any other code (a host tag before setTags) shows the code itself, light, in the fallback color. */
|
|
17600
|
+
function productTagLook(tag, fallbackColor) {
|
|
17601
|
+
if (!(tag in PRODUCT_TAG_LABEL))
|
|
17602
|
+
return { label: tag, color: fallbackColor, variant: 'light' };
|
|
17603
|
+
const known = tag;
|
|
17604
|
+
return { label: PRODUCT_TAG_LABEL[known], color: PRODUCT_TAG_COLOR[known], variant: PRODUCT_TAG_VARIANT[known] };
|
|
17605
|
+
}
|
|
17422
17606
|
|
|
17423
17607
|
/**
|
|
17424
17608
|
* The product tags configured by the host app (setTags, e.g. from its backend at startup). Once set,
|
|
@@ -17587,12 +17771,9 @@ class ClxProductComponent {
|
|
|
17587
17771
|
addToCart = output();
|
|
17588
17772
|
addToWishlist = output();
|
|
17589
17773
|
viewOptions = output();
|
|
17590
|
-
PRODUCT_TAG_LABEL = PRODUCT_TAG_LABEL;
|
|
17591
|
-
PRODUCT_TAG_COLOR = PRODUCT_TAG_COLOR;
|
|
17592
|
-
PRODUCT_TAG_VARIANT = PRODUCT_TAG_VARIANT;
|
|
17593
17774
|
_h = computed(() => this.layout() === 'horizontal', ...(ngDevMode ? [{ debugName: "_h" }] : /* istanbul ignore next */ []));
|
|
17594
17775
|
_sm = computed(() => this.size() === 'sm', ...(ngDevMode ? [{ debugName: "_sm" }] : /* istanbul ignore next */ []));
|
|
17595
|
-
_tags = computed(() => (this.product()?.tags ?? []), ...(ngDevMode ? [{ debugName: "_tags" }] : /* istanbul ignore next */ []));
|
|
17776
|
+
_tags = computed(() => (this.product()?.tags ?? []).map(code => ({ code, ...productTagLook(code, this.clxColor()) })), ...(ngDevMode ? [{ debugName: "_tags" }] : /* istanbul ignore next */ []));
|
|
17596
17777
|
_tagSvc = inject(ClxProductTagService);
|
|
17597
17778
|
_badges = computed(() => {
|
|
17598
17779
|
const p = this.product();
|
|
@@ -17733,14 +17914,14 @@ class ClxProductComponent {
|
|
|
17733
17914
|
<clx-product-badges [badges]="_badges()" bottomRightOffset="bottom-14" />
|
|
17734
17915
|
} @else if (_tags().length) {
|
|
17735
17916
|
<div class="absolute top-3 left-3 flex flex-col gap-1">
|
|
17736
|
-
@for (
|
|
17917
|
+
@for (t of _tags(); track t.code) {
|
|
17737
17918
|
<span
|
|
17738
17919
|
clx-badge
|
|
17739
|
-
[variant]="
|
|
17740
|
-
[color]="
|
|
17920
|
+
[variant]="t.variant"
|
|
17921
|
+
[color]="t.color"
|
|
17741
17922
|
size="sm"
|
|
17742
17923
|
shape="pill"
|
|
17743
|
-
>{{
|
|
17924
|
+
>{{ t.label }}</span>
|
|
17744
17925
|
}
|
|
17745
17926
|
</div>
|
|
17746
17927
|
}
|
|
@@ -17873,14 +18054,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
17873
18054
|
<clx-product-badges [badges]="_badges()" bottomRightOffset="bottom-14" />
|
|
17874
18055
|
} @else if (_tags().length) {
|
|
17875
18056
|
<div class="absolute top-3 left-3 flex flex-col gap-1">
|
|
17876
|
-
@for (
|
|
18057
|
+
@for (t of _tags(); track t.code) {
|
|
17877
18058
|
<span
|
|
17878
18059
|
clx-badge
|
|
17879
|
-
[variant]="
|
|
17880
|
-
[color]="
|
|
18060
|
+
[variant]="t.variant"
|
|
18061
|
+
[color]="t.color"
|
|
17881
18062
|
size="sm"
|
|
17882
18063
|
shape="pill"
|
|
17883
|
-
>{{
|
|
18064
|
+
>{{ t.label }}</span>
|
|
17884
18065
|
}
|
|
17885
18066
|
</div>
|
|
17886
18067
|
}
|
|
@@ -18022,6 +18203,11 @@ class ClxProductDetailComponent {
|
|
|
18022
18203
|
/** Hides the tabs (Características/Especificaciones/Descripción) and the Reviews section —
|
|
18023
18204
|
* for contexts like a quick-view modal where only the gallery + spec selector + CTA matter. */
|
|
18024
18205
|
compact = input(false, ...(ngDevMode ? [{ debugName: "compact" }] : /* istanbul ignore next */ []));
|
|
18206
|
+
/** False = a catalog view with nothing to buy (e.g. an ERP looking a product up to share it):
|
|
18207
|
+
* hides favorites, rating, quantity, "Comprar ahora" / "Agrega al carrito" and the Reviews
|
|
18208
|
+
* section, keeping the gallery, the spec selectors and the tabs. Whatever the host projects with
|
|
18209
|
+
* [clxProductDetailActions] renders where the purchase buttons were, in either mode. */
|
|
18210
|
+
purchasable = input(true, ...(ngDevMode ? [{ debugName: "purchasable" }] : /* istanbul ignore next */ []));
|
|
18025
18211
|
/** Same ClxCardVariant concept as clx-card: 'elevated' (border + shadow, the default the
|
|
18026
18212
|
* gallery/info panels and each review card have always rendered with), 'outlined' (border, no
|
|
18027
18213
|
* shadow), 'flat' (neither). The border itself always reads cardBorderColor() regardless of
|
|
@@ -18167,20 +18353,8 @@ class ClxProductDetailComponent {
|
|
|
18167
18353
|
return 'Regular';
|
|
18168
18354
|
return 'Deficiente';
|
|
18169
18355
|
}, ...(ngDevMode ? [{ debugName: "_ratingLabel" }] : /* istanbul ignore next */ []));
|
|
18170
|
-
/**
|
|
18171
|
-
|
|
18172
|
-
isKnownTag(tag) {
|
|
18173
|
-
return tag in PRODUCT_TAG_LABEL;
|
|
18174
|
-
}
|
|
18175
|
-
tagLabel(tag) {
|
|
18176
|
-
return this.isKnownTag(tag) ? PRODUCT_TAG_LABEL[tag] : tag;
|
|
18177
|
-
}
|
|
18178
|
-
tagColor(tag) {
|
|
18179
|
-
return this.isKnownTag(tag) ? PRODUCT_TAG_COLOR[tag] : this.clxColor();
|
|
18180
|
-
}
|
|
18181
|
-
tagVariant(tag) {
|
|
18182
|
-
return this.isKnownTag(tag) ? PRODUCT_TAG_VARIANT[tag] : 'light';
|
|
18183
|
-
}
|
|
18356
|
+
/** The built-in tags (shown until the host configures ClxProductTagService). */
|
|
18357
|
+
_tags = computed(() => (this.product()?.tags ?? []).map(code => ({ code, ...productTagLook(code, this.clxColor()) })), ...(ngDevMode ? [{ debugName: "_tags" }] : /* istanbul ignore next */ []));
|
|
18184
18358
|
/** The selected variation's own discount_percentage when the host sends one, else the product's
|
|
18185
18359
|
* (it applies on top of the list price — see listPrice() above). discount_price is only a fallback
|
|
18186
18360
|
* for a caller whose backend instead stores a precomputed higher reference price to derive the
|
|
@@ -18276,7 +18450,7 @@ class ClxProductDetailComponent {
|
|
|
18276
18450
|
specsLeft(specs) { return specs.slice(0, Math.ceil(specs.length / 2)); }
|
|
18277
18451
|
specsRight(specs) { return specs.slice(Math.ceil(specs.length / 2)); }
|
|
18278
18452
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18279
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxProductDetailComponent, isStandalone: true, selector: "clx-product-detail", inputs: { product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, initialVariationId: { classPropertyName: "initialVariationId", publicName: "initialVariationId", isSignal: true, isRequired: false, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowColor: { classPropertyName: "buyNowColor", publicName: "buyNowColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowTextColor: { classPropertyName: "buyNowTextColor", publicName: "buyNowTextColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartColor: { classPropertyName: "addToCartColor", publicName: "addToCartColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartTextColor: { classPropertyName: "addToCartTextColor", publicName: "addToCartTextColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewColor: { classPropertyName: "leaveReviewColor", publicName: "leaveReviewColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewTextColor: { classPropertyName: "leaveReviewTextColor", publicName: "leaveReviewTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistColor: { classPropertyName: "wishlistColor", publicName: "wishlistColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistTextColor: { classPropertyName: "wishlistTextColor", publicName: "wishlistTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistVariant: { classPropertyName: "wishlistVariant", publicName: "wishlistVariant", isSignal: true, isRequired: false, transformFunction: null }, stockBadgeColor: { classPropertyName: "stockBadgeColor", publicName: "stockBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, discountBadgeColor: { classPropertyName: "discountBadgeColor", publicName: "discountBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, quantityColor: { classPropertyName: "quantityColor", publicName: "quantityColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonColor: { classPropertyName: "specButtonColor", publicName: "specButtonColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonTextColor: { classPropertyName: "specButtonTextColor", publicName: "specButtonTextColor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addToCart: "addToCart", buyNow: "buyNow", wishlist: "wishlist", variationChange: "variationChange", leaveReview: "leaveReview" }, viewQueries: [{ propertyName: "_carousel", first: true, predicate: ClxCarouselComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u2550\u2550 SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (loading()) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\n <div class=\"space-y-3\">\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\n <div class=\"flex gap-2\">\n @for (t of [1,2,3,4]; track t) {\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\n }\n </div>\n </div>\n <div class=\"space-y-4 pt-2\">\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n </div>\n </div>\n}\n\n<!-- \u2550\u2550 CONTENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (!loading() && product(); as p) {\n @let v = activeVariation();\n <div class=\"space-y-8\">\n\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\n purchase info column, especially once a product has specs/variants to list; a fixed\n max-width on the gallery alone (tried first) just left dead space beside it instead of\n letting the info column actually grow into it. -->\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\n\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\n header. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\n <div class=\"relative aspect-4/3\">\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <ng-template clxSlide>\n <div class=\"w-full h-full p-9\">\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\n </div>\n </ng-template>\n }\n </clx-carousel>\n </div>\n @if (_tagSvc.configured()) {\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\n } @else if (v && !v.in_stock) {\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\n </div>\n }\n <div class=\"absolute top-3 right-3 z-10\">\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\n (click)=\"wishlist.emit()\">\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\n </button>\n </div>\n </div>\n <div class=\"flex flex-wrap gap-2\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <img [src]=\"img\" [alt]=\"p.product_name\"\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\n [class]=\"_carousel()?.currentPageIndex() === i\n ? _thumbBorderClass() + ' opacity-100'\n : 'border-transparent opacity-60 hover:opacity-90'\"\n loading=\"lazy\"\n (click)=\"_carousel()?.goTo(i)\" />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\n\n <!-- Stock status + tags \u2014 over the gallery instead when the host configured its tags -->\n @if (v && !_tagSvc.configured()) {\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\n @if (v.in_stock) {\n @if (v.stock_quantity != null) {\n {{ v.stock_quantity }} en stock\n } @else {\n En stock\n }\n } @else {\n Sin stock\n }\n </span>\n }\n\n <!-- Tags (st_products.tags) -->\n <div class=\"flex flex-wrap gap-1\" [class.hidden]=\"_tagSvc.configured()\">\n @for (tag of p.tags; track tag) {\n <span clx-badge [variant]=\"tagVariant(tag)\" [color]=\"tagColor(tag)\" size=\"sm\" shape=\"pill\">\n {{ tagLabel(tag) }}\n </span>\n }\n </div>\n\n <!-- Title -->\n <div>\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\n @if (p.product_detail) {\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\n }\n </div>\n\n <!-- Rating -->\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\n </div>\n\n <!-- Price block -->\n <div>\n <div class=\"flex items-baseline gap-3\">\n <span class=\"text-2xl font-black text-clx-text-label\">\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n @if (discount() > 0) {\n <span class=\"text-base text-clx-text-subtle line-through\">\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\n -{{ discount() }}%\n </span>\n }\n </div>\n </div>\n\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\n @for (specType of p.spec_types; track specType.id) {\n @if (specType.input_type === 'color') {\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button\n type=\"button\"\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\n [style.background-color]=\"opt.value\"\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\n [clxTooltip]=\"opt.label ?? opt.value\"\n clxTooltipPosition=\"top\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n </button>\n }\n }\n </clx-button-group>\n } @else {\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button clx-button\n type=\"button\"\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\n [color]=\"_specButtonColor()\"\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\n size=\"sm\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n {{ opt.label ?? opt.value }}\n </button>\n }\n }\n </clx-button-group>\n }\n }\n\n <!-- Quantity -->\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\n </clx-number>\n\n <!-- CTA -->\n @if (v?.in_stock && !isSelectionComplete()) {\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\n }\n <div class=\"flex gap-3\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\n </div>\n\n </div>\n </div>\n\n @if (!compact()) {\n <!-- \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\n\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\n <ng-template [clxTabPanel]=\"'general'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsLeft(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsRight(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\n <ng-template [clxTabPanel]=\"'specs'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsLeft(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsRight(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\n <ng-template [clxTabPanel]=\"'description'\">\n <div class=\"space-y-5\">\n @for (block of p.descriptions; track block.id) {\n <div>\n @if (block.title) {\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\n }\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\n </div>\n }\n </div>\n </ng-template>\n\n </clx-tabs>\n\n <!-- \u2500\u2500 Reviews \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\n\n <div class=\"space-y-3\">\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\n <div class=\"flex items-end gap-3\">\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <div class=\"pb-1 space-y-1\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\n </div>\n </div>\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\n <div class=\"pt-2 space-y-2\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\n (click)=\"leaveReview.emit()\">\n Dejar una rese\u00F1a\n </button>\n </div>\n </div>\n\n <div class=\"lg:col-span-2 space-y-4\">\n @for (review of p.reviews ?? []; track review.id) {\n <div [class]=\"_reviewCardClass()\">\n <div class=\"flex items-center gap-3 px-4 py-3\">\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\n <span class=\"text-sm font-bold text-clx-text-muted\">\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\n </div>\n @if (review.is_verified) {\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\n }\n </div>\n <div class=\"px-4 pb-4 space-y-2\">\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\n @if (review.title) {\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\n }\n </div>\n @if (review.body) {\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\n }\n <div class=\"flex items-center gap-3 pt-1\">\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\n </div>\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxButtonGroupComponent, selector: "clx-button-group", inputs: ["label", "shape", "attached", "orientation"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned", "textColor"] }, { kind: "component", type: ClxProductBadgesComponent, selector: "clx-product-badges", inputs: ["badges", "size", "topRightOffset", "bottomRightOffset"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSkeletonComponent, selector: "clx-skeleton", inputs: ["variant", "size", "width", "height"] }, { kind: "component", type: ClxRatingComponent, selector: "clx-rating", inputs: ["value", "max", "color"] }, { kind: "component", type: ClxCarouselComponent, selector: "clx-carousel", inputs: ["clxColor", "autoPlay", "interval", "loop", "aspectRatio", "transparent", "rounded", "pauseOnHover", "arrows"] }, { kind: "directive", type: ClxCarouselDirective, selector: "[clxSlide]" }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }, { kind: "directive", type: ClxTabDirective, selector: "[clxTabPanel]", inputs: ["clxTabPanel"] }, { kind: "component", type: ClxNumberComponent, selector: "clx-number", inputs: ["variant", "size", "color", "label", "placeholder", "hint", "prefixIcon", "min", "max", "step", "value", "disabled"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }, { kind: "component", type: ClxListComponent, selector: "clx-list", inputs: ["color", "variant", "size", "numbered"] }, { kind: "component", type: ClxListItemComponent, selector: "clx-list-item", inputs: ["label", "sublabel", "wrapSublabel", "icon", "meta", "metaColor", "disabled"] }, { kind: "pipe", type: CurrencyPipe, name: "currency" }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
18453
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.15", type: ClxProductDetailComponent, isStandalone: true, selector: "clx-product-detail", inputs: { product: { classPropertyName: "product", publicName: "product", isSignal: true, isRequired: false, transformFunction: null }, initialVariationId: { classPropertyName: "initialVariationId", publicName: "initialVariationId", isSignal: true, isRequired: false, transformFunction: null }, clxColor: { classPropertyName: "clxColor", publicName: "clxColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowColor: { classPropertyName: "buyNowColor", publicName: "buyNowColor", isSignal: true, isRequired: false, transformFunction: null }, buyNowTextColor: { classPropertyName: "buyNowTextColor", publicName: "buyNowTextColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartColor: { classPropertyName: "addToCartColor", publicName: "addToCartColor", isSignal: true, isRequired: false, transformFunction: null }, addToCartTextColor: { classPropertyName: "addToCartTextColor", publicName: "addToCartTextColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewColor: { classPropertyName: "leaveReviewColor", publicName: "leaveReviewColor", isSignal: true, isRequired: false, transformFunction: null }, leaveReviewTextColor: { classPropertyName: "leaveReviewTextColor", publicName: "leaveReviewTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistColor: { classPropertyName: "wishlistColor", publicName: "wishlistColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistTextColor: { classPropertyName: "wishlistTextColor", publicName: "wishlistTextColor", isSignal: true, isRequired: false, transformFunction: null }, wishlistVariant: { classPropertyName: "wishlistVariant", publicName: "wishlistVariant", isSignal: true, isRequired: false, transformFunction: null }, stockBadgeColor: { classPropertyName: "stockBadgeColor", publicName: "stockBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, discountBadgeColor: { classPropertyName: "discountBadgeColor", publicName: "discountBadgeColor", isSignal: true, isRequired: false, transformFunction: null }, quantityColor: { classPropertyName: "quantityColor", publicName: "quantityColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonColor: { classPropertyName: "specButtonColor", publicName: "specButtonColor", isSignal: true, isRequired: false, transformFunction: null }, specButtonTextColor: { classPropertyName: "specButtonTextColor", publicName: "specButtonTextColor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null }, purchasable: { classPropertyName: "purchasable", publicName: "purchasable", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { addToCart: "addToCart", buyNow: "buyNow", wishlist: "wishlist", variationChange: "variationChange", leaveReview: "leaveReview" }, viewQueries: [{ propertyName: "_carousel", first: true, predicate: ClxCarouselComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- \u2550\u2550 SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (loading()) {\r\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\r\n <div class=\"space-y-3\">\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\r\n <div class=\"flex gap-2\">\r\n @for (t of [1,2,3,4]; track t) {\r\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"space-y-4 pt-2\">\r\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n </div>\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550 CONTENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (!loading() && product(); as p) {\r\n @let v = activeVariation();\r\n <div class=\"space-y-8\">\r\n\r\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\r\n purchase info column, especially once a product has specs/variants to list; a fixed\r\n max-width on the gallery alone (tried first) just left dead space beside it instead of\r\n letting the info column actually grow into it. -->\r\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\r\n\r\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\r\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\r\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\r\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\r\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\r\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\r\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\r\n header. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\r\n <div class=\"relative aspect-4/3\">\r\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\r\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <ng-template clxSlide>\r\n <div class=\"w-full h-full p-9\">\r\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\r\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\r\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\r\n </div>\r\n </ng-template>\r\n }\r\n </clx-carousel>\r\n </div>\r\n @if (_tagSvc.configured()) {\r\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\r\n } @else if (v && !v.in_stock) {\r\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\r\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\r\n </div>\r\n }\r\n @if (purchasable()) {\r\n <div class=\"absolute top-3 right-3 z-10\">\r\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\r\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\r\n (click)=\"wishlist.emit()\">\r\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <img [src]=\"img\" [alt]=\"p.product_name\"\r\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\r\n [class]=\"_carousel()?.currentPageIndex() === i\r\n ? _thumbBorderClass() + ' opacity-100'\r\n : 'border-transparent opacity-60 hover:opacity-90'\"\r\n loading=\"lazy\"\r\n (click)=\"_carousel()?.goTo(i)\" />\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\r\n\r\n <!-- Stock status + tags \u2014 over the gallery instead when the host configured its tags -->\r\n @if (v && !_tagSvc.configured()) {\r\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\r\n @if (v.in_stock) {\r\n @if (v.stock_quantity != null) {\r\n {{ v.stock_quantity }} en stock\r\n } @else {\r\n En stock\r\n }\r\n } @else {\r\n Sin stock\r\n }\r\n </span>\r\n }\r\n\r\n <!-- Tags (st_products.tags) -->\r\n <div class=\"flex flex-wrap gap-1\" [class.hidden]=\"_tagSvc.configured()\">\r\n @for (t of _tags(); track t.code) {\r\n <span clx-badge [variant]=\"t.variant\" [color]=\"t.color\" size=\"sm\" shape=\"pill\">\r\n {{ t.label }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Title -->\r\n <div>\r\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\r\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\r\n @if (p.product_detail) {\r\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\r\n }\r\n </div>\r\n\r\n <!-- Rating -->\r\n @if (purchasable()) {\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\r\n </div>\r\n }\r\n\r\n <!-- Price block -->\r\n <div>\r\n <div class=\"flex items-baseline gap-3\">\r\n <span class=\"text-2xl font-black text-clx-text-label\">\r\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n @if (discount() > 0) {\r\n <span class=\"text-base text-clx-text-subtle line-through\">\r\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\r\n -{{ discount() }}%\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\r\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\r\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\r\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\r\n @for (specType of p.spec_types; track specType.id) {\r\n @if (specType.input_type === 'color') {\r\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button\r\n type=\"button\"\r\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\r\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\r\n [style.background-color]=\"opt.value\"\r\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\r\n [clxTooltip]=\"opt.label ?? opt.value\"\r\n clxTooltipPosition=\"top\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n } @else {\r\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button clx-button\r\n type=\"button\"\r\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\r\n [color]=\"_specButtonColor()\"\r\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\r\n size=\"sm\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n {{ opt.label ?? opt.value }}\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n }\r\n }\r\n\r\n @if (purchasable()) {\r\n <!-- Quantity -->\r\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\r\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\r\n </clx-number>\r\n\r\n <!-- CTA -->\r\n @if (v?.in_stock && !isSelectionComplete()) {\r\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\r\n }\r\n <div class=\"flex gap-3\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\r\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\r\n </div>\r\n }\r\n\r\n <!-- Host actions (e.g. an ERP's share options) -->\r\n <ng-content select=\"[clxProductDetailActions]\" />\r\n\r\n </div>\r\n </div>\r\n\r\n @if (!compact()) {\r\n <!-- \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\r\n\r\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\r\n <ng-template [clxTabPanel]=\"'general'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsLeft(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsRight(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\r\n <ng-template [clxTabPanel]=\"'specs'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsLeft(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsRight(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\r\n <ng-template [clxTabPanel]=\"'description'\">\r\n <div class=\"space-y-5\">\r\n @for (block of p.descriptions; track block.id) {\r\n <div>\r\n @if (block.title) {\r\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\r\n }\r\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n </clx-tabs>\r\n\r\n <!-- \u2500\u2500 Reviews \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n @if (purchasable()) {\r\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\r\n\r\n <div class=\"space-y-3\">\r\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\r\n <div class=\"flex items-end gap-3\">\r\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <div class=\"pb-1 space-y-1\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\r\n </div>\r\n </div>\r\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\r\n <div class=\"pt-2 space-y-2\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\r\n (click)=\"leaveReview.emit()\">\r\n Dejar una rese\u00F1a\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"lg:col-span-2 space-y-4\">\r\n @for (review of p.reviews ?? []; track review.id) {\r\n <div [class]=\"_reviewCardClass()\">\r\n <div class=\"flex items-center gap-3 px-4 py-3\">\r\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\r\n <span class=\"text-sm font-bold text-clx-text-muted\">\r\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\r\n </span>\r\n </div>\r\n <div class=\"flex-1 min-w-0\">\r\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\r\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\r\n </div>\r\n @if (review.is_verified) {\r\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\r\n }\r\n </div>\r\n <div class=\"px-4 pb-4 space-y-2\">\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\r\n @if (review.title) {\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\r\n }\r\n </div>\r\n @if (review.body) {\r\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\r\n }\r\n <div class=\"flex items-center gap-3 pt-1\">\r\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n }\r\n }\r\n\r\n </div>\r\n}\r\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ClxButtonComponent, selector: "button[clx-button], a[clx-button]", inputs: ["variant", "color", "emphasis", "textColor", "size", "shape", "loading", "disabled", "block", "icon", "iconPosition", "iconOnly", "badge", "badgeColor"] }, { kind: "component", type: ClxButtonGroupComponent, selector: "clx-button-group", inputs: ["label", "shape", "attached", "orientation"] }, { kind: "component", type: ClxBadgeComponent, selector: "span[clx-badge]", inputs: ["variant", "color", "size", "shape", "positioned", "textColor"] }, { kind: "component", type: ClxProductBadgesComponent, selector: "clx-product-badges", inputs: ["badges", "size", "topRightOffset", "bottomRightOffset"] }, { kind: "component", type: ClxIconComponent, selector: "span[clx-icon]", inputs: ["name", "size", "color", "fill"] }, { kind: "component", type: ClxSkeletonComponent, selector: "clx-skeleton", inputs: ["variant", "size", "width", "height"] }, { kind: "component", type: ClxRatingComponent, selector: "clx-rating", inputs: ["value", "max", "color"] }, { kind: "component", type: ClxCarouselComponent, selector: "clx-carousel", inputs: ["clxColor", "autoPlay", "interval", "loop", "aspectRatio", "transparent", "rounded", "pauseOnHover", "arrows"] }, { kind: "directive", type: ClxCarouselDirective, selector: "[clxSlide]" }, { kind: "component", type: ClxTabsComponent, selector: "clx-tabs", inputs: ["tabs", "clxColor", "activeTextColor", "underlineColor", "activeTab"], outputs: ["activeTabChange"] }, { kind: "directive", type: ClxTabDirective, selector: "[clxTabPanel]", inputs: ["clxTabPanel"] }, { kind: "component", type: ClxNumberComponent, selector: "clx-number", inputs: ["variant", "size", "color", "label", "placeholder", "hint", "prefixIcon", "min", "max", "step", "value", "disabled"] }, { kind: "directive", type: ClxTooltipDirective, selector: "[clxTooltip]", inputs: ["clxTooltip", "clxTooltipPosition", "clxTooltipColor", "clxTooltipSize", "clxTooltipDelay"] }, { kind: "component", type: ClxListComponent, selector: "clx-list", inputs: ["color", "variant", "size", "numbered"] }, { kind: "component", type: ClxListItemComponent, selector: "clx-list-item", inputs: ["label", "sublabel", "wrapSublabel", "icon", "meta", "metaColor", "disabled"] }, { kind: "pipe", type: CurrencyPipe, name: "currency" }, { kind: "pipe", type: DecimalPipe, name: "number" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
18280
18454
|
}
|
|
18281
18455
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductDetailComponent, decorators: [{
|
|
18282
18456
|
type: Component,
|
|
@@ -18299,8 +18473,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
18299
18473
|
ClxTooltipDirective,
|
|
18300
18474
|
ClxListComponent,
|
|
18301
18475
|
ClxListItemComponent,
|
|
18302
|
-
], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- \u2550\u2550 SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (loading()) {\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\n <div class=\"space-y-3\">\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\n <div class=\"flex gap-2\">\n @for (t of [1,2,3,4]; track t) {\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\n }\n </div>\n </div>\n <div class=\"space-y-4 pt-2\">\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\n </div>\n </div>\n}\n\n<!-- \u2550\u2550 CONTENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n@if (!loading() && product(); as p) {\n @let v = activeVariation();\n <div class=\"space-y-8\">\n\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\n purchase info column, especially once a product has specs/variants to list; a fixed\n max-width on the gallery alone (tried first) just left dead space beside it instead of\n letting the info column actually grow into it. -->\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\n\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\n header. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\n <div class=\"relative aspect-4/3\">\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <ng-template clxSlide>\n <div class=\"w-full h-full p-9\">\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\n </div>\n </ng-template>\n }\n </clx-carousel>\n </div>\n @if (_tagSvc.configured()) {\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\n } @else if (v && !v.in_stock) {\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\n </div>\n }\n <div class=\"absolute top-3 right-3 z-10\">\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\n (click)=\"wishlist.emit()\">\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\n </button>\n </div>\n </div>\n <div class=\"flex flex-wrap gap-2\">\n @for (img of v?.images ?? []; track i; let i = $index) {\n <img [src]=\"img\" [alt]=\"p.product_name\"\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\n [class]=\"_carousel()?.currentPageIndex() === i\n ? _thumbBorderClass() + ' opacity-100'\n : 'border-transparent opacity-60 hover:opacity-90'\"\n loading=\"lazy\"\n (click)=\"_carousel()?.goTo(i)\" />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\n\n <!-- Stock status + tags \u2014 over the gallery instead when the host configured its tags -->\n @if (v && !_tagSvc.configured()) {\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\n @if (v.in_stock) {\n @if (v.stock_quantity != null) {\n {{ v.stock_quantity }} en stock\n } @else {\n En stock\n }\n } @else {\n Sin stock\n }\n </span>\n }\n\n <!-- Tags (st_products.tags) -->\n <div class=\"flex flex-wrap gap-1\" [class.hidden]=\"_tagSvc.configured()\">\n @for (tag of p.tags; track tag) {\n <span clx-badge [variant]=\"tagVariant(tag)\" [color]=\"tagColor(tag)\" size=\"sm\" shape=\"pill\">\n {{ tagLabel(tag) }}\n </span>\n }\n </div>\n\n <!-- Title -->\n <div>\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\n @if (p.product_detail) {\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\n }\n </div>\n\n <!-- Rating -->\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\n </div>\n\n <!-- Price block -->\n <div>\n <div class=\"flex items-baseline gap-3\">\n <span class=\"text-2xl font-black text-clx-text-label\">\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n @if (discount() > 0) {\n <span class=\"text-base text-clx-text-subtle line-through\">\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\n </span>\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\n -{{ discount() }}%\n </span>\n }\n </div>\n </div>\n\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\n @for (specType of p.spec_types; track specType.id) {\n @if (specType.input_type === 'color') {\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button\n type=\"button\"\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\n [style.background-color]=\"opt.value\"\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\n [clxTooltip]=\"opt.label ?? opt.value\"\n clxTooltipPosition=\"top\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n </button>\n }\n }\n </clx-button-group>\n } @else {\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\n @for (opt of specType.options; track opt.id) {\n @if (isOptionAvailable(specType.id, opt.id)) {\n <button clx-button\n type=\"button\"\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\n [color]=\"_specButtonColor()\"\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\n size=\"sm\"\n (click)=\"selectSpec(specType.id, opt.id)\">\n {{ opt.label ?? opt.value }}\n </button>\n }\n }\n </clx-button-group>\n }\n }\n\n <!-- Quantity -->\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\n </clx-number>\n\n <!-- CTA -->\n @if (v?.in_stock && !isSelectionComplete()) {\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\n }\n <div class=\"flex gap-3\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\n </div>\n\n </div>\n </div>\n\n @if (!compact()) {\n <!-- \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\n\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\n <ng-template [clxTabPanel]=\"'general'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsLeft(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (spec of specsRight(p.product_specs); track spec.id) {\n <clx-list-item [icon]=\"spec.icon ?? ''\">\n <div clxContent>\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\n </div>\n </clx-list-item>\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\n <ng-template [clxTabPanel]=\"'specs'\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsLeft(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\n @for (st of specsRight(p.spec_types); track st.id) {\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\n }\n </clx-list>\n </div>\n </ng-template>\n\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\n <ng-template [clxTabPanel]=\"'description'\">\n <div class=\"space-y-5\">\n @for (block of p.descriptions; track block.id) {\n <div>\n @if (block.title) {\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\n }\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\n </div>\n }\n </div>\n </ng-template>\n\n </clx-tabs>\n\n <!-- \u2500\u2500 Reviews \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\n\n <div class=\"space-y-3\">\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\n <div class=\"flex items-end gap-3\">\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\n <div class=\"pb-1 space-y-1\">\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\n </div>\n </div>\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\n <div class=\"pt-2 space-y-2\">\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\n (click)=\"leaveReview.emit()\">\n Dejar una rese\u00F1a\n </button>\n </div>\n </div>\n\n <div class=\"lg:col-span-2 space-y-4\">\n @for (review of p.reviews ?? []; track review.id) {\n <div [class]=\"_reviewCardClass()\">\n <div class=\"flex items-center gap-3 px-4 py-3\">\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\n <span class=\"text-sm font-bold text-clx-text-muted\">\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\n </span>\n </div>\n <div class=\"flex-1 min-w-0\">\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\n </div>\n @if (review.is_verified) {\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\n }\n </div>\n <div class=\"px-4 pb-4 space-y-2\">\n <div class=\"flex items-center gap-2\">\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\n @if (review.title) {\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\n }\n </div>\n @if (review.body) {\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\n }\n <div class=\"flex items-center gap-3 pt-1\">\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\n </div>\n </div>\n </div>\n }\n </div>\n\n </div>\n }\n\n </div>\n}\n" }]
|
|
18303
|
-
}], ctorParameters: () => [], propDecorators: { product: [{ type: i0.Input, args: [{ isSignal: true, alias: "product", required: false }] }], initialVariationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialVariationId", required: false }] }], clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], buyNowColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowColor", required: false }] }], buyNowTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowTextColor", required: false }] }], addToCartColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartColor", required: false }] }], addToCartTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartTextColor", required: false }] }], leaveReviewColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewColor", required: false }] }], leaveReviewTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewTextColor", required: false }] }], wishlistColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistColor", required: false }] }], wishlistTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistTextColor", required: false }] }], wishlistVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistVariant", required: false }] }], stockBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "stockBadgeColor", required: false }] }], discountBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "discountBadgeColor", required: false }] }], quantityColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "quantityColor", required: false }] }], specButtonColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonColor", required: false }] }], specButtonTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonTextColor", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], addToCart: [{ type: i0.Output, args: ["addToCart"] }], buyNow: [{ type: i0.Output, args: ["buyNow"] }], wishlist: [{ type: i0.Output, args: ["wishlist"] }], variationChange: [{ type: i0.Output, args: ["variationChange"] }], leaveReview: [{ type: i0.Output, args: ["leaveReview"] }], _carousel: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ClxCarouselComponent), { isSignal: true }] }] } });
|
|
18476
|
+
], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- \u2550\u2550 SKELETON \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (loading()) {\r\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-8 p-6\">\r\n <div class=\"space-y-3\">\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"420px\" class=\"rounded-2xl\"></clx-skeleton>\r\n <div class=\"flex gap-2\">\r\n @for (t of [1,2,3,4]; track t) {\r\n <clx-skeleton variant=\"rectangular\" width=\"80px\" height=\"80px\" class=\"rounded-xl\"></clx-skeleton>\r\n }\r\n </div>\r\n </div>\r\n <div class=\"space-y-4 pt-2\">\r\n <clx-skeleton variant=\"text\" size=\"lg\" width=\"70%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"sm\" width=\"40%\"></clx-skeleton>\r\n <clx-skeleton variant=\"text\" size=\"xl\" width=\"50%\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"100px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n <clx-skeleton variant=\"rectangular\" width=\"100%\" height=\"52px\" class=\"rounded-xl\"></clx-skeleton>\r\n </div>\r\n </div>\r\n}\r\n\r\n<!-- \u2550\u2550 CONTENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\r\n@if (!loading() && product(); as p) {\r\n @let v = activeVariation();\r\n <div class=\"space-y-8\">\r\n\r\n <!-- 2fr/3fr instead of an even 50/50 split \u2014 the gallery doesn't need as much width as the\r\n purchase info column, especially once a product has specs/variants to list; a fixed\r\n max-width on the gallery alone (tried first) just left dead space beside it instead of\r\n letting the info column actually grow into it. -->\r\n <div class=\"grid grid-cols-1 md:grid-cols-[2fr_3fr] gap-8 md:items-start\">\r\n\r\n <!-- \u2500\u2500 Column 1: Image Gallery \u2014 each column sizes to its own content (no forced equal\r\n height, no position:sticky). A `sticky` gallery reads great in isolation, but a host app\r\n that already has its own `position: sticky` header (a full-width top bar, as opposed to\r\n this library's own `clx-app-layout` sidebar, which is `fixed` and never competes) forces\r\n the browser to keep recomputing both stickies' viewport-relative offsets together on every\r\n resize \u2014 confirmed as the source of a persistent resize-lag bug in codexly-store's product\r\n page. Not worth reintroducing until sticky positioning is made robust to an arbitrary host\r\n header. \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div [class]=\"_panelClass() + ' flex flex-col gap-3'\">\r\n <div class=\"relative aspect-4/3\">\r\n <div [class]=\"v && !v.in_stock ? 'opacity-40 grayscale' : ''\">\r\n <clx-carousel [clxColor]=\"clxColor()\" aspectRatio=\"4/3\" [loop]=\"true\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <ng-template clxSlide>\r\n <div class=\"w-full h-full p-9\">\r\n <img [src]=\"img\" [alt]=\"p.product_name\" class=\"w-full h-full object-contain\"\r\n [loading]=\"i === 0 ? 'eager' : 'lazy'\"\r\n [attr.fetchpriority]=\"i === 0 ? 'high' : null\" />\r\n </div>\r\n </ng-template>\r\n }\r\n </clx-carousel>\r\n </div>\r\n @if (_tagSvc.configured()) {\r\n <clx-product-badges [badges]=\"_badges()\" topRightOffset=\"top-16\" />\r\n } @else if (v && !v.in_stock) {\r\n <div class=\"absolute inset-0 flex items-center justify-center pointer-events-none z-10\">\r\n <span clx-badge variant=\"solid\" color=\"slate\" size=\"md\" shape=\"pill\">Agotado</span>\r\n </div>\r\n }\r\n @if (purchasable()) {\r\n <div class=\"absolute top-3 right-3 z-10\">\r\n <button clx-button type=\"button\" [variant]=\"wishlistVariant()\" [color]=\"_wishlistColor()\" [textColor]=\"wishlistTextColor()\" shape=\"circle\" size=\"md\"\r\n [iconOnly]=\"true\" clxTooltip=\"Agregar a favoritos\" [clxTooltipColor]=\"_wishlistColor()\" clxTooltipPosition=\"left\"\r\n (click)=\"wishlist.emit()\">\r\n <span clx-icon name=\"favorite_border\" size=\"sm\"></span>\r\n </button>\r\n </div>\r\n }\r\n </div>\r\n <div class=\"flex flex-wrap gap-2\">\r\n @for (img of v?.images ?? []; track i; let i = $index) {\r\n <img [src]=\"img\" [alt]=\"p.product_name\"\r\n class=\"w-20 h-20 rounded-xl object-cover border-[3px] cursor-pointer shrink-0 transition-[border-color,opacity]\"\r\n [class]=\"_carousel()?.currentPageIndex() === i\r\n ? _thumbBorderClass() + ' opacity-100'\r\n : 'border-transparent opacity-60 hover:opacity-90'\"\r\n loading=\"lazy\"\r\n (click)=\"_carousel()?.goTo(i)\" />\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- \u2500\u2500 Column 2: Purchase Info \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <div [class]=\"_panelClass() + ' space-y-2.5'\">\r\n\r\n <!-- Stock status + tags \u2014 over the gallery instead when the host configured its tags -->\r\n @if (v && !_tagSvc.configured()) {\r\n <span clx-badge variant=\"solid\" [color]=\"v.in_stock ? _stockBadgeColor() : 'red'\" size=\"sm\" shape=\"pill\">\r\n @if (v.in_stock) {\r\n @if (v.stock_quantity != null) {\r\n {{ v.stock_quantity }} en stock\r\n } @else {\r\n En stock\r\n }\r\n } @else {\r\n Sin stock\r\n }\r\n </span>\r\n }\r\n\r\n <!-- Tags (st_products.tags) -->\r\n <div class=\"flex flex-wrap gap-1\" [class.hidden]=\"_tagSvc.configured()\">\r\n @for (t of _tags(); track t.code) {\r\n <span clx-badge [variant]=\"t.variant\" [color]=\"t.color\" size=\"sm\" shape=\"pill\">\r\n {{ t.label }}\r\n </span>\r\n }\r\n </div>\r\n\r\n <!-- Title -->\r\n <div>\r\n <p class=\"text-sm text-clx-text-subtle uppercase tracking-wide font-medium\">{{ p.trademark }}</p>\r\n <h1 class=\"text-2xl font-bold text-clx-text-label leading-tight mt-0.5\">{{ v?.name || p.product_name }}</h1>\r\n @if (p.product_detail) {\r\n <p class=\"text-clx-text-muted text-sm mt-1\">{{ p.product_detail }}</p>\r\n }\r\n </div>\r\n\r\n <!-- Rating -->\r\n @if (purchasable()) {\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <span class=\"text-sm text-clx-text-subtle\">({{ p.review_count }} reviews)</span>\r\n </div>\r\n }\r\n\r\n <!-- Price block -->\r\n <div>\r\n <div class=\"flex items-baseline gap-3\">\r\n <span class=\"text-2xl font-black text-clx-text-label\">\r\n {{ finalPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n @if (discount() > 0) {\r\n <span class=\"text-base text-clx-text-subtle line-through\">\r\n {{ listPrice() | currency:currency():'$':'1.0-0' }}\r\n </span>\r\n <span clx-badge variant=\"solid\" [color]=\"_discountBadgeColor()\" size=\"sm\" shape=\"pill\">\r\n -{{ discount() }}%\r\n </span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Selectores de variaci\u00F3n (st_spec_types + st_spec_options) \u2014 opciones no disponibles\r\n para la combinaci\u00F3n actual (ej. Talla S con Color Rojo) se ocultan por completo en vez\r\n de mostrarse deshabilitadas; reaparecen si el usuario cambia a una combinaci\u00F3n donde s\u00ED\r\n aplican, ya que isOptionAvailable() se reeval\u00FAa contra selectedSpecs() en cada cambio. -->\r\n @for (specType of p.spec_types; track specType.id) {\r\n @if (specType.input_type === 'color') {\r\n <clx-button-group [label]=\"specType.name\" shape=\"circular\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button\r\n type=\"button\"\r\n class=\"w-7 h-7 rounded-full border-2 transition-[transform,box-shadow] shrink-0 cursor-pointer\"\r\n [class]=\"isSpecSelected(specType.id, opt.id) ? 'scale-110 shadow-md' : 'opacity-90 hover:opacity-100'\"\r\n [style.background-color]=\"opt.value\"\r\n [style.border-color]=\"isSpecSelected(specType.id, opt.id) ? 'var(--clx-text-label, #111)' : 'transparent'\"\r\n [clxTooltip]=\"opt.label ?? opt.value\"\r\n clxTooltipPosition=\"top\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n } @else {\r\n <clx-button-group [label]=\"specType.name\" shape=\"flat\" [attached]=\"false\">\r\n @for (opt of specType.options; track opt.id) {\r\n @if (isOptionAvailable(specType.id, opt.id)) {\r\n <button clx-button\r\n type=\"button\"\r\n [variant]=\"isSpecSelected(specType.id, opt.id) ? 'solid' : 'outline'\"\r\n [color]=\"_specButtonColor()\"\r\n [textColor]=\"isSpecSelected(specType.id, opt.id) ? specButtonTextColor() : undefined\"\r\n size=\"sm\"\r\n (click)=\"selectSpec(specType.id, opt.id)\">\r\n {{ opt.label ?? opt.value }}\r\n </button>\r\n }\r\n }\r\n </clx-button-group>\r\n }\r\n }\r\n\r\n @if (purchasable()) {\r\n <!-- Quantity -->\r\n <clx-number label=\"Cantidad:\" variant=\"stepper\" [color]=\"_quantityColor()\"\r\n [min]=\"1\" [max]=\"99\" [ngModel]=\"quantity()\" (ngModelChange)=\"quantity.set($event)\">\r\n </clx-number>\r\n\r\n <!-- CTA -->\r\n @if (v?.in_stock && !isSelectionComplete()) {\r\n <p class=\"text-sm text-red-500\">Selecciona {{ missingSpecNames().join(' y ') }} para continuar.</p>\r\n }\r\n <div class=\"flex gap-3\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_buyNowColor()\" [textColor]=\"buyNowTextColor()\" size=\"md\" [block]=\"true\" icon=\"bolt\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitBuyNow()\">Comprar ahora</button>\r\n <button clx-button type=\"button\" variant=\"light\" emphasis=\"secondary\" [color]=\"addToCartColor()\" [textColor]=\"addToCartTextColor()\" size=\"md\" [block]=\"true\" icon=\"shopping_cart\"\r\n [disabled]=\"!v?.in_stock || !isSelectionComplete()\" (click)=\"_emitAddToCart()\">Agrega al carrito</button>\r\n </div>\r\n }\r\n\r\n <!-- Host actions (e.g. an ERP's share options) -->\r\n <ng-content select=\"[clxProductDetailActions]\" />\r\n\r\n </div>\r\n </div>\r\n\r\n @if (!compact()) {\r\n <!-- \u2500\u2500 Tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n <clx-tabs [tabs]=\"tabItems\" [clxColor]=\"clxColor()\" [(activeTab)]=\"activeTab\">\r\n\r\n <!-- Tab: Caracter\u00EDsticas generales (st_product_specs \u2014 ficha t\u00E9cnica global) -->\r\n <ng-template [clxTabPanel]=\"'general'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsLeft(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (spec of specsRight(p.product_specs); track spec.id) {\r\n <clx-list-item [icon]=\"spec.icon ?? ''\">\r\n <div clxContent>\r\n <p class=\"text-sm font-medium text-clx-text-label\">{{ spec.name }}:</p>\r\n <p class=\"text-sm text-clx-text-muted mt-0.5\">{{ spec.value }}</p>\r\n </div>\r\n </clx-list-item>\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Especificaciones (st_spec_types \u2014 specs de la variaci\u00F3n activa) -->\r\n <ng-template [clxTabPanel]=\"'specs'\">\r\n <div class=\"grid grid-cols-1 sm:grid-cols-2 gap-x-8\">\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsLeft(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n <clx-list [color]=\"clxColor()\" variant=\"flush\" size=\"sm\">\r\n @for (st of specsRight(p.spec_types); track st.id) {\r\n <clx-list-item [label]=\"st.name + ':'\" [sublabel]=\"selectedOptionLabel(st)\" [icon]=\"st.icon ?? ''\" />\r\n }\r\n </clx-list>\r\n </div>\r\n </ng-template>\r\n\r\n <!-- Tab: Descripci\u00F3n (st_product_descriptions) -->\r\n <ng-template [clxTabPanel]=\"'description'\">\r\n <div class=\"space-y-5\">\r\n @for (block of p.descriptions; track block.id) {\r\n <div>\r\n @if (block.title) {\r\n <h3 class=\"text-base font-semibold text-clx-text-label mb-1\">{{ block.title }}</h3>\r\n }\r\n <p class=\"text-sm text-clx-text-muted leading-relaxed\">{{ block.body }}</p>\r\n </div>\r\n }\r\n </div>\r\n </ng-template>\r\n\r\n </clx-tabs>\r\n\r\n <!-- \u2500\u2500 Reviews \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\r\n @if (purchasable()) {\r\n <div class=\"grid grid-cols-1 xl:grid-cols-3 gap-8 border-t border-clx-border pt-8\">\r\n\r\n <div class=\"space-y-3\">\r\n <h2 class=\"text-base font-bold text-clx-text-label\">Opiniones de clientes</h2>\r\n <div class=\"flex items-end gap-3\">\r\n <span class=\"text-5xl font-black text-clx-text-label\">{{ p.rating | number:'1.1-1' }}</span>\r\n <div class=\"pb-1 space-y-1\">\r\n <clx-rating [value]=\"p.rating\" [color]=\"clxColor()\"></clx-rating>\r\n <p class=\"text-xs text-clx-text-muted\">{{ p.review_count }} valoraciones verificadas</p>\r\n </div>\r\n </div>\r\n <p class=\"text-sm font-semibold\" [class]=\"_ratingLabelClass()\">{{ _ratingLabel() }}</p>\r\n <div class=\"pt-2 space-y-2\">\r\n <button clx-button type=\"button\" variant=\"solid\" [color]=\"_leaveReviewColor()\" [textColor]=\"leaveReviewTextColor()\" size=\"md\" [block]=\"true\" icon=\"rate_review\"\r\n (click)=\"leaveReview.emit()\">\r\n Dejar una rese\u00F1a\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"lg:col-span-2 space-y-4\">\r\n @for (review of p.reviews ?? []; track review.id) {\r\n <div [class]=\"_reviewCardClass()\">\r\n <div class=\"flex items-center gap-3 px-4 py-3\">\r\n <div class=\"w-10 h-10 rounded-full bg-clx-surface-2 flex items-center justify-center shrink-0\">\r\n <span class=\"text-sm font-bold text-clx-text-muted\">\r\n {{ review.author_name.substring(0,1) }}{{ review.author_name.split(' ')[1]?.substring(0,1) ?? '' }}\r\n </span>\r\n </div>\r\n <div class=\"flex-1 min-w-0\">\r\n <p class=\"font-semibold text-sm text-clx-text-label truncate\">{{ review.author_name }}</p>\r\n <p class=\"text-xs text-clx-text-subtle\">{{ review.created_at }}</p>\r\n </div>\r\n @if (review.is_verified) {\r\n <span clx-badge variant=\"light\" color=\"emerald\" size=\"sm\" shape=\"pill\">Verified</span>\r\n }\r\n </div>\r\n <div class=\"px-4 pb-4 space-y-2\">\r\n <div class=\"flex items-center gap-2\">\r\n <clx-rating [value]=\"review.rating\" [color]=\"clxColor()\"></clx-rating>\r\n @if (review.title) {\r\n <span class=\"text-sm font-semibold text-clx-text-label\">{{ review.title }}</span>\r\n }\r\n </div>\r\n @if (review.body) {\r\n <p class=\"text-sm text-clx-text-muted\">{{ review.body }}</p>\r\n }\r\n <div class=\"flex items-center gap-3 pt-1\">\r\n <span class=\"text-xs text-clx-text-subtle\">\u00BFTe result\u00F3 \u00FAtil?</span>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_up\">{{ review.helpful }}</button>\r\n <button clx-button type=\"button\" variant=\"ghost\" color=\"slate\" size=\"xs\" icon=\"thumb_down\">{{ review.not_helpful }}</button>\r\n </div>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n </div>\r\n }\r\n }\r\n\r\n </div>\r\n}\r\n" }]
|
|
18477
|
+
}], ctorParameters: () => [], propDecorators: { product: [{ type: i0.Input, args: [{ isSignal: true, alias: "product", required: false }] }], initialVariationId: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialVariationId", required: false }] }], clxColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "clxColor", required: false }] }], buyNowColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowColor", required: false }] }], buyNowTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "buyNowTextColor", required: false }] }], addToCartColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartColor", required: false }] }], addToCartTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "addToCartTextColor", required: false }] }], leaveReviewColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewColor", required: false }] }], leaveReviewTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaveReviewTextColor", required: false }] }], wishlistColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistColor", required: false }] }], wishlistTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistTextColor", required: false }] }], wishlistVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "wishlistVariant", required: false }] }], stockBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "stockBadgeColor", required: false }] }], discountBadgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "discountBadgeColor", required: false }] }], quantityColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "quantityColor", required: false }] }], specButtonColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonColor", required: false }] }], specButtonTextColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "specButtonTextColor", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }], purchasable: [{ type: i0.Input, args: [{ isSignal: true, alias: "purchasable", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], addToCart: [{ type: i0.Output, args: ["addToCart"] }], buyNow: [{ type: i0.Output, args: ["buyNow"] }], wishlist: [{ type: i0.Output, args: ["wishlist"] }], variationChange: [{ type: i0.Output, args: ["variationChange"] }], leaveReview: [{ type: i0.Output, args: ["leaveReview"] }], _carousel: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ClxCarouselComponent), { isSignal: true }] }] } });
|
|
18304
18478
|
|
|
18305
18479
|
class ClxProductQuickViewComponent {
|
|
18306
18480
|
ref = inject(CLX_MODAL_REF, { optional: true });
|
|
@@ -18330,7 +18504,7 @@ class ClxProductQuickViewComponent {
|
|
|
18330
18504
|
(buyNow)="_onBuyNow($event)">
|
|
18331
18505
|
</clx-product-detail>
|
|
18332
18506
|
</clx-modal>
|
|
18333
|
-
`, isInline: true, dependencies: [{ kind: "component", type: ClxModalComponent, selector: "clx-modal", inputs: ["size", "showClose", "confirmButton", "cancelButton", "showCancelButton"], outputs: ["confirmClick", "cancelClick"] }, { kind: "component", type: ClxProductDetailComponent, selector: "clx-product-detail", inputs: ["product", "initialVariationId", "clxColor", "buyNowColor", "buyNowTextColor", "addToCartColor", "addToCartTextColor", "leaveReviewColor", "leaveReviewTextColor", "wishlistColor", "wishlistTextColor", "wishlistVariant", "stockBadgeColor", "discountBadgeColor", "quantityColor", "specButtonColor", "specButtonTextColor", "loading", "currency", "compact", "variant"], outputs: ["addToCart", "buyNow", "wishlist", "variationChange", "leaveReview"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18507
|
+
`, isInline: true, dependencies: [{ kind: "component", type: ClxModalComponent, selector: "clx-modal", inputs: ["size", "showClose", "confirmButton", "cancelButton", "showCancelButton"], outputs: ["confirmClick", "cancelClick"] }, { kind: "component", type: ClxProductDetailComponent, selector: "clx-product-detail", inputs: ["product", "initialVariationId", "clxColor", "buyNowColor", "buyNowTextColor", "addToCartColor", "addToCartTextColor", "leaveReviewColor", "leaveReviewTextColor", "wishlistColor", "wishlistTextColor", "wishlistVariant", "stockBadgeColor", "discountBadgeColor", "quantityColor", "specButtonColor", "specButtonTextColor", "loading", "currency", "compact", "purchasable", "variant"], outputs: ["addToCart", "buyNow", "wishlist", "variationChange", "leaveReview"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18334
18508
|
}
|
|
18335
18509
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImport: i0, type: ClxProductQuickViewComponent, decorators: [{
|
|
18336
18510
|
type: Component,
|
|
@@ -20376,5 +20550,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.15", ngImpo
|
|
|
20376
20550
|
* Generated bundle index. Do not edit.
|
|
20377
20551
|
*/
|
|
20378
20552
|
|
|
20379
|
-
export { CLX_ADDON_BORDER, CLX_ADDON_TEXT, CLX_ALERT_OPTIONS, CLX_ALERT_RESOLVE, CLX_BG_ADDON, CLX_BG_DISABLED, CLX_BG_ICON_WRAP, CLX_BG_SECTION, CLX_BG_SURFACE, CLX_BORDER_DEFAULT, CLX_BORDER_DISABLED, CLX_BORDER_MEDIUM, CLX_COLOR_HEX, CLX_COLOR_HEX_100, CLX_COLOR_MAP, CLX_FONT_CATALOG, CLX_MODAL_ANIM_CONFIG, CLX_MODAL_DATA, CLX_MODAL_REF, CLX_OPTION_DISABLED, CLX_PLACEHOLDER, CLX_RADIO_GROUP, CLX_RADIUS_MAP, CLX_TEXT_BODY, CLX_TEXT_DISABLED, CLX_TEXT_HEADING, CLX_TEXT_HINT, CLX_TEXT_IDLE, CLX_TEXT_INPUT, CLX_TEXT_LABEL, CLX_TEXT_OPTION, CLX_TEXT_SUBTITLE, CLX_TEXT_TITLE, CLX_THEME_CONFIG, CLX_THEME_DEFAULTS, CLX_TOAST_DEFAULTS, ClxAlertComponent, ClxAlertListComponent, ClxAlertService, ClxAnimateDirective, ClxAnimateGroupDirective, ClxAnimateService, ClxAppLayoutComponent, ClxAvatarComponent, ClxBadgeComponent, ClxBrandComponent, ClxButtonComponent, ClxButtonGroupComponent, ClxCardBodyDirective, ClxCardComponent, ClxCardExpiryInputComponent, ClxCardFooterDirective, ClxCardHeaderActionsDirective, ClxCardHeaderDirective, ClxCardNumberInputComponent, ClxCardValidators, ClxCarouselComponent, ClxCarouselDirective, ClxCartComponent, ClxCartSummaryDrawer, ClxCellDirective, ClxCheckboxComponent, ClxCollapseComponent, ClxColorPickerComponent, ClxColumnDefDirective, ClxCopyTextComponent, ClxDateRangePickerComponent, ClxDatepickerComponent, ClxDrawerComponent, ClxDrawerService, ClxEditorComponent, ClxEditorImageModalComponent, ClxEditorLinkModalComponent, ClxFabComponent, ClxFilterPanelComponent, ClxHeaderCellDirective, ClxIconComponent, ClxInputComponent, ClxListComponent, ClxListItemComponent, ClxMaintenanceBannerComponent, ClxMenuComponent, ClxMenuItemComponent, ClxMenuItemTrailingDirective, ClxModalComponent, ClxModalService, ClxNativeOverlayService, ClxNavGroupComponent, ClxNotificationComponent, ClxNumberComponent, ClxOtpComponent, ClxPageEmptyComponent, ClxPageHeaderComponent, ClxPageHeaderTitleDirective, ClxPageMaintenanceComponent, ClxPageNotFoundComponent, ClxPageServerErrorComponent, ClxPageUnauthorizedComponent, ClxPaginationComponent, ClxProductBadgesComponent, ClxProductComponent, ClxProductDetailComponent, ClxProductQuickViewComponent, ClxProductTagService, ClxProfileComponent, ClxProgressBarComponent, ClxRadioComponent, ClxRadioGroupComponent, ClxRankedListComponent, ClxRatingComponent, ClxSearchComponent, ClxSelectComponent, ClxSkeletonComponent, ClxSliderComponent, ClxSocialIconComponent, ClxSpinnerComponent, ClxStatCardComponent, ClxStepComponent, ClxStepperComponent, ClxSwitchComponent, ClxTabDirective, ClxTableActionsComponent, ClxTableComponent, ClxTableProductCellComponent, ClxTableProgressCellComponent, ClxTableStatusCellComponent, ClxTabsComponent, ClxTagComponent, ClxTextareaComponent, ClxThemeService, ClxTimelineComponent, ClxTimelineItemComponent, ClxTimepickerComponent, ClxToastComponent, ClxToastContainerComponent, ClxToastService, ClxTooltipComponent, ClxTooltipDirective, ClxTreeComponent, ClxUploadComponent, ClxWidgetCardComponent, ClxWishlistComponent, ClxWizardComponent, TIMEPICKER_SIZE_MAP, detectClxCardBrand, formatClxTime, formatClxWindow, isClxCardExpired, parseColorInput, provideCodexlyTheme, resolveColor, resolveContainerRadius, resolveRadius };
|
|
20553
|
+
export { CLX_ADDON_BORDER, CLX_ADDON_TEXT, CLX_ALERT_OPTIONS, CLX_ALERT_RESOLVE, CLX_BG_ADDON, CLX_BG_DISABLED, CLX_BG_ICON_WRAP, CLX_BG_SECTION, CLX_BG_SURFACE, CLX_BORDER_DEFAULT, CLX_BORDER_DISABLED, CLX_BORDER_MEDIUM, CLX_COLOR_HEX, CLX_COLOR_HEX_100, CLX_COLOR_MAP, CLX_FONT_CATALOG, CLX_MODAL_ANIM_CONFIG, CLX_MODAL_DATA, CLX_MODAL_REF, CLX_OPTION_DISABLED, CLX_PLACEHOLDER, CLX_RADIO_GROUP, CLX_RADIUS_MAP, CLX_TEXT_BODY, CLX_TEXT_DISABLED, CLX_TEXT_HEADING, CLX_TEXT_HINT, CLX_TEXT_IDLE, CLX_TEXT_INPUT, CLX_TEXT_LABEL, CLX_TEXT_OPTION, CLX_TEXT_SUBTITLE, CLX_TEXT_TITLE, CLX_THEME_CONFIG, CLX_THEME_DEFAULTS, CLX_TOAST_DEFAULTS, ClxAlertComponent, ClxAlertListComponent, ClxAlertService, ClxAnimateDirective, ClxAnimateGroupDirective, ClxAnimateService, ClxAppLayoutComponent, ClxAvatarComponent, ClxBadgeComponent, ClxBrandComponent, ClxButtonComponent, ClxButtonGroupComponent, ClxCardBodyDirective, ClxCardComponent, ClxCardExpiryInputComponent, ClxCardFooterDirective, ClxCardHeaderActionsDirective, ClxCardHeaderDirective, ClxCardNumberInputComponent, ClxCardValidators, ClxCarouselComponent, ClxCarouselDirective, ClxCartComponent, ClxCartSummaryDrawer, ClxCellDirective, ClxChannelPreferencesComponent, ClxCheckboxComponent, ClxCollapseComponent, ClxColorPickerComponent, ClxColumnDefDirective, ClxCopyTextComponent, ClxDateRangePickerComponent, ClxDatepickerComponent, ClxDrawerComponent, ClxDrawerService, ClxEditorComponent, ClxEditorImageModalComponent, ClxEditorLinkModalComponent, ClxFabComponent, ClxFilterPanelComponent, ClxHeaderCellDirective, ClxIconComponent, ClxInputComponent, ClxListComponent, ClxListItemComponent, ClxMaintenanceBannerComponent, ClxMenuComponent, ClxMenuItemComponent, ClxMenuItemTrailingDirective, ClxModalComponent, ClxModalService, ClxNativeOverlayService, ClxNavGroupComponent, ClxNotificationComponent, ClxNumberComponent, ClxOtpComponent, ClxPageEmptyComponent, ClxPageHeaderComponent, ClxPageHeaderTitleDirective, ClxPageMaintenanceComponent, ClxPageNotFoundComponent, ClxPageServerErrorComponent, ClxPageUnauthorizedComponent, ClxPaginationComponent, ClxProductBadgesComponent, ClxProductComponent, ClxProductDetailComponent, ClxProductQuickViewComponent, ClxProductTagService, ClxProfileComponent, ClxProgressBarComponent, ClxRadioComponent, ClxRadioGroupComponent, ClxRankedListComponent, ClxRatingComponent, ClxSearchComponent, ClxSelectComponent, ClxSkeletonComponent, ClxSliderComponent, ClxSocialIconComponent, ClxSpinnerComponent, ClxStatCardComponent, ClxStepComponent, ClxStepperComponent, ClxSwitchComponent, ClxTabDirective, ClxTableActionsComponent, ClxTableComponent, ClxTableProductCellComponent, ClxTableProgressCellComponent, ClxTableStatusCellComponent, ClxTabsComponent, ClxTagComponent, ClxTextareaComponent, ClxThemeService, ClxTimelineComponent, ClxTimelineItemComponent, ClxTimepickerComponent, ClxToastComponent, ClxToastContainerComponent, ClxToastService, ClxTooltipComponent, ClxTooltipDirective, ClxTreeComponent, ClxUploadComponent, ClxWidgetCardComponent, ClxWishlistComponent, ClxWizardComponent, TIMEPICKER_SIZE_MAP, detectClxCardBrand, formatClxTime, formatClxWindow, isClxCardExpired, parseColorInput, provideCodexlyTheme, resolveColor, resolveContainerRadius, resolveRadius };
|
|
20380
20554
|
//# sourceMappingURL=codexly-ui.mjs.map
|