From ea1a5d9b8a5cbf9242c0eb351109f19834c8290f Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Thu, 23 Jul 2026 11:47:57 +0400 Subject: [PATCH] fix(storefront): premium UX polish for product, compare, wishlist - delivery-information, product-actions, product-description, product-gallery, product-information, related-products, variant-selector: normalize hardcoded hex colors to design tokens (--text-primary, --text-secondary, --border-color, --bg-primary/ --bg-secondary, --primary-color/--primary-hover); stock status and discount badge now use semantic --success-color/--warning-color/ --error-color instead of near-duplicate literal hex - product-actions: add aria-pressed to wishlist/compare toggle buttons; add hover/active/disabled states to action buttons - product-gallery: add aria-current + aria-label to active thumbnail button; add focus-visible ring and hover state on thumbnails and toolbar buttons - variant-selector: add aria-pressed to colour/size option buttons; add a visible checkmark glyph on the selected colour swatch so selection isn't color-only; add hover states - product-tabs: add role="tab"/aria-selected to tab buttons - star-selector: add per-star aria-label (new starsLabel i18n key added to en/hy/ru + translations.ts interface) - question-list: add aria-expanded to the ask-question disclosure toggle; swap plain empty-state

for app-empty-state; add hover/disabled states to pager buttons - review-list: swap plain empty-state

for app-empty-state; add hover/disabled states to pager and load-more buttons - question-card, question-form, review-form: normalize accepted/ success/error colors to semantic tokens; add focus-visible and hover/disabled states to inputs and submit buttons - compare-table: add scope="col"/scope="row" to table headers; make header row and attribute column sticky for easier comparison on long tables - compare-page: add hover/focus states to the remove-from-compare chip button Build verified green via `npm run build`. Out of scope / skipped: - src/app/pages/item-detail/* is dead code (not referenced by any route or component) - left untouched - wishlist page and product-details-container were already fully composed with shared skeleton/empty-state/button components from the RC-Visual-02 pass - no changes needed - stars.component display-only rating glyphs use a light gray not an exact token match - left as-is to avoid an unintended visual shift Co-Authored-By: Claude Sonnet 5 --- .../delivery-information.component.scss | 21 ++++---- .../product-actions.component.html | 4 +- .../product-actions.component.scss | 48 +++++++++++++++---- .../product-description.component.scss | 8 ++-- .../product-gallery.component.html | 2 +- .../product-gallery.component.scss | 37 ++++++++++---- .../product-information.component.scss | 44 ++++++++++------- .../related-products.component.scss | 4 +- .../variant-selector.component.html | 8 +++- .../variant-selector.component.scss | 38 ++++++++++----- .../variant-selector.component.ts | 3 +- .../product-tabs/product-tabs.component.html | 2 + .../question-card.component.scss | 4 +- .../question-form.component.scss | 18 ++++++- .../question-list.component.html | 4 +- .../question-list.component.scss | 11 +++++ .../question-list/question-list.component.ts | 3 +- .../review-form/review-form.component.scss | 21 ++++++-- .../review-list/review-list.component.html | 2 +- .../review-list/review-list.component.scss | 16 +++++++ .../review-list/review-list.component.ts | 3 +- .../star-selector.component.html | 1 + .../components/compare-table.component.html | 6 +-- .../components/compare-table.component.scss | 12 +++++ .../containers/compare-page.component.scss | 28 +++++++++++ src/app/i18n/en.ts | 1 + src/app/i18n/hy.ts | 1 + src/app/i18n/ru.ts | 1 + src/app/i18n/translations.ts | 1 + 29 files changed, 271 insertions(+), 81 deletions(-) diff --git a/src/app/features/website/product/components/delivery-information/delivery-information.component.scss b/src/app/features/website/product/components/delivery-information/delivery-information.component.scss index 32e097f..542ef01 100644 --- a/src/app/features/website/product/components/delivery-information/delivery-information.component.scss +++ b/src/app/features/website/product/components/delivery-information/delivery-information.component.scss @@ -1,15 +1,20 @@ .delivery-information { display: flex; flex-direction: column; - gap: 12px; - padding: 16px; - border: 1px solid #d3dad9; + gap: var(--space-sm); + padding: var(--space-md); + border: 1px solid var(--border-color); border-radius: var(--radius-lg, 13px); - background: #f8faf9; + background: var(--bg-secondary); + transition: box-shadow var(--transition-normal); + + &:hover { + box-shadow: var(--shadow-sm); + } h2 { margin: 0; - color: #1e3c38; + color: var(--text-primary); font-size: var(--font-size-lg, 1rem); } } @@ -19,7 +24,7 @@ padding: 6px 10px; border-radius: var(--radius-full, 999px); background: rgba(73, 118, 113, 0.12); - color: #3d635f; + color: var(--primary-hover); font-weight: 800; } @@ -33,7 +38,7 @@ display: flex; justify-content: space-between; gap: 14px; - color: #697777; + color: var(--text-secondary); div { display: flex; @@ -42,7 +47,7 @@ } strong { - color: #1e3c38; + color: var(--text-primary); white-space: nowrap; } } diff --git a/src/app/features/website/product/components/product-actions/product-actions.component.html b/src/app/features/website/product/components/product-actions/product-actions.component.html index c21d9bf..dcea255 100644 --- a/src/app/features/website/product/components/product-actions/product-actions.component.html +++ b/src/app/features/website/product/components/product-actions/product-actions.component.html @@ -8,11 +8,11 @@ } @if (config.wishlist) { - + } @if (config.compare) { - + } @if (config.share) { diff --git a/src/app/features/website/product/components/product-actions/product-actions.component.scss b/src/app/features/website/product/components/product-actions/product-actions.component.scss index 77505b5..bcc201a 100644 --- a/src/app/features/website/product/components/product-actions/product-actions.component.scss +++ b/src/app/features/website/product/components/product-actions/product-actions.component.scss @@ -7,32 +7,60 @@ button { min-height: 40px; border-radius: 10px; - border: 1px solid #d3dad9; - background: #fff; - color: #1e3c38; + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--text-primary); font-weight: var(--font-weight-bold, 700); padding: 0 14px; cursor: pointer; + transition: background var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast); +} + +button:hover { + border-color: var(--primary-color); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +button:active { + transform: translateY(0); +} + +button:disabled { + cursor: not-allowed; + opacity: 0.6; + transform: none; + box-shadow: none; } button.primary { - border-color: #497671; - background: #497671; - color: #fff; + border-color: var(--primary-color); + background: var(--primary-color); + color: var(--bg-primary); +} + +button.primary:hover { + background: var(--primary-hover); + border-color: var(--primary-hover); } button.primary.ghost { - background: #eef4f3; - color: #2e5e59; + background: var(--accent-color); + color: var(--primary-hover); +} + +button.primary.ghost:hover { + background: var(--accent-hover); } button.secondary.active { - border-color: #497671; + border-color: var(--primary-color); + color: var(--primary-hover); box-shadow: 0 0 0 2px rgba(73, 118, 113, 0.18); } button:focus-visible { - outline: 2px solid #497671; + outline: 2px solid var(--primary-color); outline-offset: 2px; } diff --git a/src/app/features/website/product/components/product-description/product-description.component.scss b/src/app/features/website/product/components/product-description/product-description.component.scss index 83db4f4..bcbfa8b 100644 --- a/src/app/features/website/product/components/product-description/product-description.component.scss +++ b/src/app/features/website/product/components/product-description/product-description.component.scss @@ -9,13 +9,13 @@ h2 { margin: 0; - color: #1e3c38; - font-size: 1.45rem; + color: var(--text-primary); + font-size: var(--font-size-3xl); } p { margin: 0; - color: #697777; - line-height: 1.65; + color: var(--text-secondary); + line-height: var(--line-height-relaxed); } } diff --git a/src/app/features/website/product/components/product-gallery/product-gallery.component.html b/src/app/features/website/product/components/product-gallery/product-gallery.component.html index 3b9101c..39982bd 100644 --- a/src/app/features/website/product/components/product-gallery/product-gallery.component.html +++ b/src/app/features/website/product/components/product-gallery/product-gallery.component.html @@ -39,7 +39,7 @@ @if (media.length > 1) {

diff --git a/src/app/features/website/product/components/variant-selector/variant-selector.component.scss b/src/app/features/website/product/components/variant-selector/variant-selector.component.scss index 3254647..399ad57 100644 --- a/src/app/features/website/product/components/variant-selector/variant-selector.component.scss +++ b/src/app/features/website/product/components/variant-selector/variant-selector.component.scss @@ -11,7 +11,7 @@ } .variant-label { - color: #697777; + color: var(--text-secondary); font-size: var(--font-size-base, 0.875rem); font-weight: 800; } @@ -25,12 +25,22 @@ .colour-swatch { width: 34px; height: 34px; - border: 2px solid #d3dad9; + border: 2px solid var(--border-color); border-radius: 50%; cursor: pointer; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--bg-primary); + transition: transform var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast); + + &:hover { + transform: translateY(-1px); + } &.active { - border-color: #1e3c38; + border-color: var(--text-primary); box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.2); } @@ -40,7 +50,7 @@ } &:focus-visible { - outline: 2px solid #1e3c38; + outline: 2px solid var(--text-primary); outline-offset: 2px; } } @@ -48,17 +58,23 @@ .size-chip { min-height: 34px; padding: 0 14px; - border: 1px solid #d3dad9; + border: 1px solid var(--border-color); border-radius: var(--radius-full, 999px); - background: #fff; - color: #1e3c38; + background: var(--bg-primary); + color: var(--text-primary); font-weight: 800; cursor: pointer; + transition: transform var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast); + + &:hover { + border-color: var(--primary-color); + transform: translateY(-1px); + } &.active { - border-color: #497671; - background: #497671; - color: #fff; + border-color: var(--primary-color); + background: var(--primary-color); + color: var(--bg-primary); } &.readonly { @@ -68,7 +84,7 @@ } &:focus-visible { - outline: 2px solid #497671; + outline: 2px solid var(--primary-color); outline-offset: 2px; } } diff --git a/src/app/features/website/product/components/variant-selector/variant-selector.component.ts b/src/app/features/website/product/components/variant-selector/variant-selector.component.ts index 310ad68..08d1767 100644 --- a/src/app/features/website/product/components/variant-selector/variant-selector.component.ts +++ b/src/app/features/website/product/components/variant-selector/variant-selector.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { TranslatePipe } from '../../../../../i18n/translate.pipe'; +import { IconComponent } from '../../../../../shared/ui/icon/icon.component'; export interface VariantOption { value: string; @@ -15,7 +16,7 @@ export interface VariantOptionGroup { @Component({ selector: 'app-product-variant-selector', standalone: true, - imports: [TranslatePipe], + imports: [TranslatePipe, IconComponent], templateUrl: './variant-selector.component.html', styleUrls: ['./variant-selector.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/src/app/features/website/product/engagement/components/product-tabs/product-tabs.component.html b/src/app/features/website/product/engagement/components/product-tabs/product-tabs.component.html index 81654c9..bbd5780 100644 --- a/src/app/features/website/product/engagement/components/product-tabs/product-tabs.component.html +++ b/src/app/features/website/product/engagement/components/product-tabs/product-tabs.component.html @@ -4,7 +4,9 @@ } @else { @@ -23,7 +23,7 @@ } } @else { -

{{ 'productDetails.questionsEmpty' | translate }}

+ } @if (result && totalPages > 1) { diff --git a/src/app/features/website/product/engagement/components/question-list/question-list.component.scss b/src/app/features/website/product/engagement/components/question-list/question-list.component.scss index 287d20b..5d81542 100644 --- a/src/app/features/website/product/engagement/components/question-list/question-list.component.scss +++ b/src/app/features/website/product/engagement/components/question-list/question-list.component.scss @@ -51,6 +51,17 @@ background: var(--bg-primary); padding: 0 12px; cursor: pointer; + transition: border-color var(--transition-fast), transform var(--transition-fast); +} + +.pager button:hover:not(:disabled) { + border-color: var(--primary-color); + transform: translateY(-1px); +} + +.pager button:disabled { + opacity: 0.6; + cursor: not-allowed; } .skeleton-list { diff --git a/src/app/features/website/product/engagement/components/question-list/question-list.component.ts b/src/app/features/website/product/engagement/components/question-list/question-list.component.ts index 8ee7e0c..f87f5bc 100644 --- a/src/app/features/website/product/engagement/components/question-list/question-list.component.ts +++ b/src/app/features/website/product/engagement/components/question-list/question-list.component.ts @@ -3,11 +3,12 @@ import { EngagementListResult, Question, SubmitQuestionInput } from '../../../.. import { TranslatePipe } from '../../../../../../i18n/translate.pipe'; import { QuestionCardComponent } from '../question-card/question-card.component'; import { QuestionFormComponent } from '../question-form/question-form.component'; +import { EmptyStateComponent } from '../../../../../../shared/ui/empty-state/empty-state.component'; @Component({ selector: 'app-question-list', standalone: true, - imports: [QuestionCardComponent, QuestionFormComponent, TranslatePipe], + imports: [QuestionCardComponent, QuestionFormComponent, TranslatePipe, EmptyStateComponent], templateUrl: './question-list.component.html', styleUrls: ['./question-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/src/app/features/website/product/engagement/components/review-form/review-form.component.scss b/src/app/features/website/product/engagement/components/review-form/review-form.component.scss index 2a9ede2..53b2817 100644 --- a/src/app/features/website/product/engagement/components/review-form/review-form.component.scss +++ b/src/app/features/website/product/engagement/components/review-form/review-form.component.scss @@ -22,6 +22,12 @@ textarea { border-radius: var(--radius-sm); padding: 10px 12px; font: inherit; + transition: border-color var(--transition-fast); +} + +input:focus-visible, +textarea:focus-visible { + border-color: var(--primary-color); } textarea { @@ -48,12 +54,12 @@ textarea { } .success { - color: #166534; + color: var(--success-color); font-weight: var(--font-weight-bold, 700); } .error { - color: #991b1b; + color: var(--error-color); } button { @@ -62,10 +68,19 @@ button { border: 0; border-radius: var(--radius-sm); background: var(--primary-color); - color: #fff; + color: var(--bg-primary); padding: 0 16px; font-weight: var(--font-weight-bold, 700); cursor: pointer; + transition: background var(--transition-fast), transform var(--transition-fast); +} + +button:hover:not(:disabled) { + background: var(--primary-hover); +} + +button:active:not(:disabled) { + transform: translateY(1px); } button:disabled { diff --git a/src/app/features/website/product/engagement/components/review-list/review-list.component.html b/src/app/features/website/product/engagement/components/review-list/review-list.component.html index c3935e8..c8bab18 100644 --- a/src/app/features/website/product/engagement/components/review-list/review-list.component.html +++ b/src/app/features/website/product/engagement/components/review-list/review-list.component.html @@ -17,7 +17,7 @@ } } @else { -

{{ 'productDetails.reviewsEmpty' | translate }}

+ } @if (result && totalPages > 1 && mode === 'pages') { diff --git a/src/app/features/website/product/engagement/components/review-list/review-list.component.scss b/src/app/features/website/product/engagement/components/review-list/review-list.component.scss index 0b65dfe..6bf4847 100644 --- a/src/app/features/website/product/engagement/components/review-list/review-list.component.scss +++ b/src/app/features/website/product/engagement/components/review-list/review-list.component.scss @@ -35,6 +35,11 @@ transform: translateY(-1px); } +.pager button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + .load-more-wrap { display: grid; justify-items: center; @@ -50,6 +55,17 @@ padding: 0 16px; font-weight: var(--font-weight-bold, 700); cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.load-more:hover:not(:disabled) { + background: var(--primary-color); + color: var(--bg-primary); +} + +.load-more:disabled { + opacity: 0.6; + cursor: not-allowed; } .load-more-wrap small { diff --git a/src/app/features/website/product/engagement/components/review-list/review-list.component.ts b/src/app/features/website/product/engagement/components/review-list/review-list.component.ts index 732fa64..b62e266 100644 --- a/src/app/features/website/product/engagement/components/review-list/review-list.component.ts +++ b/src/app/features/website/product/engagement/components/review-list/review-list.component.ts @@ -5,11 +5,12 @@ import { TranslatePipe } from '../../../../../../i18n/translate.pipe'; import { RatingSummaryComponent } from '../rating-summary/rating-summary.component'; import { ReviewCardComponent } from '../review-card/review-card.component'; import { ReviewFormComponent } from '../review-form/review-form.component'; +import { EmptyStateComponent } from '../../../../../../shared/ui/empty-state/empty-state.component'; @Component({ selector: 'app-review-list', standalone: true, - imports: [RatingSummaryComponent, ReviewCardComponent, ReviewFormComponent, TranslatePipe], + imports: [RatingSummaryComponent, ReviewCardComponent, ReviewFormComponent, TranslatePipe, EmptyStateComponent], templateUrl: './review-list.component.html', styleUrls: ['./review-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/src/app/features/website/product/engagement/components/star-selector/star-selector.component.html b/src/app/features/website/product/engagement/components/star-selector/star-selector.component.html index 7e7899f..f12bfc1 100644 --- a/src/app/features/website/product/engagement/components/star-selector/star-selector.component.html +++ b/src/app/features/website/product/engagement/components/star-selector/star-selector.component.html @@ -5,6 +5,7 @@ class="star-btn" [class.selected]="star <= rating" [attr.aria-checked]="star === rating" + [attr.aria-label]="star + ' ' + ('productDetails.starsLabel' | translate)" role="radio" (click)="ratingChange.emit(star)"> ★ diff --git a/src/app/features/website/user-experience/compare/components/compare-table.component.html b/src/app/features/website/user-experience/compare/components/compare-table.component.html index a2c562f..29a640a 100644 --- a/src/app/features/website/user-experience/compare/components/compare-table.component.html +++ b/src/app/features/website/user-experience/compare/components/compare-table.component.html @@ -3,9 +3,9 @@ - + @for (product of products; track product.itemID) { - } @@ -14,7 +14,7 @@ @for (row of rows(); track row.key) { - + @for (value of row.values; track $index) { } diff --git a/src/app/features/website/user-experience/compare/components/compare-table.component.scss b/src/app/features/website/user-experience/compare/components/compare-table.component.scss index 1159044..3d5a534 100644 --- a/src/app/features/website/user-experience/compare/components/compare-table.component.scss +++ b/src/app/features/website/user-experience/compare/components/compare-table.component.scss @@ -24,6 +24,18 @@ background: color-mix(in srgb, var(--bg-secondary) 60%, white); } +.compare-table thead th { + position: sticky; + top: 0; + z-index: 1; +} + +.compare-table tbody th { + position: sticky; + left: 0; + z-index: 1; +} + .compare-product-title { font-size: var(--font-size-md, 0.9375rem); font-weight: var(--font-weight-bold, 700); diff --git a/src/app/features/website/user-experience/compare/containers/compare-page.component.scss b/src/app/features/website/user-experience/compare/containers/compare-page.component.scss index 60c6937..6a6520f 100644 --- a/src/app/features/website/user-experience/compare/containers/compare-page.component.scss +++ b/src/app/features/website/user-experience/compare/containers/compare-page.component.scss @@ -50,6 +50,12 @@ border-radius: var(--radius-full, 999px); padding: 4px 10px; background: var(--bg-primary); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} + +.compare-product-chip:hover { + border-color: var(--primary-color); + box-shadow: var(--shadow-sm); } .compare-product-chip a { @@ -58,11 +64,33 @@ font-weight: var(--font-weight-semibold, 600); } +.compare-product-chip a:hover { + color: var(--primary-color); + text-decoration: underline; +} + .compare-product-chip button { border: 0; + border-radius: 50%; + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; background: transparent; color: var(--text-secondary); cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.compare-product-chip button:hover { + background: var(--bg-tertiary); + color: var(--error-color); +} + +.compare-product-chip button:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; } .compare-empty { diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index c0dc9d5..7fae878 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -283,6 +283,7 @@ export const en: Translations = { mediaAria: 'Product media', ratingStarsAria: 'Rating stars', selectRatingAria: 'Select rating', + starsLabel: 'stars', reviewsEmpty: 'No reviews yet.', questionsEmpty: 'No questions yet.', askQuestion: 'Ask question', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 0c39c99..311430c 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -283,6 +283,7 @@ export const hy: Translations = { mediaAria: 'Ապրանքի մեդիա', ratingStarsAria: 'Աստղային վարկանիշ', selectRatingAria: 'Ընտրեք գնահատականը', + starsLabel: 'աստղ', reviewsEmpty: 'Կարծիքներ դեռ չկան։', questionsEmpty: 'Հարցեր դեռ չկան։', askQuestion: 'Տալ հարց', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 1f2a585..71cbb11 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -283,6 +283,7 @@ export const ru: Translations = { mediaAria: 'Медиа товара', ratingStarsAria: 'Рейтинг в звездах', selectRatingAria: 'Выберите оценку', + starsLabel: 'звёзд', reviewsEmpty: 'Пока нет отзывов.', questionsEmpty: 'Пока нет вопросов.', askQuestion: 'Задать вопрос', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index c63b3be..ea9ec14 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -281,6 +281,7 @@ export interface Translations { mediaAria: string; ratingStarsAria: string; selectRatingAria: string; + starsLabel: string; reviewsEmpty: string; questionsEmpty: string; askQuestion: string;
{{ 'ux.compareAttribute' | translate }}{{ 'ux.compareAttribute' | translate }} +
{{ product.name }}
{{ row.label }}{{ row.label }}{{ value }}