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:
sdarbinyan
2026-07-23 11:34:05 +04:00
parent 2c244ad366
commit 9ea8c98faf
11 changed files with 74 additions and 42 deletions

View File

@@ -12,10 +12,10 @@
@if (showFavoriteControl() || showCompareControl() || showShareAction || showQuickViewPlaceholder) { @if (showFavoriteControl() || showCompareControl() || showShareAction || showQuickViewPlaceholder) {
<div class="product-actions-overlay"> <div class="product-actions-overlay">
@if (showFavoriteControl()) { @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()) { @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) { @if (showShareAction) {
<button type="button" class="product-action-btn" [attr.aria-label]="'catalog.share' | translate" (click)="onShare($event)"></button> <button type="button" class="product-action-btn" [attr.aria-label]="'catalog.share' | translate" (click)="onShare($event)"></button>

View File

@@ -5,11 +5,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform var(--transition-normal), box-shadow var(--transition-normal);
&:hover { &:hover {
transform: translateY(-2px); 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%; width: 100%;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
min-height: 220px; min-height: 220px;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: 13px 13px 0 0; border-radius: 13px 13px 0 0;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #f0f0f0; background: var(--bg-tertiary);
img { img {
width: 100%; width: 100%;
@@ -72,7 +72,7 @@
align-items: center; align-items: center;
padding: 2px 8px; padding: 2px 8px;
border-radius: var(--radius-full, 999px); 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; color: #fff;
font-size: var(--font-size-xs, 0.75rem); font-size: var(--font-size-xs, 0.75rem);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
@@ -80,7 +80,7 @@
} }
.stock-badge.out { .stock-badge.out {
background: rgba(239, 68, 68, 0.9); background: color-mix(in srgb, var(--error-color) 90%, black);
} }
.product-actions-overlay { .product-actions-overlay {
@@ -98,7 +98,7 @@
border: 1px solid rgba(30, 60, 56, 0.15); border: 1px solid rgba(30, 60, 56, 0.15);
border-radius: 50%; border-radius: 50%;
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
color: #1e3c38; color: var(--text-primary);
font-size: var(--font-size-lg, 1rem); font-size: var(--font-size-lg, 1rem);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
display: inline-flex; display: inline-flex;
@@ -110,12 +110,12 @@
.product-action-btn:hover { .product-action-btn:hover {
transform: translateY(-1px); transform: translateY(-1px);
border-color: #497671; border-color: var(--primary-color);
} }
.product-action-btn.active { .product-action-btn.active {
border-color: #497671; border-color: var(--primary-color);
color: #497671; color: var(--primary-color);
} }
.product-action-btn:focus-visible, .product-action-btn:focus-visible,
@@ -158,7 +158,7 @@
min-height: 24px; min-height: 24px;
padding: 3px 8px; padding: 3px 8px;
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
background: #497671; background: var(--primary-color);
color: #fff; color: #fff;
font-size: var(--font-size-xs, 0.75rem); font-size: var(--font-size-xs, 0.75rem);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
@@ -167,12 +167,12 @@
.product-details { .product-details {
width: 100%; width: 100%;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-top: none; border-top: none;
border-radius: 0 0 13px 13px; border-radius: 0 0 13px 13px;
padding: 16px; padding: 16px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: #f5f3f9; background: var(--bg-secondary);
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -182,7 +182,7 @@
.product-name { .product-name {
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
font-size: clamp(14px, 1.4vw, 18px); font-size: clamp(14px, 1.4vw, 18px);
color: #1e3c38; color: var(--text-primary);
margin: 0; margin: 0;
line-height: 1.3; line-height: 1.3;
display: -webkit-box; display: -webkit-box;
@@ -196,7 +196,7 @@
.product-description { .product-description {
margin: 0; margin: 0;
color: #697777; color: var(--text-secondary);
font-size: var(--font-size-base, 0.875rem); font-size: var(--font-size-base, 0.875rem);
line-height: 1.45; line-height: 1.45;
display: -webkit-box; display: -webkit-box;
@@ -211,7 +211,7 @@
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: var(--font-size-sm, 0.8125rem); font-size: var(--font-size-sm, 0.8125rem);
color: #697777; color: var(--text-secondary);
.rating-stars { .rating-stars {
color: #b45309; color: #b45309;
@@ -234,7 +234,7 @@
.discounted-price, .discounted-price,
.current-price { .current-price {
color: #1e3c38; color: var(--text-primary);
font-size: var(--font-size-xl, 1.125rem); font-size: var(--font-size-xl, 1.125rem);
font-weight: 800; font-weight: 800;
} }
@@ -252,18 +252,18 @@
.bar-segment { .bar-segment {
flex: 1; flex: 1;
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
background: #d3dad9; background: var(--border-color);
&.filled.high { &.filled.high {
background: #16a34a; background: var(--success-color);
} }
&.filled.medium { &.filled.medium {
background: #f59e0b; background: var(--warning-color);
} }
&.filled.low { &.filled.low {
background: #ef4444; background: var(--error-color);
} }
} }
@@ -273,14 +273,14 @@
min-height: 42px; min-height: 42px;
border: 0; border: 0;
border-radius: var(--radius-lg, 13px); border-radius: var(--radius-lg, 13px);
background: #497671; background: var(--primary-color);
color: #fff; color: #fff;
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
cursor: pointer; cursor: pointer;
transition: background 0.2s ease, transform 0.15s ease; transition: background var(--transition-normal), transform var(--transition-fast);
&:hover { &:hover {
background: #3d635f; background: var(--primary-hover);
transform: translateY(-1px); transform: translateY(-1px);
} }

View File

@@ -18,7 +18,7 @@
} }
&:focus-visible { &: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; outline-offset: 4px;
border-radius: var(--radius-lg, 13px); border-radius: var(--radius-lg, 13px);
} }
@@ -27,14 +27,14 @@
.catalog-category-image { .catalog-category-image {
width: 100%; width: 100%;
aspect-ratio: 4 / 3; aspect-ratio: 4 / 3;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: 13px 13px 0 0; border-radius: 13px 13px 0 0;
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.15); box-shadow: 0 3px 4px rgba(0, 0, 0, 0.15);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #f5f5f5; background: var(--bg-secondary);
img { img {
width: 100%; width: 100%;
@@ -49,7 +49,7 @@
height: 100%; height: 100%;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #497671; color: var(--primary-color);
font-size: 4rem; font-size: 4rem;
font-weight: 800; font-weight: 800;
background: linear-gradient(135deg, #f7f8f8 0%, #e6eceb 100%); background: linear-gradient(135deg, #f7f8f8 0%, #e6eceb 100%);
@@ -58,18 +58,18 @@
.catalog-category-content { .catalog-category-content {
min-height: 88px; min-height: 88px;
padding: 13px 16px; padding: 13px 16px;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-top: 0; border-top: 0;
border-radius: 0 0 13px 13px; border-radius: 0 0 13px 13px;
box-shadow: 0 3px 4px rgba(0, 0, 0, 0.15); box-shadow: 0 3px 4px rgba(0, 0, 0, 0.15);
background: #f5f3f9; background: var(--bg-secondary);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.catalog-category-title { .catalog-category-title {
color: #1e3c38; color: var(--text-primary);
font-size: var(--font-size-lg, 1rem); font-size: var(--font-size-lg, 1rem);
font-weight: 800; font-weight: 800;
line-height: 1.25; line-height: 1.25;

View File

@@ -95,9 +95,14 @@
type="button" type="button"
class="color-swatch" class="color-swatch"
[class.active]="isSelected(filter.id, option.value)" [class.active]="isSelected(filter.id, option.value)"
[attr.aria-pressed]="isSelected(filter.id, option.value)"
[style.background]="option.colorHex ?? '#94a3b8'" [style.background]="option.colorHex ?? '#94a3b8'"
[attr.aria-label]="option.label" [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> </div>
} }
@@ -109,6 +114,7 @@
type="button" type="button"
class="size-chip" class="size-chip"
[class.active]="isSelected(filter.id, option.value)" [class.active]="isSelected(filter.id, option.value)"
[attr.aria-pressed]="isSelected(filter.id, option.value)"
(click)="toggleOption(filter.id, option.value)">{{ option.label }}</button> (click)="toggleOption(filter.id, option.value)">{{ option.label }}</button>
} }
</div> </div>
@@ -121,6 +127,7 @@
type="button" type="button"
class="rating-chip" class="rating-chip"
[class.active]="isSelected(filter.id, option.value)" [class.active]="isSelected(filter.id, option.value)"
[attr.aria-pressed]="isSelected(filter.id, option.value)"
(click)="toggleOption(filter.id, option.value)"> (click)="toggleOption(filter.id, option.value)">
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span>{{ option.label }}</span> <span>{{ option.label }}</span>

View File

@@ -185,6 +185,9 @@
.color-swatch { .color-swatch {
width: 30px; width: 30px;
height: 30px; height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
@@ -193,6 +196,14 @@
.color-swatch.active { .color-swatch.active {
border-color: var(--text-primary); 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 { .color-swatch:hover {

View File

@@ -5,6 +5,7 @@
class="layout-btn" class="layout-btn"
[class.active]="layout === active" [class.active]="layout === active"
[attr.aria-label]="labels[layout] | translate" [attr.aria-label]="labels[layout] | translate"
[attr.aria-pressed]="layout === active"
(click)="activeChange.emit(layout)"> (click)="activeChange.emit(layout)">
@switch (layout) { @switch (layout) {
@case ('grid') { @case ('grid') {

View File

@@ -28,11 +28,11 @@
.skeleton-card { .skeleton-card {
min-height: 340px; min-height: 340px;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: var(--radius-lg, 13px); border-radius: var(--radius-lg, 13px);
background: background:
linear-gradient(90deg, #edf1f1 25%, #e2ebea 50%, #edf1f1 75%) 16px 16px / calc(100% - 32px) calc(100% - 32px) no-repeat, 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%; background-size: 200% 100%;
animation: shimmer 1.2s linear infinite; animation: shimmer 1.2s linear infinite;
} }

View File

@@ -19,6 +19,12 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 0 8px; padding: 0 8px;
font: inherit; font: inherit;
cursor: pointer;
transition: border-color var(--transition-fast);
}
.catalog-sorting select:hover {
border-color: var(--primary-color);
} }
@media (max-width: 640px) { @media (max-width: 640px) {

View File

@@ -262,7 +262,7 @@
<div class="catalog-sort-sheet-options"> <div class="catalog-sort-sheet-options">
@for (option of mobileSortOptions(); track option.id) { @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 }} {{ option.label }}
</button> </button>
} }
@@ -280,7 +280,7 @@
<div class="catalog-sort-sheet-options"> <div class="catalog-sort-sheet-options">
@for (layout of availableLayouts; track layout) { @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 }} {{ layoutLabelKey(layout) | translate }}
</button> </button>
} }

View File

@@ -320,6 +320,13 @@
.catalog-sort-option.active { .catalog-sort-option.active {
border-color: var(--primary-color); border-color: var(--primary-color);
background: color-mix(in srgb, var(--primary-color) 10%, white); 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 { .catalog-sort-option:hover {

View File

@@ -1,16 +1,16 @@
.home-loading__grid { .home-loading__grid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 24px; gap: var(--space-lg);
padding-block: 24px; padding-block: var(--space-lg);
} }
.home-loading__row { .home-loading__row {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px; gap: var(--space-md);
} }
.home-empty { .home-empty {
padding-block: 48px; padding-block: var(--space-2xl);
} }