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 <p> for app-empty-state; add
  hover/disabled states to pager buttons
- review-list: swap plain empty-state <p> 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 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-23 11:47:57 +04:00
parent 9ea8c98faf
commit ea1a5d9b8a
29 changed files with 271 additions and 81 deletions

View File

@@ -1,15 +1,20 @@
.delivery-information { .delivery-information {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: var(--space-sm);
padding: 16px; padding: var(--space-md);
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: var(--radius-lg, 13px); 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 { h2 {
margin: 0; margin: 0;
color: #1e3c38; color: var(--text-primary);
font-size: var(--font-size-lg, 1rem); font-size: var(--font-size-lg, 1rem);
} }
} }
@@ -19,7 +24,7 @@
padding: 6px 10px; padding: 6px 10px;
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
background: rgba(73, 118, 113, 0.12); background: rgba(73, 118, 113, 0.12);
color: #3d635f; color: var(--primary-hover);
font-weight: 800; font-weight: 800;
} }
@@ -33,7 +38,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 14px; gap: 14px;
color: #697777; color: var(--text-secondary);
div { div {
display: flex; display: flex;
@@ -42,7 +47,7 @@
} }
strong { strong {
color: #1e3c38; color: var(--text-primary);
white-space: nowrap; white-space: nowrap;
} }
} }

View File

@@ -8,11 +8,11 @@
} }
@if (config.wishlist) { @if (config.wishlist) {
<button type="button" class="secondary" [class.active]="inWishlist" (click)="wishlist.emit()">{{ 'productDetails.wishlist' | translate }}</button> <button type="button" class="secondary" [class.active]="inWishlist" [attr.aria-pressed]="inWishlist" (click)="wishlist.emit()">{{ 'productDetails.wishlist' | translate }}</button>
} }
@if (config.compare) { @if (config.compare) {
<button type="button" class="secondary" [class.active]="inCompare" (click)="compare.emit()">{{ 'productDetails.compare' | translate }}</button> <button type="button" class="secondary" [class.active]="inCompare" [attr.aria-pressed]="inCompare" (click)="compare.emit()">{{ 'productDetails.compare' | translate }}</button>
} }
@if (config.share) { @if (config.share) {

View File

@@ -7,32 +7,60 @@
button { button {
min-height: 40px; min-height: 40px;
border-radius: 10px; border-radius: 10px;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
background: #fff; background: var(--bg-primary);
color: #1e3c38; color: var(--text-primary);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
padding: 0 14px; padding: 0 14px;
cursor: pointer; 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 { button.primary {
border-color: #497671; border-color: var(--primary-color);
background: #497671; background: var(--primary-color);
color: #fff; color: var(--bg-primary);
}
button.primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
} }
button.primary.ghost { button.primary.ghost {
background: #eef4f3; background: var(--accent-color);
color: #2e5e59; color: var(--primary-hover);
}
button.primary.ghost:hover {
background: var(--accent-hover);
} }
button.secondary.active { 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); box-shadow: 0 0 0 2px rgba(73, 118, 113, 0.18);
} }
button:focus-visible { button:focus-visible {
outline: 2px solid #497671; outline: 2px solid var(--primary-color);
outline-offset: 2px; outline-offset: 2px;
} }

View File

@@ -9,13 +9,13 @@
h2 { h2 {
margin: 0; margin: 0;
color: #1e3c38; color: var(--text-primary);
font-size: 1.45rem; font-size: var(--font-size-3xl);
} }
p { p {
margin: 0; margin: 0;
color: #697777; color: var(--text-secondary);
line-height: 1.65; line-height: var(--line-height-relaxed);
} }
} }

View File

@@ -39,7 +39,7 @@
@if (media.length > 1) { @if (media.length > 1) {
<div class="product-gallery-thumbs"> <div class="product-gallery-thumbs">
@for (item of media; track $index) { @for (item of media; track $index) {
<button type="button" class="product-gallery-thumb" [class.active]="selectedIndex === $index" (click)="select($index)"> <button type="button" class="product-gallery-thumb" [class.active]="selectedIndex === $index" [attr.aria-current]="selectedIndex === $index ? 'true' : null" [attr.aria-label]="('productDetails.photo' | translate) + ' ' + ($index + 1)" (click)="select($index)">
@if (item.type === 'video') { @if (item.type === 'video') {
<span class="product-gallery-video"></span> <span class="product-gallery-video"></span>
} @else if (item.type !== 'image') { } @else if (item.type !== 'image') {

View File

@@ -7,14 +7,14 @@
.product-gallery-main { .product-gallery-main {
position: relative; position: relative;
aspect-ratio: 1; aspect-ratio: 1;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: var(--radius-lg, 13px); border-radius: var(--radius-lg, 13px);
overflow: hidden; overflow: hidden;
background: #fff; background: var(--bg-primary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: box-shadow 0.24s ease, border-color 0.24s ease; transition: box-shadow var(--transition-slow), border-color var(--transition-slow);
img, img,
video, video,
@@ -39,11 +39,22 @@
min-height: 30px; min-height: 30px;
border: 1px solid rgba(30, 60, 56, 0.25); border: 1px solid rgba(30, 60, 56, 0.25);
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
color: #1e3c38; color: var(--text-primary);
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
padding: 0 10px; padding: 0 10px;
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
cursor: pointer; cursor: pointer;
transition: background var(--transition-fast), box-shadow var(--transition-fast);
}
.toolbar-btn:hover {
background: var(--bg-primary);
box-shadow: var(--shadow-sm);
}
.toolbar-btn:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
} }
.document-viewer { .document-viewer {
@@ -55,7 +66,7 @@
p { p {
margin: 0; margin: 0;
color: #1e3c38; color: var(--text-primary);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
} }
@@ -81,21 +92,27 @@
.product-gallery-thumb { .product-gallery-thumb {
position: relative; position: relative;
aspect-ratio: 1; aspect-ratio: 1;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: var(--bg-primary);
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease, border-color 0.2s ease; padding: 0;
transition: transform var(--transition-normal), border-color var(--transition-normal), box-shadow var(--transition-normal);
&.active { &.active {
border-color: #497671; border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.18); box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.18);
} }
&:hover { &:hover {
transform: translateY(-1px); transform: translateY(-1px);
border-color: #497671; border-color: var(--primary-color);
}
&:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
} }
img { img {

View File

@@ -5,7 +5,7 @@
h1 { h1 {
margin: 0; margin: 0;
color: #1e3c38; color: var(--text-primary);
font-size: clamp(1.8rem, 4vw, 2.8rem); font-size: clamp(1.8rem, 4vw, 2.8rem);
line-height: 1.08; line-height: 1.08;
} }
@@ -28,19 +28,19 @@
min-height: 26px; min-height: 26px;
padding: 3px 9px; padding: 3px 9px;
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
background: #497671; background: var(--primary-color);
color: #fff; color: var(--bg-primary);
font-size: var(--font-size-xs, 0.75rem); font-size: var(--font-size-xs, 0.75rem);
font-weight: 900; font-weight: 900;
text-transform: uppercase; text-transform: uppercase;
} }
.discount-badge { .discount-badge {
background: #dc2626; background: var(--error-color);
} }
.product-rating { .product-rating {
color: #697777; color: var(--text-secondary);
font-weight: 800; font-weight: 800;
} }
@@ -50,35 +50,35 @@
gap: 4px; gap: 4px;
strong { strong {
color: #1e3c38; color: var(--text-primary);
font-size: var(--font-size-4xl, 2rem); font-size: var(--font-size-4xl, 2rem);
line-height: 1; line-height: 1;
} }
} }
.old-price { .old-price {
color: #a1b4b5; color: var(--secondary-color);
text-decoration: line-through; text-decoration: line-through;
} }
.product-stock { .product-stock {
color: #697777; color: var(--text-secondary);
font-weight: 800; font-weight: 800;
&.out { &.out {
color: #b91c1c; color: var(--error-color);
} }
&.low { &.low {
color: #b45309; color: var(--warning-color);
} }
&.medium { &.medium {
color: #497671; color: var(--primary-color);
} }
&.high { &.high {
color: #15803d; color: var(--success-color);
} }
} }
@@ -94,16 +94,28 @@
min-height: 48px; min-height: 48px;
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: var(--bg-primary);
font-size: var(--font-size-lg, 1rem); font-size: var(--font-size-lg, 1rem);
font-weight: 900; font-weight: 900;
cursor: pointer; cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease; transition: transform var(--transition-normal), background-color var(--transition-normal), box-shadow var(--transition-normal);
&:hover { &:hover {
background: #3d635f; background: var(--primary-hover);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
&:active {
transform: translateY(0);
}
&:disabled {
cursor: not-allowed;
opacity: 0.6;
transform: none;
box-shadow: none;
} }
&:focus-visible { &:focus-visible {

View File

@@ -5,8 +5,8 @@
h2 { h2 {
margin: 0; margin: 0;
color: #1e3c38; color: var(--text-primary);
font-size: 1.45rem; font-size: var(--font-size-3xl);
} }
} }

View File

@@ -6,9 +6,13 @@
<div class="variant-options"> <div class="variant-options">
@for (option of group.options; track option.value) { @for (option of group.options; track option.value) {
@if (group.key === 'colour') { @if (group.key === 'colour') {
<button type="button" class="colour-swatch" [class.active]="isActive(group.key, option.value)" [style.background-color]="option.value" [attr.aria-label]="option.label || option.value" (click)="onSelect(group.key, option.value)"></button> <button type="button" class="colour-swatch" [class.active]="isActive(group.key, option.value)" [attr.aria-pressed]="isActive(group.key, option.value)" [style.background-color]="option.value" [attr.aria-label]="option.label || option.value" (click)="onSelect(group.key, option.value)">
@if (isActive(group.key, option.value)) {
<app-icon name="check" [size]="14" />
}
</button>
} @else { } @else {
<button type="button" class="size-chip" [class.active]="isActive(group.key, option.value)" (click)="onSelect(group.key, option.value)">{{ option.label || option.value }}</button> <button type="button" class="size-chip" [class.active]="isActive(group.key, option.value)" [attr.aria-pressed]="isActive(group.key, option.value)" (click)="onSelect(group.key, option.value)">{{ option.label || option.value }}</button>
} }
} }
</div> </div>

View File

@@ -11,7 +11,7 @@
} }
.variant-label { .variant-label {
color: #697777; color: var(--text-secondary);
font-size: var(--font-size-base, 0.875rem); font-size: var(--font-size-base, 0.875rem);
font-weight: 800; font-weight: 800;
} }
@@ -25,12 +25,22 @@
.colour-swatch { .colour-swatch {
width: 34px; width: 34px;
height: 34px; height: 34px;
border: 2px solid #d3dad9; border: 2px solid var(--border-color);
border-radius: 50%; border-radius: 50%;
cursor: pointer; 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 { &.active {
border-color: #1e3c38; border-color: var(--text-primary);
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.2); box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.2);
} }
@@ -40,7 +50,7 @@
} }
&:focus-visible { &:focus-visible {
outline: 2px solid #1e3c38; outline: 2px solid var(--text-primary);
outline-offset: 2px; outline-offset: 2px;
} }
} }
@@ -48,17 +58,23 @@
.size-chip { .size-chip {
min-height: 34px; min-height: 34px;
padding: 0 14px; padding: 0 14px;
border: 1px solid #d3dad9; border: 1px solid var(--border-color);
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
background: #fff; background: var(--bg-primary);
color: #1e3c38; color: var(--text-primary);
font-weight: 800; font-weight: 800;
cursor: pointer; 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 { &.active {
border-color: #497671; border-color: var(--primary-color);
background: #497671; background: var(--primary-color);
color: #fff; color: var(--bg-primary);
} }
&.readonly { &.readonly {
@@ -68,7 +84,7 @@
} }
&:focus-visible { &:focus-visible {
outline: 2px solid #497671; outline: 2px solid var(--primary-color);
outline-offset: 2px; outline-offset: 2px;
} }
} }

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { TranslatePipe } from '../../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../../i18n/translate.pipe';
import { IconComponent } from '../../../../../shared/ui/icon/icon.component';
export interface VariantOption { export interface VariantOption {
value: string; value: string;
@@ -15,7 +16,7 @@ export interface VariantOptionGroup {
@Component({ @Component({
selector: 'app-product-variant-selector', selector: 'app-product-variant-selector',
standalone: true, standalone: true,
imports: [TranslatePipe], imports: [TranslatePipe, IconComponent],
templateUrl: './variant-selector.component.html', templateUrl: './variant-selector.component.html',
styleUrls: ['./variant-selector.component.scss'], styleUrls: ['./variant-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -4,7 +4,9 @@
<button <button
type="button" type="button"
class="tab-btn" class="tab-btn"
role="tab"
[class.active]="activeKey === tab.key" [class.active]="activeKey === tab.key"
[attr.aria-selected]="activeKey === tab.key"
[disabled]="tab.enabled === false" [disabled]="tab.enabled === false"
(click)="selectTab(tab)"> (click)="selectTab(tab)">
<span>{{ tab.label }}</span> <span>{{ tab.label }}</span>

View File

@@ -60,8 +60,8 @@ header {
} }
.accepted-pill { .accepted-pill {
background: #e9f9ec; background: color-mix(in srgb, var(--success-color) 16%, white);
color: #18723a; color: var(--success-color);
} }
footer { footer {

View File

@@ -16,6 +16,11 @@ textarea {
padding: 10px 12px; padding: 10px 12px;
font: inherit; font: inherit;
resize: vertical; resize: vertical;
transition: border-color var(--transition-fast);
}
textarea:focus-visible {
border-color: var(--primary-color);
} }
.check { .check {
@@ -30,11 +35,22 @@ button {
border: 0; border: 0;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--primary-color); background: var(--primary-color);
color: #fff; color: var(--bg-primary);
padding: 0 16px; padding: 0 16px;
font-weight: var(--font-weight-bold, 700); 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 { button:disabled {
opacity: 0.6; opacity: 0.6;
cursor: not-allowed;
} }

View File

@@ -1,6 +1,6 @@
<section class="question-list section"> <section class="question-list section">
@if (allowSubmission) { @if (allowSubmission) {
<button type="button" class="ask-btn" (click)="askFormVisible.set(!askFormVisible())"> <button type="button" class="ask-btn" [attr.aria-expanded]="askFormVisible()" (click)="askFormVisible.set(!askFormVisible())">
@if (askFormVisible()) { {{ 'productDetails.hideQuestionForm' | translate }} } @else { {{ 'productDetails.askQuestion' | translate }} } @if (askFormVisible()) { {{ 'productDetails.hideQuestionForm' | translate }} } @else { {{ 'productDetails.askQuestion' | translate }} }
</button> </button>
} @else { } @else {
@@ -23,7 +23,7 @@
} }
</div> </div>
} @else { } @else {
<p class="empty">{{ 'productDetails.questionsEmpty' | translate }}</p> <app-empty-state class="empty" [title]="'productDetails.questionsEmpty' | translate" />
} }
@if (result && totalPages > 1) { @if (result && totalPages > 1) {

View File

@@ -51,6 +51,17 @@
background: var(--bg-primary); background: var(--bg-primary);
padding: 0 12px; padding: 0 12px;
cursor: pointer; 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 { .skeleton-list {

View File

@@ -3,11 +3,12 @@ import { EngagementListResult, Question, SubmitQuestionInput } from '../../../..
import { TranslatePipe } from '../../../../../../i18n/translate.pipe'; import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { QuestionCardComponent } from '../question-card/question-card.component'; import { QuestionCardComponent } from '../question-card/question-card.component';
import { QuestionFormComponent } from '../question-form/question-form.component'; import { QuestionFormComponent } from '../question-form/question-form.component';
import { EmptyStateComponent } from '../../../../../../shared/ui/empty-state/empty-state.component';
@Component({ @Component({
selector: 'app-question-list', selector: 'app-question-list',
standalone: true, standalone: true,
imports: [QuestionCardComponent, QuestionFormComponent, TranslatePipe], imports: [QuestionCardComponent, QuestionFormComponent, TranslatePipe, EmptyStateComponent],
templateUrl: './question-list.component.html', templateUrl: './question-list.component.html',
styleUrls: ['./question-list.component.scss'], styleUrls: ['./question-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -22,6 +22,12 @@ textarea {
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 10px 12px; padding: 10px 12px;
font: inherit; font: inherit;
transition: border-color var(--transition-fast);
}
input:focus-visible,
textarea:focus-visible {
border-color: var(--primary-color);
} }
textarea { textarea {
@@ -48,12 +54,12 @@ textarea {
} }
.success { .success {
color: #166534; color: var(--success-color);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
} }
.error { .error {
color: #991b1b; color: var(--error-color);
} }
button { button {
@@ -62,10 +68,19 @@ button {
border: 0; border: 0;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: var(--primary-color); background: var(--primary-color);
color: #fff; color: var(--bg-primary);
padding: 0 16px; padding: 0 16px;
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
cursor: pointer; 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 { button:disabled {

View File

@@ -17,7 +17,7 @@
} }
</div> </div>
} @else { } @else {
<p class="empty">{{ 'productDetails.reviewsEmpty' | translate }}</p> <app-empty-state class="empty" [title]="'productDetails.reviewsEmpty' | translate" />
} }
@if (result && totalPages > 1 && mode === 'pages') { @if (result && totalPages > 1 && mode === 'pages') {

View File

@@ -35,6 +35,11 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
.pager button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.load-more-wrap { .load-more-wrap {
display: grid; display: grid;
justify-items: center; justify-items: center;
@@ -50,6 +55,17 @@
padding: 0 16px; padding: 0 16px;
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);
cursor: pointer; 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 { .load-more-wrap small {

View File

@@ -5,11 +5,12 @@ import { TranslatePipe } from '../../../../../../i18n/translate.pipe';
import { RatingSummaryComponent } from '../rating-summary/rating-summary.component'; import { RatingSummaryComponent } from '../rating-summary/rating-summary.component';
import { ReviewCardComponent } from '../review-card/review-card.component'; import { ReviewCardComponent } from '../review-card/review-card.component';
import { ReviewFormComponent } from '../review-form/review-form.component'; import { ReviewFormComponent } from '../review-form/review-form.component';
import { EmptyStateComponent } from '../../../../../../shared/ui/empty-state/empty-state.component';
@Component({ @Component({
selector: 'app-review-list', selector: 'app-review-list',
standalone: true, standalone: true,
imports: [RatingSummaryComponent, ReviewCardComponent, ReviewFormComponent, TranslatePipe], imports: [RatingSummaryComponent, ReviewCardComponent, ReviewFormComponent, TranslatePipe, EmptyStateComponent],
templateUrl: './review-list.component.html', templateUrl: './review-list.component.html',
styleUrls: ['./review-list.component.scss'], styleUrls: ['./review-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -5,6 +5,7 @@
class="star-btn" class="star-btn"
[class.selected]="star <= rating" [class.selected]="star <= rating"
[attr.aria-checked]="star === rating" [attr.aria-checked]="star === rating"
[attr.aria-label]="star + ' ' + ('productDetails.starsLabel' | translate)"
role="radio" role="radio"
(click)="ratingChange.emit(star)"> (click)="ratingChange.emit(star)">

View File

@@ -3,9 +3,9 @@
<table class="compare-table"> <table class="compare-table">
<thead> <thead>
<tr> <tr>
<th>{{ 'ux.compareAttribute' | translate }}</th> <th scope="col">{{ 'ux.compareAttribute' | translate }}</th>
@for (product of products; track product.itemID) { @for (product of products; track product.itemID) {
<th> <th scope="col">
<div class="compare-product-title">{{ product.name }}</div> <div class="compare-product-title">{{ product.name }}</div>
</th> </th>
} }
@@ -14,7 +14,7 @@
<tbody> <tbody>
@for (row of rows(); track row.key) { @for (row of rows(); track row.key) {
<tr [class.compare-different]="isDifferentRow(row)"> <tr [class.compare-different]="isDifferentRow(row)">
<th>{{ row.label }}</th> <th scope="row">{{ row.label }}</th>
@for (value of row.values; track $index) { @for (value of row.values; track $index) {
<td>{{ value }}</td> <td>{{ value }}</td>
} }

View File

@@ -24,6 +24,18 @@
background: color-mix(in srgb, var(--bg-secondary) 60%, white); 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 { .compare-product-title {
font-size: var(--font-size-md, 0.9375rem); font-size: var(--font-size-md, 0.9375rem);
font-weight: var(--font-weight-bold, 700); font-weight: var(--font-weight-bold, 700);

View File

@@ -50,6 +50,12 @@
border-radius: var(--radius-full, 999px); border-radius: var(--radius-full, 999px);
padding: 4px 10px; padding: 4px 10px;
background: var(--bg-primary); 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 { .compare-product-chip a {
@@ -58,11 +64,33 @@
font-weight: var(--font-weight-semibold, 600); font-weight: var(--font-weight-semibold, 600);
} }
.compare-product-chip a:hover {
color: var(--primary-color);
text-decoration: underline;
}
.compare-product-chip button { .compare-product-chip button {
border: 0; border: 0;
border-radius: 50%;
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent; background: transparent;
color: var(--text-secondary); color: var(--text-secondary);
cursor: pointer; 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 { .compare-empty {

View File

@@ -283,6 +283,7 @@ export const en: Translations = {
mediaAria: 'Product media', mediaAria: 'Product media',
ratingStarsAria: 'Rating stars', ratingStarsAria: 'Rating stars',
selectRatingAria: 'Select rating', selectRatingAria: 'Select rating',
starsLabel: 'stars',
reviewsEmpty: 'No reviews yet.', reviewsEmpty: 'No reviews yet.',
questionsEmpty: 'No questions yet.', questionsEmpty: 'No questions yet.',
askQuestion: 'Ask question', askQuestion: 'Ask question',

View File

@@ -283,6 +283,7 @@ export const hy: Translations = {
mediaAria: 'Ապրանքի մեդիա', mediaAria: 'Ապրանքի մեդիա',
ratingStarsAria: 'Աստղային վարկանիշ', ratingStarsAria: 'Աստղային վարկանիշ',
selectRatingAria: 'Ընտրեք գնահատականը', selectRatingAria: 'Ընտրեք գնահատականը',
starsLabel: 'աստղ',
reviewsEmpty: 'Կարծիքներ դեռ չկան։', reviewsEmpty: 'Կարծիքներ դեռ չկան։',
questionsEmpty: 'Հարցեր դեռ չկան։', questionsEmpty: 'Հարցեր դեռ չկան։',
askQuestion: 'Տալ հարց', askQuestion: 'Տալ հարց',

View File

@@ -283,6 +283,7 @@ export const ru: Translations = {
mediaAria: 'Медиа товара', mediaAria: 'Медиа товара',
ratingStarsAria: 'Рейтинг в звездах', ratingStarsAria: 'Рейтинг в звездах',
selectRatingAria: 'Выберите оценку', selectRatingAria: 'Выберите оценку',
starsLabel: 'звёзд',
reviewsEmpty: 'Пока нет отзывов.', reviewsEmpty: 'Пока нет отзывов.',
questionsEmpty: 'Пока нет вопросов.', questionsEmpty: 'Пока нет вопросов.',
askQuestion: 'Задать вопрос', askQuestion: 'Задать вопрос',

View File

@@ -281,6 +281,7 @@ export interface Translations {
mediaAria: string; mediaAria: string;
ratingStarsAria: string; ratingStarsAria: string;
selectRatingAria: string; selectRatingAria: string;
starsLabel: string;
reviewsEmpty: string; reviewsEmpty: string;
questionsEmpty: string; questionsEmpty: string;
askQuestion: string; askQuestion: string;