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>
This commit is contained in:
@@ -12,10 +12,10 @@
|
||||
@if (showFavoriteControl() || showCompareControl() || showShareAction || showQuickViewPlaceholder) {
|
||||
<div class="product-actions-overlay">
|
||||
@if (showFavoriteControl()) {
|
||||
<button type="button" class="product-action-btn product-action-favorite" [attr.aria-label]="'catalog.favorite' | translate" [class.active]="isFavorite" (click)="onFavoritePlaceholder($event)">❤</button>
|
||||
<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>
|
||||
}
|
||||
@if (showCompareControl()) {
|
||||
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.compare' | translate" [class.active]="isCompared" (click)="onComparePlaceholder($event)">⇄</button>
|
||||
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.compare' | translate" [attr.aria-pressed]="isCompared" [class.active]="isCompared" (click)="onComparePlaceholder($event)">⇄</button>
|
||||
}
|
||||
@if (showShareAction) {
|
||||
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.share' | translate" (click)="onShare($event)">↗</button>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.12);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
min-height: 220px;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 13px 13px 0 0;
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f0f0f0;
|
||||
background: var(--bg-tertiary);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -72,7 +72,7 @@
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: rgba(22, 163, 74, 0.9);
|
||||
background: color-mix(in srgb, var(--success-color) 90%, black);
|
||||
color: #fff;
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
|
||||
.stock-badge.out {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
background: color-mix(in srgb, var(--error-color) 90%, black);
|
||||
}
|
||||
|
||||
.product-actions-overlay {
|
||||
@@ -98,7 +98,7 @@
|
||||
border: 1px solid rgba(30, 60, 56, 0.15);
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg, 1rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
display: inline-flex;
|
||||
@@ -110,12 +110,12 @@
|
||||
|
||||
.product-action-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #497671;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.product-action-btn.active {
|
||||
border-color: #497671;
|
||||
color: #497671;
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.product-action-btn:focus-visible,
|
||||
@@ -158,7 +158,7 @@
|
||||
min-height: 24px;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: #497671;
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
@@ -167,12 +167,12 @@
|
||||
|
||||
.product-details {
|
||||
width: 100%;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: none;
|
||||
border-radius: 0 0 13px 13px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
|
||||
background: #f5f3f9;
|
||||
background: var(--bg-secondary);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -182,7 +182,7 @@
|
||||
.product-name {
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
font-size: clamp(14px, 1.4vw, 18px);
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
@@ -196,7 +196,7 @@
|
||||
|
||||
.product-description {
|
||||
margin: 0;
|
||||
color: #697777;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-base, 0.875rem);
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
@@ -211,7 +211,7 @@
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: var(--font-size-sm, 0.8125rem);
|
||||
color: #697777;
|
||||
color: var(--text-secondary);
|
||||
|
||||
.rating-stars {
|
||||
color: #b45309;
|
||||
@@ -234,7 +234,7 @@
|
||||
|
||||
.discounted-price,
|
||||
.current-price {
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-xl, 1.125rem);
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -252,18 +252,18 @@
|
||||
.bar-segment {
|
||||
flex: 1;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: #d3dad9;
|
||||
background: var(--border-color);
|
||||
|
||||
&.filled.high {
|
||||
background: #16a34a;
|
||||
background: var(--success-color);
|
||||
}
|
||||
|
||||
&.filled.medium {
|
||||
background: #f59e0b;
|
||||
background: var(--warning-color);
|
||||
}
|
||||
|
||||
&.filled.low {
|
||||
background: #ef4444;
|
||||
background: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,14 +273,14 @@
|
||||
min-height: 42px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg, 13px);
|
||||
background: #497671;
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.15s ease;
|
||||
transition: background var(--transition-normal), transform var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: #3d635f;
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 3px solid rgba(73, 118, 113, 0.28);
|
||||
outline: 3px solid color-mix(in srgb, var(--primary-color) 28%, white);
|
||||
outline-offset: 4px;
|
||||
border-radius: var(--radius-lg, 13px);
|
||||
}
|
||||
@@ -27,14 +27,14 @@
|
||||
.catalog-category-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 13px 13px 0 0;
|
||||
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
background: var(--bg-secondary);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
@@ -49,7 +49,7 @@
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #497671;
|
||||
color: var(--primary-color);
|
||||
font-size: 4rem;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #f7f8f8 0%, #e6eceb 100%);
|
||||
@@ -58,18 +58,18 @@
|
||||
.catalog-category-content {
|
||||
min-height: 88px;
|
||||
padding: 13px 16px;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-top: 0;
|
||||
border-radius: 0 0 13px 13px;
|
||||
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.15);
|
||||
background: #f5f3f9;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.catalog-category-title {
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-lg, 1rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
|
||||
@@ -95,9 +95,14 @@
|
||||
type="button"
|
||||
class="color-swatch"
|
||||
[class.active]="isSelected(filter.id, option.value)"
|
||||
[attr.aria-pressed]="isSelected(filter.id, option.value)"
|
||||
[style.background]="option.colorHex ?? '#94a3b8'"
|
||||
[attr.aria-label]="option.label"
|
||||
(click)="toggleOption(filter.id, option.value)"></button>
|
||||
(click)="toggleOption(filter.id, option.value)">
|
||||
@if (isSelected(filter.id, option.value)) {
|
||||
<span class="color-swatch-check" aria-hidden="true">✓</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -109,6 +114,7 @@
|
||||
type="button"
|
||||
class="size-chip"
|
||||
[class.active]="isSelected(filter.id, option.value)"
|
||||
[attr.aria-pressed]="isSelected(filter.id, option.value)"
|
||||
(click)="toggleOption(filter.id, option.value)">{{ option.label }}</button>
|
||||
}
|
||||
</div>
|
||||
@@ -121,6 +127,7 @@
|
||||
type="button"
|
||||
class="rating-chip"
|
||||
[class.active]="isSelected(filter.id, option.value)"
|
||||
[attr.aria-pressed]="isSelected(filter.id, option.value)"
|
||||
(click)="toggleOption(filter.id, option.value)">
|
||||
<span aria-hidden="true">★</span>
|
||||
<span>{{ option.label }}</span>
|
||||
|
||||
@@ -185,6 +185,9 @@
|
||||
.color-swatch {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
@@ -193,6 +196,14 @@
|
||||
|
||||
.color-swatch.active {
|
||||
border-color: var(--text-primary);
|
||||
box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 3px var(--text-primary);
|
||||
}
|
||||
|
||||
.color-swatch-check {
|
||||
color: #fff;
|
||||
font-size: var(--font-size-sm, 0.8125rem);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.color-swatch:hover {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
class="layout-btn"
|
||||
[class.active]="layout === active"
|
||||
[attr.aria-label]="labels[layout] | translate"
|
||||
[attr.aria-pressed]="layout === active"
|
||||
(click)="activeChange.emit(layout)">
|
||||
@switch (layout) {
|
||||
@case ('grid') {
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
|
||||
.skeleton-card {
|
||||
min-height: 340px;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg, 13px);
|
||||
background:
|
||||
linear-gradient(90deg, #edf1f1 25%, #e2ebea 50%, #edf1f1 75%) 16px 16px / calc(100% - 32px) calc(100% - 32px) no-repeat,
|
||||
#f5f3f9;
|
||||
var(--bg-secondary);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0 8px;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.catalog-sorting select:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -262,7 +262,7 @@
|
||||
|
||||
<div class="catalog-sort-sheet-options">
|
||||
@for (option of mobileSortOptions(); track option.id) {
|
||||
<button type="button" class="catalog-sort-option" [class.active]="state().sort === option.id" (click)="selectSortFromSheet(option.id)">
|
||||
<button type="button" class="catalog-sort-option" [class.active]="state().sort === option.id" [attr.aria-current]="state().sort === option.id ? 'true' : null" (click)="selectSortFromSheet(option.id)">
|
||||
{{ option.label }}
|
||||
</button>
|
||||
}
|
||||
@@ -280,7 +280,7 @@
|
||||
|
||||
<div class="catalog-sort-sheet-options">
|
||||
@for (layout of availableLayouts; track layout) {
|
||||
<button type="button" class="catalog-sort-option" [class.active]="state().layout === layout" (click)="selectLayoutFromSheet(layout)">
|
||||
<button type="button" class="catalog-sort-option" [class.active]="state().layout === layout" [attr.aria-current]="state().layout === layout ? 'true' : null" (click)="selectLayoutFromSheet(layout)">
|
||||
{{ layoutLabelKey(layout) | translate }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -320,6 +320,13 @@
|
||||
.catalog-sort-option.active {
|
||||
border-color: var(--primary-color);
|
||||
background: color-mix(in srgb, var(--primary-color) 10%, white);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
|
||||
&::after {
|
||||
content: '✓';
|
||||
float: right;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-sort-option:hover {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
.home-loading__grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding-block: 24px;
|
||||
gap: var(--space-lg);
|
||||
padding-block: var(--space-lg);
|
||||
}
|
||||
|
||||
.home-loading__row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.home-empty {
|
||||
padding-block: 48px;
|
||||
padding-block: var(--space-2xl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user