2026-07-05 01:05:48 +04:00
|
|
|
<article class="product-card" [class.product-card-compact]="appearance === 'compact'" (mouseenter)="preview.emit(item.itemID)">
|
2026-07-05 01:43:56 +04:00
|
|
|
<a [routerLink]="['/product', item.itemID] | langRoute" class="product-link" (click)="onSelected($event)">
|
2026-07-05 01:05:48 +04:00
|
|
|
<div class="product-image">
|
|
|
|
|
<img [src]="getMainImage(item)" [alt]="title || item.name" loading="lazy" decoding="async" width="300" height="300" />
|
2026-07-09 00:55:50 +04:00
|
|
|
@if (showDiscountBadge && item.discount > 0) {
|
2026-07-25 23:54:08 +04:00
|
|
|
<div class="discount-badge" [attr.aria-label]="'common.discountLabel' | translate:{ value: item.discount }">-{{ item.discount }}%</div>
|
2026-07-05 01:05:48 +04:00
|
|
|
}
|
2026-07-09 00:55:50 +04:00
|
|
|
@if (showStock && item.remainings) {
|
2026-07-19 22:04:13 +04:00
|
|
|
<span class="stock-badge" [class.out]="item.remainings.toLowerCase() === 'out'">{{ stockLabelKey(item.remainings) | translate }}</span>
|
2026-07-09 00:55:50 +04:00
|
|
|
}
|
|
|
|
|
|
2026-07-09 01:13:54 +04:00
|
|
|
@if (showFavoriteControl() || showCompareControl() || showShareAction || showQuickViewPlaceholder) {
|
2026-07-09 00:55:50 +04:00
|
|
|
<div class="product-actions-overlay">
|
2026-07-09 01:13:54 +04:00
|
|
|
@if (showFavoriteControl()) {
|
fix(storefront): premium UX polish for home, catalog, search
- product-card: normalize hardcoded hex colors to design tokens
(--text-primary, --border-color, --primary-color, --bg-tertiary,
--bg-secondary); stock bar and stock badge now use semantic
--success-color/--warning-color/--error-color instead of
near-duplicate literal hex; add-to-cart hover uses --primary-hover
and --transition-* tokens; card hover shadow uses --shadow-lg
- product-card: add aria-pressed to favorite/compare toggle buttons
so their selected state isn't color-only
- filters-panel: add aria-pressed to color/size/rating filter chips;
add a visible checkmark glyph on selected color swatches plus a
focus-style selection ring, so selection isn't conveyed by border
color alone
- layout-switcher: add aria-pressed to the active layout button
- catalog-container: add aria-current to the mobile sort-sheet and
grid-sheet option buttons; active sort option gets a checkmark
and bold weight instead of color-only highlighting
- category-grid: normalize hardcoded border/background/text colors
to tokens; align focus ring with the color-mix pattern used
elsewhere in catalog
- search-results, sorting-control: normalize skeleton/select colors
to tokens; sort <select> gets a hover border state
- home: convert loading-grid/empty-state spacing to --space-* tokens
Build verified green via `npm run build`.
Out of scope / skipped:
- pages/category and pages/search retain their existing hand-rolled
skeleton markup (not app-skeleton) - replacing it is a composition
change, not covered by this visual-polish pass
- product-card rating-stars color and legacy pages/category,
pages/search hex literals left as-is where no exact token match
exists, to avoid an unintended visual shift
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:34:05 +04:00
|
|
|
<button type="button" class="product-action-btn product-action-favorite" [attr.aria-label]="'catalog.favorite' | translate" [attr.aria-pressed]="isFavorite" [class.active]="isFavorite" (click)="onFavoritePlaceholder($event)">❤</button>
|
2026-07-09 00:55:50 +04:00
|
|
|
}
|
2026-07-09 01:13:54 +04:00
|
|
|
@if (showCompareControl()) {
|
fix(storefront): premium UX polish for home, catalog, search
- product-card: normalize hardcoded hex colors to design tokens
(--text-primary, --border-color, --primary-color, --bg-tertiary,
--bg-secondary); stock bar and stock badge now use semantic
--success-color/--warning-color/--error-color instead of
near-duplicate literal hex; add-to-cart hover uses --primary-hover
and --transition-* tokens; card hover shadow uses --shadow-lg
- product-card: add aria-pressed to favorite/compare toggle buttons
so their selected state isn't color-only
- filters-panel: add aria-pressed to color/size/rating filter chips;
add a visible checkmark glyph on selected color swatches plus a
focus-style selection ring, so selection isn't conveyed by border
color alone
- layout-switcher: add aria-pressed to the active layout button
- catalog-container: add aria-current to the mobile sort-sheet and
grid-sheet option buttons; active sort option gets a checkmark
and bold weight instead of color-only highlighting
- category-grid: normalize hardcoded border/background/text colors
to tokens; align focus ring with the color-mix pattern used
elsewhere in catalog
- search-results, sorting-control: normalize skeleton/select colors
to tokens; sort <select> gets a hover border state
- home: convert loading-grid/empty-state spacing to --space-* tokens
Build verified green via `npm run build`.
Out of scope / skipped:
- pages/category and pages/search retain their existing hand-rolled
skeleton markup (not app-skeleton) - replacing it is a composition
change, not covered by this visual-polish pass
- product-card rating-stars color and legacy pages/category,
pages/search hex literals left as-is where no exact token match
exists, to avoid an unintended visual shift
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:34:05 +04:00
|
|
|
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.compare' | translate" [attr.aria-pressed]="isCompared" [class.active]="isCompared" (click)="onComparePlaceholder($event)">⇄</button>
|
2026-07-09 01:13:54 +04:00
|
|
|
}
|
|
|
|
|
@if (showShareAction) {
|
2026-07-09 02:43:39 +04:00
|
|
|
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.share' | translate" (click)="onShare($event)">↗</button>
|
2026-07-09 00:55:50 +04:00
|
|
|
}
|
|
|
|
|
@if (showQuickViewPlaceholder) {
|
2026-07-09 02:43:39 +04:00
|
|
|
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.quickView' | translate" (click)="onQuickViewPlaceholder($event)">○</button>
|
2026-07-09 00:55:50 +04:00
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 01:05:48 +04:00
|
|
|
@if (item.badges && item.badges.length > 0) {
|
|
|
|
|
<div class="product-badges-overlay">
|
2026-07-09 01:40:22 +04:00
|
|
|
@for (badge of item.badges; track $index) {
|
2026-07-05 01:05:48 +04:00
|
|
|
<span class="product-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="product-details">
|
|
|
|
|
<h3 class="product-name">{{ title || item.name }}</h3>
|
|
|
|
|
|
|
|
|
|
@if (showDescription && description) {
|
2026-07-19 22:56:12 +04:00
|
|
|
<p class="product-description">{{ cleanDescription(description) }}</p>
|
2026-07-05 01:05:48 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@if (showRating) {
|
2026-07-25 23:54:08 +04:00
|
|
|
<div class="product-rating" [attr.aria-label]="'common.ratingLabel' | translate:{ value: item.rating }">
|
2026-07-05 01:05:48 +04:00
|
|
|
<span class="rating-stars">{{ item.rating | number:'1.1-1' }}</span>
|
|
|
|
|
<span class="rating-count">({{ item.callbacks?.length || item.comments?.length || 0 }})</span>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<div class="product-price">
|
|
|
|
|
@if (item.discount > 0) {
|
|
|
|
|
<span class="original-price">{{ item.price | number:'1.2-2' }} {{ item.currency }}</span>
|
|
|
|
|
<span class="discounted-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}</span>
|
|
|
|
|
} @else {
|
|
|
|
|
<span class="current-price">{{ item.price | number:'1.2-2' }} {{ item.currency }}</span>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@if (showStock && item.remainings) {
|
|
|
|
|
<div class="product-stock" aria-hidden="true">
|
|
|
|
|
<div class="stock-bar">
|
|
|
|
|
<span class="bar-segment" [class.filled]="item.remainings === 'high' || item.remainings === 'medium' || item.remainings === 'low'" [class.high]="item.remainings === 'high'" [class.medium]="item.remainings === 'medium'" [class.low]="item.remainings === 'low'"></span>
|
|
|
|
|
<span class="bar-segment" [class.filled]="item.remainings === 'high' || item.remainings === 'medium'" [class.high]="item.remainings === 'high'" [class.medium]="item.remainings === 'medium'"></span>
|
|
|
|
|
<span class="bar-segment" [class.filled]="item.remainings === 'high'" [class.high]="item.remainings === 'high'"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
|
2026-07-09 02:29:12 +04:00
|
|
|
<button class="add-to-cart-btn" type="button" (click)="onAddToCart($event)" [attr.aria-label]="(addToCartLabel || ('catalog.addToCart' | translate)) + ': ' + (title || item.name)">
|
|
|
|
|
{{ addToCartLabel || ('catalog.addToCart' | translate) }}
|
2026-07-05 01:05:48 +04:00
|
|
|
</button>
|
|
|
|
|
</article>
|