diff --git a/docs/Catalog-Module-Report.md b/docs/Catalog-Module-Report.md
index 7183244..35c3ec8 100644
--- a/docs/Catalog-Module-Report.md
+++ b/docs/Catalog-Module-Report.md
@@ -207,6 +207,71 @@ Catalog spacing and controls were polished for desktop/tablet/mobile:
- drawer/sheet interaction surfaces
- horizontal overflow prevention
+## Sprint UI Polish (Visual Only)
+
+This sprint applies visual and responsive UX polish only. No facade contracts, business logic, API contracts, runtime/bootstrap architecture, or widget contracts were changed.
+
+### Desktop Layout
+
+- Catalog products section uses a cleaner two-column structure with consistent spacing tokens (8/12/16/24/32).
+- Filters panel remains sticky on desktop and uses collapsible sections with smooth expand/collapse animation.
+- Product cards keep consistent image height/aspect ratio and improved vertical rhythm between image/title/rating/price/actions.
+- Product action controls are vertical floating circles in the image top-right zone with fixed spacing and no overlap.
+
+### Tablet Layout
+
+- Sidebar filters transition into drawer interaction for better content width.
+- Sort and layout controls retain consistent sizing and spacing.
+- Grid/list results avoid horizontal overflow and preserve button/input containment.
+
+### Mobile Layout
+
+- Permanent sidebar is hidden.
+- Sticky toolbar provides three entry points: Filters, Sort, Grid.
+- Filters open in drawer form with scrollable content and fixed bottom actions.
+- Sort and Grid open bottom-sheet style dialogs.
+- Focus states and keyboard dismissal (`Esc`) are preserved for all overlays.
+
+### Grid Types
+
+- `grid`
+- `large-grid`
+- `compact-grid`
+- `list`
+
+All grid switch icons are normalized in size and selected state is visually highlighted.
+
+### Filter Drawer and Sections
+
+- Filter groups (Price, Availability, Rating, Brand, etc.) are collapsible.
+- Range inputs are stacked vertically (`From`, `To`) with full-width controls and 12px+ spacing.
+- Slider remains below price inputs for predictable scan order.
+
+### Product Card Anatomy
+
+- Image area: square ratio, `object-fit: contain`, padded image content.
+- Status elements (discount/stock/badges) positioned to avoid action collisions.
+- Actions: top-right vertical controls with equal circular dimensions.
+- Content: title, optional description, rating, pricing, stock indicator, CTA.
+
+### Animations
+
+- Card hover: subtle elevation + `translateY(-2px)`.
+- Button/selector transitions: ~180-200ms.
+- Filter group expand/collapse: smooth height/opacity transition.
+- Drawer and sheet overlays: subtle slide/fade entrance.
+
+### Empty and Loading States
+
+- Empty results state keeps friendly message and action while hiding non-essential catalog controls when no products are rendered.
+- Skeletons for cards/results keep stable heights to reduce layout shift.
+
+### Accessibility Notes
+
+- Added/standardized visible `:focus-visible` outlines for interactive elements.
+- Product quick action controls now expose aria labels.
+- Modal/drawer interactions continue to use dialog semantics and focus trap.
+
### Localization and Accessibility
- New strings for empty states, drawer/sheet UI, and toolbar were added to all languages:
diff --git a/src/app/components/product-card/product-card.component.html b/src/app/components/product-card/product-card.component.html
index 394cb0c..e499317 100644
--- a/src/app/components/product-card/product-card.component.html
+++ b/src/app/components/product-card/product-card.component.html
@@ -3,7 +3,7 @@
![]()
@if (showDiscountBadge && item.discount > 0) {
-
-{{ item.discount }}%
+
-{{ item.discount }}%
}
@if (showStock && item.remainings) {
{{ item.remainings }}
@@ -12,16 +12,16 @@
@if (showFavoriteControl() || showCompareControl() || showShareAction || showQuickViewPlaceholder) {
@if (showFavoriteControl()) {
-
+
}
@if (showCompareControl()) {
-
+
}
@if (showShareAction) {
-
+
}
@if (showQuickViewPlaceholder) {
-
+
}
}
diff --git a/src/app/components/product-card/product-card.component.scss b/src/app/components/product-card/product-card.component.scss
index 06742b2..bd307f5 100644
--- a/src/app/components/product-card/product-card.component.scss
+++ b/src/app/components/product-card/product-card.component.scss
@@ -8,7 +8,8 @@
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
- transform: translateY(-4px);
+ transform: translateY(-2px);
+ box-shadow: 0 10px 26px rgba(15, 23, 42, 0.12);
}
}
@@ -23,7 +24,8 @@
.product-image {
position: relative;
width: 100%;
- aspect-ratio: 4 / 3;
+ aspect-ratio: 1 / 1;
+ min-height: 220px;
border: 1px solid #d3dad9;
border-radius: 13px 13px 0 0;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
@@ -37,22 +39,22 @@
width: 100%;
height: 100%;
object-fit: contain;
- background: white;
- padding: 12px;
- transition: transform 0.3s ease, opacity 0.3s ease;
+ background: #fff;
+ padding: 16px;
+ transition: transform 0.2s ease;
}
&:hover img {
- transform: scale(1.05);
+ transform: scale(1.02);
}
}
.discount-badge {
position: absolute;
top: 12px;
- right: 12px;
+ left: 12px;
background: #ef4444;
- color: white;
+ color: #fff;
padding: 6px 12px;
border-radius: 20px;
font-weight: 700;
@@ -62,7 +64,7 @@
.stock-badge {
position: absolute;
- right: 12px;
+ left: 12px;
bottom: 12px;
z-index: 1;
min-height: 22px;
@@ -84,26 +86,26 @@
.product-actions-overlay {
position: absolute;
right: 12px;
- top: 44px;
+ top: 12px;
z-index: 2;
display: grid;
- gap: 6px;
+ gap: 8px;
}
.product-action-btn {
- width: 30px;
- height: 30px;
+ width: 36px;
+ height: 36px;
border: 1px solid rgba(30, 60, 56, 0.15);
border-radius: 50%;
background: rgba(255, 255, 255, 0.95);
color: #1e3c38;
- font-size: 0.9rem;
+ font-size: 1rem;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
- transition: transform 0.18s ease, border-color 0.2s ease, color 0.2s ease;
+ transition: transform 0.18s ease, border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.product-action-btn:hover {
@@ -116,6 +118,13 @@
color: #497671;
}
+.product-action-btn:focus-visible,
+.product-link:focus-visible,
+.add-to-cart-btn:focus-visible {
+ outline: 3px solid rgba(73, 118, 113, 0.25);
+ outline-offset: 2px;
+}
+
.product-action-favorite.active {
animation: favorite-pulse 280ms ease-out;
}
@@ -134,12 +143,13 @@
.product-badges-overlay {
position: absolute;
- left: 10px;
- top: 10px;
+ left: 12px;
+ top: 46px;
display: flex;
gap: 6px;
flex-wrap: wrap;
z-index: 1;
+ max-width: calc(100% - 60px);
}
.product-badge {
@@ -160,13 +170,13 @@
border: 1px solid #d3dad9;
border-top: none;
border-radius: 0 0 13px 13px;
- padding: 12px 16px;
+ padding: 16px;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
background: #f5f3f9;
flex: 1;
display: flex;
flex-direction: column;
- gap: 8px;
+ gap: 12px;
}
.product-name {
@@ -212,7 +222,7 @@
.product-price {
display: flex;
align-items: baseline;
- gap: 8px;
+ gap: 12px;
flex-wrap: wrap;
}
@@ -259,7 +269,7 @@
.add-to-cart-btn {
width: 100%;
- margin-top: 10px;
+ margin-top: 12px;
min-height: 42px;
border: 0;
border-radius: 13px;
@@ -280,9 +290,26 @@
}
}
+@media (max-width: 640px) {
+ .product-image {
+ min-height: 200px;
+ }
+
+ .product-action-btn {
+ width: 34px;
+ height: 34px;
+ }
+
+ .product-details {
+ gap: 10px;
+ padding: 14px;
+ }
+}
+
.product-card-compact {
.product-details {
padding: 10px 12px;
+ gap: 8px;
}
.product-description,
diff --git a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.html b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.html
index fef7181..8eaf843 100644
--- a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.html
+++ b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.html
@@ -6,121 +6,139 @@
@for (filter of definitions; track filter.id) {
@if (filter.enabled) {
- {{ filter.label }}
+
- @if (filter.type === 'checkbox' || filter.type === 'availability') {
-
}
}
diff --git a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.scss b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.scss
index 4560da0..f09304f 100644
--- a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.scss
+++ b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.scss
@@ -1,7 +1,7 @@
.catalog-filters {
- padding: 14px;
+ padding: 16px;
display: grid;
- gap: 14px;
+ gap: 16px;
align-content: start;
}
@@ -19,29 +19,107 @@
.filter-group {
display: grid;
- gap: 8px;
+ gap: 12px;
border-top: 1px solid var(--border-color);
- padding-top: 10px;
+ padding-top: 12px;
}
-.filter-group h4 {
- margin: 0;
- font-size: 0.95rem;
+.filter-group-toggle {
+ min-height: 36px;
+ border: 0;
+ background: transparent;
color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 0.95rem;
+ font-weight: 700;
+ text-align: left;
+ cursor: pointer;
+ padding: 0;
+}
+
+.filter-group-chevron {
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ transition: transform 0.2s ease;
+}
+
+.filter-group-toggle.collapsed .filter-group-chevron {
+ transform: rotate(-90deg);
+}
+
+.filter-group-body {
+ display: grid;
+ gap: 12px;
+ grid-template-rows: 1fr;
+ overflow: hidden;
+ transition: grid-template-rows 0.2s ease, opacity 0.2s ease;
+ opacity: 1;
+}
+
+.filter-group-body.collapsed {
+ grid-template-rows: 0fr;
+ opacity: 0;
+}
+
+.filter-group-body > * {
+ min-height: 0;
+}
+
+.filter-group-toggle:focus-visible,
+.option-check input:focus-visible,
+.range-inputs input:focus-visible,
+.slider-inputs input:focus-visible,
+.size-chip:focus-visible,
+.rating-chip:focus-visible,
+.color-swatch:focus-visible {
+ outline: 3px solid color-mix(in srgb, var(--primary-color) 28%, white);
+ outline-offset: 2px;
+}
+
+.filter-group h4,
+.range-field span {
+ margin: 0;
+ font-size: 0.85rem;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+ color: var(--text-secondary);
+}
+
+.range-field {
+ display: grid;
+ gap: 8px;
+}
+
+.range-field input {
+ width: 100%;
}
.options {
display: grid;
- gap: 6px;
+ gap: 8px;
}
.option-check {
display: grid;
- grid-template-columns: auto auto 1fr auto;
+ grid-template-columns: auto auto minmax(0, 1fr) auto;
align-items: center;
- gap: 8px;
+ gap: 10px;
+ color: var(--text-primary);
+ min-height: 32px;
+}
+
+.option-check small {
color: var(--text-secondary);
- font-size: 0.9rem;
+}
+
+.option-check input {
+ width: 16px;
+ height: 16px;
+ accent-color: var(--primary-color);
}
.availability-dot {
@@ -65,20 +143,23 @@
.range-inputs {
display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
+ grid-template-columns: 1fr;
+ gap: 12px;
}
.range-inputs input {
- min-height: 34px;
+ min-height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
- padding: 0 8px;
+ padding: 0 12px;
+ font: inherit;
+ color: var(--text-primary);
+ transition: border-color 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease;
}
.slider-inputs {
display: grid;
- gap: 8px;
+ gap: 12px;
}
.slider-inputs input {
@@ -87,7 +168,8 @@
.slider-inputs span {
color: var(--text-secondary);
- font-size: 0.85rem;
+ font-size: 0.88rem;
+ font-weight: 600;
}
.color-options,
@@ -99,28 +181,35 @@
}
.color-swatch {
- width: 24px;
- height: 24px;
+ width: 30px;
+ height: 30px;
border: 2px solid transparent;
border-radius: 50%;
cursor: pointer;
+ transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.color-swatch.active {
border-color: var(--text-primary);
}
+.color-swatch:hover {
+ transform: translateY(-1px);
+}
+
.size-chip,
.rating-chip {
- min-height: 30px;
+ min-height: 34px;
border: 1px solid var(--border-color);
- border-radius: 999px;
+ border-radius: var(--radius-sm);
background: var(--bg-primary);
- padding: 0 10px;
+ color: var(--text-primary);
+ padding: 0 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
- gap: 4px;
+ gap: 6px;
+ transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
}
.size-chip.active,
@@ -129,15 +218,26 @@
background: color-mix(in srgb, var(--primary-color) 8%, white);
}
+.size-chip:hover,
+.rating-chip:hover {
+ transform: translateY(-1px);
+ border-color: var(--primary-color);
+}
+
+.size-chip:disabled,
+.rating-chip:disabled,
+.color-swatch:disabled,
+.range-inputs input:disabled,
+.slider-inputs input:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+
@media (max-width: 1024px) {
.catalog-filters {
gap: 12px;
}
- .range-inputs {
- grid-template-columns: 1fr;
- }
-
.option-check {
grid-template-columns: auto auto 1fr;
}
diff --git a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts
index 170d0e7..2cf6971 100644
--- a/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts
+++ b/src/app/features/website/catalog/components/filters-panel/filters-panel.component.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FilterGroup, SearchFilterState } from '../../../../../core/search/models/search.model';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
@@ -17,6 +17,19 @@ export class CatalogFiltersPanelComponent {
@Output() stateChange = new EventEmitter
();
+ readonly collapsed = signal>({});
+
+ toggleGroup(filterId: string): void {
+ this.collapsed.update(current => ({
+ ...current,
+ [filterId]: !current[filterId],
+ }));
+ }
+
+ isCollapsed(filterId: string): boolean {
+ return this.collapsed()[filterId] ?? false;
+ }
+
isSelected(filterId: string, optionValue: string): boolean {
return (this.state.values[filterId] ?? []).includes(optionValue);
}
diff --git a/src/app/features/website/catalog/components/layout-switcher/layout-switcher.component.scss b/src/app/features/website/catalog/components/layout-switcher/layout-switcher.component.scss
index fadd2bf..60ce0f3 100644
--- a/src/app/features/website/catalog/components/layout-switcher/layout-switcher.component.scss
+++ b/src/app/features/website/catalog/components/layout-switcher/layout-switcher.component.scss
@@ -6,23 +6,24 @@
}
.layout-btn {
- min-height: 36px;
+ min-height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-primary);
font-weight: 700;
- padding: 0 10px;
+ padding: 0 12px;
display: inline-flex;
align-items: center;
- gap: 6px;
+ gap: 8px;
cursor: pointer;
- transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
+ transition: transform 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
}
.layout-btn svg {
- width: 16px;
- height: 16px;
+ width: 18px;
+ height: 18px;
+ flex: 0 0 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
@@ -30,7 +31,8 @@
.layout-btn.active {
border-color: var(--primary-color);
- background: color-mix(in srgb, var(--primary-color) 10%, white);
+ background: color-mix(in srgb, var(--primary-color) 12%, white);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary-color) 30%, white);
}
.layout-btn:hover {
@@ -38,6 +40,11 @@
border-color: var(--primary-color);
}
+.layout-btn:focus-visible {
+ outline: 3px solid color-mix(in srgb, var(--primary-color) 28%, white);
+ outline-offset: 2px;
+}
+
@media (max-width: 640px) {
.catalog-layout-switcher {
width: 100%;
diff --git a/src/app/features/website/catalog/components/search-results/search-results.component.scss b/src/app/features/website/catalog/components/search-results/search-results.component.scss
index 9624025..98de9dd 100644
--- a/src/app/features/website/catalog/components/search-results/search-results.component.scss
+++ b/src/app/features/website/catalog/components/search-results/search-results.component.scss
@@ -1,6 +1,6 @@
.catalog-search-results {
display: grid;
- gap: 14px;
+ gap: 16px;
}
.results-head {
@@ -8,7 +8,8 @@
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
- gap: 8px;
+ gap: 12px;
+ min-width: 0;
}
.results-head strong {
@@ -22,13 +23,16 @@
.results-skeletons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: 14px;
+ gap: 16px;
}
.skeleton-card {
- min-height: 220px;
- border-radius: var(--radius-md);
- background: linear-gradient(90deg, #edf1f1 25%, #e2ebea 50%, #edf1f1 75%);
+ min-height: 340px;
+ border: 1px solid #d3dad9;
+ border-radius: 13px;
+ background:
+ linear-gradient(90deg, #edf1f1 25%, #e2ebea 50%, #edf1f1 75%) 16px 16px / calc(100% - 32px) calc(100% - 32px) no-repeat,
+ #f5f3f9;
background-size: 200% 100%;
animation: shimmer 1.2s linear infinite;
}
@@ -55,11 +59,11 @@
display: flex;
align-items: center;
justify-content: space-between;
- gap: 10px;
+ gap: 12px;
}
.results-pager button {
- min-height: 36px;
+ min-height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
@@ -68,6 +72,11 @@
transition: border-color 0.2s ease, transform 0.2s ease;
}
+.results-pager button:focus-visible {
+ outline: 3px solid color-mix(in srgb, var(--primary-color) 28%, white);
+ outline-offset: 2px;
+}
+
.results-pager button:hover:not(:disabled) {
border-color: var(--primary-color);
transform: translateY(-1px);
diff --git a/src/app/features/website/catalog/containers/catalog-container.component.html b/src/app/features/website/catalog/containers/catalog-container.component.html
index 04bfa48..9506696 100644
--- a/src/app/features/website/catalog/containers/catalog-container.component.html
+++ b/src/app/features/website/catalog/containers/catalog-container.component.html
@@ -107,9 +107,9 @@
@if (showMobileToolbar()) {
-
-
-
+
+
+
}
@@ -249,5 +249,23 @@
}
+
+ @if (isMobile() && gridSheetOpen()) {
+
+
+
+ {{ 'catalog.gridCycle' | translate }}
+
+
+
+
+ @for (layout of availableLayouts; track layout) {
+
+ }
+
+
+ }
}
diff --git a/src/app/features/website/catalog/containers/catalog-container.component.scss b/src/app/features/website/catalog/containers/catalog-container.component.scss
index d6e0992..bd78b66 100644
--- a/src/app/features/website/catalog/containers/catalog-container.component.scss
+++ b/src/app/features/website/catalog/containers/catalog-container.component.scss
@@ -3,13 +3,13 @@
margin: 0 auto;
padding: 24px;
color: #1e3c38;
- overflow-x: clip;
+ overflow-x: hidden;
}
.catalog-header {
display: flex;
flex-direction: column;
- gap: 14px;
+ gap: 16px;
margin-bottom: 24px;
}
@@ -81,7 +81,7 @@
display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
align-items: start;
- gap: 16px;
+ gap: 24px;
}
.catalog-left-panel {
@@ -91,25 +91,26 @@
.catalog-results-panel {
display: grid;
- gap: 14px;
+ gap: 16px;
+ min-width: 0;
}
.catalog-tools-row {
display: flex;
flex-wrap: wrap;
- gap: 10px;
+ gap: 12px;
align-items: center;
justify-content: space-between;
}
.catalog-mobile-toolbar {
position: sticky;
- bottom: 10px;
+ bottom: 12px;
z-index: 5;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 8px;
- padding: 8px;
+ gap: 12px;
+ padding: 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--bg-primary) 92%, white);
@@ -118,14 +119,22 @@
.catalog-mobile-toolbar-btn,
.catalog-open-filters-btn {
- min-height: 40px;
+ min-height: 42px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
color: var(--text-primary);
- padding: 0 10px;
+ padding: 0 12px;
font-weight: 700;
cursor: pointer;
+ transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
+}
+
+.catalog-mobile-toolbar-btn:hover,
+.catalog-open-filters-btn:hover {
+ border-color: var(--primary-color);
+ background: color-mix(in srgb, var(--primary-color) 8%, white);
+ transform: translateY(-1px);
}
.catalog-open-filters-btn {
@@ -135,12 +144,12 @@
.catalog-sort-reset-group {
display: inline-flex;
align-items: center;
- gap: 10px;
+ gap: 12px;
flex-wrap: wrap;
}
.catalog-reset-btn {
- min-height: 36px;
+ min-height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: transparent;
@@ -158,7 +167,7 @@
}
.catalog-save-search-btn {
- min-height: 36px;
+ min-height: 40px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
@@ -169,6 +178,17 @@
transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}
+.catalog-reset-btn:focus-visible,
+.catalog-save-search-btn:focus-visible,
+.catalog-mobile-toolbar-btn:focus-visible,
+.catalog-open-filters-btn:focus-visible,
+.catalog-sort-option:focus-visible,
+.catalog-icon-btn:focus-visible,
+.catalog-apply-btn:focus-visible {
+ outline: 3px solid color-mix(in srgb, var(--primary-color) 28%, white);
+ outline-offset: 2px;
+}
+
.catalog-overlay {
position: fixed;
inset: 0;
@@ -188,6 +208,7 @@
grid-template-rows: auto 1fr auto;
border-left: 1px solid var(--border-color);
box-shadow: -12px 0 28px rgba(15, 23, 42, 0.14);
+ animation: catalog-drawer-in 0.2s ease-out;
}
.catalog-filter-drawer-head,
@@ -221,19 +242,23 @@
.catalog-filter-drawer-body {
overflow: auto;
- padding: 12px;
+ padding: 16px;
+ padding-bottom: 84px;
}
.catalog-filter-drawer-actions {
display: grid;
grid-template-columns: 1fr 1fr;
- gap: 10px;
+ gap: 12px;
padding: 12px;
border-top: 1px solid var(--border-color);
+ position: sticky;
+ bottom: 0;
+ background: var(--bg-primary);
}
.catalog-apply-btn {
- min-height: 40px;
+ min-height: 42px;
border: 0;
border-radius: var(--radius-sm);
background: var(--primary-color);
@@ -256,16 +281,17 @@
box-shadow: 0 -10px 24px rgba(15, 23, 42, 0.16);
max-height: 80vh;
overflow: auto;
+ animation: catalog-sheet-in 0.2s ease-out;
}
.catalog-sort-sheet-options {
display: grid;
- gap: 6px;
- padding: 12px;
+ gap: 8px;
+ padding: 16px;
}
.catalog-sort-option {
- min-height: 42px;
+ min-height: 44px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
background: var(--bg-primary);
@@ -274,6 +300,7 @@
padding: 0 12px;
font-weight: 600;
cursor: pointer;
+ transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
}
.catalog-sort-option.active {
@@ -281,6 +308,15 @@
background: color-mix(in srgb, var(--primary-color) 10%, white);
}
+.catalog-sort-option:hover {
+ border-color: var(--primary-color);
+ transform: translateY(-1px);
+}
+
+.catalog-grid-sheet .catalog-sort-sheet-options {
+ grid-template-columns: 1fr;
+}
+
.catalog-save-search-btn:hover {
transform: translateY(-1px);
border-color: var(--primary-color);
@@ -305,11 +341,11 @@
.subcategory-chips {
display: flex;
flex-wrap: wrap;
- gap: 8px;
+ gap: 12px;
}
.subcategory-chip {
- min-height: 32px;
+ min-height: 36px;
border: 1px solid var(--border-color);
border-radius: 999px;
background: var(--bg-primary);
@@ -361,14 +397,14 @@
.catalog-loading {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
- gap: 24px;
+ gap: 16px;
}
.catalog-skeleton-card {
- min-height: 260px;
+ min-height: 340px;
border: 1px solid #d3dad9;
border-radius: 13px;
- padding: 12px;
+ padding: 16px;
background: #f5f3f9;
}
@@ -381,13 +417,13 @@
}
.catalog-skeleton-image {
- aspect-ratio: 4 / 3;
- margin-bottom: 14px;
+ aspect-ratio: 1 / 1;
+ margin-bottom: 16px;
}
.catalog-skeleton-line {
height: 14px;
- margin-top: 10px;
+ margin-top: 12px;
}
.catalog-skeleton-title {
@@ -442,6 +478,28 @@
100% { background-position: -200% 0; }
}
+@keyframes catalog-sheet-in {
+ from {
+ transform: translateY(10px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+@keyframes catalog-drawer-in {
+ from {
+ transform: translateX(12px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
@media (max-width: 640px) {
.catalog-page {
padding: 16px;
@@ -454,7 +512,7 @@
.catalog-products-section {
grid-template-columns: 1fr;
- gap: 14px;
+ gap: 16px;
}
.catalog-left-panel {
@@ -489,6 +547,16 @@
font-size: 1.5rem;
}
+ .catalog-mobile-toolbar {
+ bottom: 8px;
+ gap: 8px;
+ padding: 8px;
+ }
+
+ .catalog-mobile-toolbar-btn {
+ min-height: 40px;
+ }
+
.catalog-section-heading h1 {
font-size: 1.35rem;
}
diff --git a/src/app/features/website/catalog/containers/catalog-container.component.ts b/src/app/features/website/catalog/containers/catalog-container.component.ts
index abc4a3a..3b5b3d3 100644
--- a/src/app/features/website/catalog/containers/catalog-container.component.ts
+++ b/src/app/features/website/catalog/containers/catalog-container.component.ts
@@ -93,6 +93,7 @@ export class CatalogContainerComponent {
readonly viewportWidth = signal(typeof window !== 'undefined' ? window.innerWidth : 1280);
readonly filterDrawerOpen = signal(false);
readonly sortSheetOpen = signal(false);
+ readonly gridSheetOpen = signal(false);
readonly noResults = computed(() => !this.loadingProducts() && this.viewMode() === 'products' && this.products().length === 0);
readonly isDrawerLayout = computed(() => this.viewportWidth() <= 1024);
readonly isMobile = computed(() => this.viewportWidth() <= 767);
@@ -101,7 +102,7 @@ export class CatalogContainerComponent {
readonly isFilteredEmptyState = computed(() => this.viewMode() === 'products' && !this.loadingProducts() && this.rawProducts().length > 0 && this.products().length === 0);
readonly showDesktopFilters = computed(() => this.viewMode() === 'products' && !this.isEmptyCategoryState() && !this.isDrawerLayout());
readonly showMobileToolbar = computed(() => this.viewMode() === 'products' && !this.isEmptyCategoryState() && this.isMobile());
- readonly showCatalogTools = computed(() => this.viewMode() === 'products' && !this.isEmptyCategoryState() && !this.loadingProducts());
+ readonly showCatalogTools = computed(() => this.viewMode() === 'products' && this.products().length > 0 && !this.loadingProducts());
readonly mobileSortOptions = computed(() => {
const allowed = new Set(['relevance', 'latest', 'price_asc', 'price_desc', 'rating', 'popular']);
const options = this.sortDefinitions().filter(option => allowed.has(option.id));
@@ -408,6 +409,10 @@ export class CatalogContainerComponent {
@HostListener('document:keydown.escape')
onEscapeKey(): void {
+ if (this.gridSheetOpen()) {
+ this.closeGridSheet();
+ }
+
if (this.sortSheetOpen()) {
this.closeSortSheet();
}
@@ -471,6 +476,8 @@ export class CatalogContainerComponent {
}
openFilterDrawer(): void {
+ this.closeSortSheet();
+ this.closeGridSheet();
this.filterDrawerOpen.set(true);
}
@@ -483,6 +490,8 @@ export class CatalogContainerComponent {
}
openSortSheet(): void {
+ this.closeFilterDrawer();
+ this.closeGridSheet();
this.sortSheetOpen.set(true);
}
@@ -495,6 +504,35 @@ export class CatalogContainerComponent {
this.closeSortSheet();
}
+ openGridSheet(): void {
+ this.closeFilterDrawer();
+ this.closeSortSheet();
+ this.gridSheetOpen.set(true);
+ }
+
+ closeGridSheet(): void {
+ this.gridSheetOpen.set(false);
+ }
+
+ selectLayoutFromSheet(layout: CatalogLayoutMode): void {
+ this.changeLayout(layout);
+ this.closeGridSheet();
+ }
+
+ layoutLabelKey(layout: CatalogLayoutMode): string {
+ switch (layout) {
+ case 'grid':
+ return 'catalog.layout.grid';
+ case 'large-grid':
+ return 'catalog.layout.largeGrid';
+ case 'compact-grid':
+ return 'catalog.layout.compactGrid';
+ case 'list':
+ default:
+ return 'catalog.layout.list';
+ }
+ }
+
browseCategories(): void {
this.router.navigate([`/${this.languageService.currentLanguage()}/catalog`]);
}