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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
}
|
||||
|
||||
@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) {
|
||||
<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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
@if (media.length > 1) {
|
||||
<div class="product-gallery-thumbs">
|
||||
@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') {
|
||||
<span class="product-gallery-video">▶</span>
|
||||
} @else if (item.type !== 'image') {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
.product-gallery-main {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg, 13px);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
background: var(--bg-primary);
|
||||
display: flex;
|
||||
align-items: 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,
|
||||
video,
|
||||
@@ -39,11 +39,22 @@
|
||||
min-height: 30px;
|
||||
border: 1px solid rgba(30, 60, 56, 0.25);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
border-radius: var(--radius-full, 999px);
|
||||
padding: 0 10px;
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
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 {
|
||||
@@ -55,7 +66,7 @@
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
}
|
||||
|
||||
@@ -81,21 +92,27 @@
|
||||
.product-gallery-thumb {
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #d3dad9;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
background: var(--bg-primary);
|
||||
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 {
|
||||
border-color: #497671;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.18);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: #497671;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
img {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(1.8rem, 4vw, 2.8rem);
|
||||
line-height: 1.08;
|
||||
}
|
||||
@@ -28,19 +28,19 @@
|
||||
min-height: 26px;
|
||||
padding: 3px 9px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: #497671;
|
||||
color: #fff;
|
||||
background: var(--primary-color);
|
||||
color: var(--bg-primary);
|
||||
font-size: var(--font-size-xs, 0.75rem);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
background: #dc2626;
|
||||
background: var(--error-color);
|
||||
}
|
||||
|
||||
.product-rating {
|
||||
color: #697777;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -50,35 +50,35 @@
|
||||
gap: 4px;
|
||||
|
||||
strong {
|
||||
color: #1e3c38;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-4xl, 2rem);
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.old-price {
|
||||
color: #a1b4b5;
|
||||
color: var(--secondary-color);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.product-stock {
|
||||
color: #697777;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 800;
|
||||
|
||||
&.out {
|
||||
color: #b91c1c;
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.low {
|
||||
color: #b45309;
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
&.medium {
|
||||
color: #497671;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&.high {
|
||||
color: #15803d;
|
||||
color: var(--success-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,16 +94,28 @@
|
||||
min-height: 48px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-lg, 13px);
|
||||
background: #497671;
|
||||
color: #fff;
|
||||
background: var(--primary-color);
|
||||
color: var(--bg-primary);
|
||||
font-size: var(--font-size-lg, 1rem);
|
||||
font-weight: 900;
|
||||
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 {
|
||||
background: #3d635f;
|
||||
background: var(--primary-hover);
|
||||
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 {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #1e3c38;
|
||||
font-size: 1.45rem;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
<div class="variant-options">
|
||||
@for (option of group.options; track option.value) {
|
||||
@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 {
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn"
|
||||
role="tab"
|
||||
[class.active]="activeKey === tab.key"
|
||||
[attr.aria-selected]="activeKey === tab.key"
|
||||
[disabled]="tab.enabled === false"
|
||||
(click)="selectTab(tab)">
|
||||
<span>{{ tab.label }}</span>
|
||||
|
||||
@@ -60,8 +60,8 @@ header {
|
||||
}
|
||||
|
||||
.accepted-pill {
|
||||
background: #e9f9ec;
|
||||
color: #18723a;
|
||||
background: color-mix(in srgb, var(--success-color) 16%, white);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
footer {
|
||||
|
||||
@@ -16,6 +16,11 @@ textarea {
|
||||
padding: 10px 12px;
|
||||
font: inherit;
|
||||
resize: vertical;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
textarea:focus-visible {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.check {
|
||||
@@ -30,11 +35,22 @@ 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 {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<section class="question-list section">
|
||||
@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 }} }
|
||||
</button>
|
||||
} @else {
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="empty">{{ 'productDetails.questionsEmpty' | translate }}</p>
|
||||
<app-empty-state class="empty" [title]="'productDetails.questionsEmpty' | translate" />
|
||||
}
|
||||
|
||||
@if (result && totalPages > 1) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<p class="empty">{{ 'productDetails.reviewsEmpty' | translate }}</p>
|
||||
<app-empty-state class="empty" [title]="'productDetails.reviewsEmpty' | translate" />
|
||||
}
|
||||
|
||||
@if (result && totalPages > 1 && mode === 'pages') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)">
|
||||
★
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<table class="compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'ux.compareAttribute' | translate }}</th>
|
||||
<th scope="col">{{ 'ux.compareAttribute' | translate }}</th>
|
||||
@for (product of products; track product.itemID) {
|
||||
<th>
|
||||
<th scope="col">
|
||||
<div class="compare-product-title">{{ product.name }}</div>
|
||||
</th>
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<tbody>
|
||||
@for (row of rows(); track row.key) {
|
||||
<tr [class.compare-different]="isDifferentRow(row)">
|
||||
<th>{{ row.label }}</th>
|
||||
<th scope="row">{{ row.label }}</th>
|
||||
@for (value of row.values; track $index) {
|
||||
<td>{{ value }}</td>
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -283,6 +283,7 @@ export const hy: Translations = {
|
||||
mediaAria: 'Ապրանքի մեդիա',
|
||||
ratingStarsAria: 'Աստղային վարկանիշ',
|
||||
selectRatingAria: 'Ընտրեք գնահատականը',
|
||||
starsLabel: 'աստղ',
|
||||
reviewsEmpty: 'Կարծիքներ դեռ չկան։',
|
||||
questionsEmpty: 'Հարցեր դեռ չկան։',
|
||||
askQuestion: 'Տալ հարց',
|
||||
|
||||
@@ -283,6 +283,7 @@ export const ru: Translations = {
|
||||
mediaAria: 'Медиа товара',
|
||||
ratingStarsAria: 'Рейтинг в звездах',
|
||||
selectRatingAria: 'Выберите оценку',
|
||||
starsLabel: 'звёзд',
|
||||
reviewsEmpty: 'Пока нет отзывов.',
|
||||
questionsEmpty: 'Пока нет вопросов.',
|
||||
askQuestion: 'Задать вопрос',
|
||||
|
||||
@@ -281,6 +281,7 @@ export interface Translations {
|
||||
mediaAria: string;
|
||||
ratingStarsAria: string;
|
||||
selectRatingAria: string;
|
||||
starsLabel: string;
|
||||
reviewsEmpty: string;
|
||||
questionsEmpty: string;
|
||||
askQuestion: string;
|
||||
|
||||
Reference in New Issue
Block a user