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') { -
- @for (option of filter.options; track option.id) { -
} } 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()) { + + + } } 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`]); }