Files
marketplaces/src/app/widgets/ui/product-carousel-widget.component.ts
sdarbinyan 38253f7e83 fix(storefront): home carousel overflow, static-page content, compare colour swatch
- product-carousel-widget: implicit CSS grid track had no min-width:0,
  so the flex scroller's intrinsic content width (fixed 220px product
  cards) overflowed the grid item and pushed body width to ~2187px on
  a 1440px viewport, squeezing the entire homepage into a ~340px column.
  Added min-width:0 to the track and scroller (standard grid/flex
  overflow fix).
- bootstrap.json mock fixture: about-us/privacy-policy/terms-of-service
  static pages (the real CMS pages served via bootstrap.staticPages,
  per docs/PROJECT_INDEX.md) used a 'content' field, but
  content-page.service.ts's normalizePage() only reads 'html' -
  ContentPageBootstrapInput has no 'content' field. Title rendered,
  body was always empty. Renamed the 3 fixture entries' field from
  content to html to match the schema; content now renders.
- compare-table: colour row rendered raw hex/name values as plain text
  (e.g. '#fCfCfC') with no swatch, inconsistent with variant-selector's
  established colour-swatch pattern used on the product page. Added a
  small circular swatch (reusing the same border-radius:50% pattern)
  next to the value.

Verified live via ng serve: overflow gone (body/viewport width match
at 1440/1280/375), static pages render real content, compare swatch
displays. tsc --noEmit and ng build both clean (pre-existing bundle-
budget warning only, already tracked in KNOWN-ISSUES item 12).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 20:34:17 +04:00

140 lines
4.3 KiB
TypeScript

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild, signal } from '@angular/core';
import { SectionConfig } from '../../shared/models/config';
import { CatalogProductGridComponent } from '../../features/website/catalog/components/product-grid/product-grid.component';
import { ProductCollectionWidgetData } from '../contracts/widget-data.contract';
@Component({
selector: 'app-product-carousel-widget',
standalone: true,
imports: [CommonModule, CatalogProductGridComponent],
template: `
<section class="product-carousel-widget">
@if (data?.title) {
<h2 class="product-carousel-widget__title">{{ data?.title }}</h2>
}
@if (data?.products?.length) {
@if (isCarousel) {
<div class="product-carousel-widget__track">
<button
type="button"
class="product-carousel-widget__arrow product-carousel-widget__arrow--prev"
aria-label="Previous products"
[disabled]="atStart()"
(click)="scrollBy(-1)"
>&larr;</button>
<div class="product-carousel-widget__scroller" #scroller (scroll)="onScroll()">
<app-catalog-product-grid [products]="data?.products ?? []" layout="compact" />
</div>
<button
type="button"
class="product-carousel-widget__arrow product-carousel-widget__arrow--next"
aria-label="Next products"
[disabled]="atEnd()"
(click)="scrollBy(1)"
>&rarr;</button>
</div>
} @else {
<app-catalog-product-grid [products]="data?.products ?? []" />
}
} @else if (data?.emptyMessage) {
<p class="product-carousel-widget__empty">{{ data?.emptyMessage }}</p>
}
</section>
`,
styles: [
`
.product-carousel-widget { display: grid; gap: var(--space-lg, 24px); }
.product-carousel-widget__title {
margin: 0;
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary, #1e3c38);
}
.product-carousel-widget__empty { margin: 0; color: var(--text-secondary, #667a77); }
.product-carousel-widget__track {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.product-carousel-widget__scroller {
flex: 1;
min-width: 0;
overflow-x: auto;
scroll-behavior: smooth;
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
::ng-deep .catalog-product-grid {
display: flex;
flex-wrap: nowrap;
gap: var(--space-md, 16px);
}
::ng-deep .catalog-product-shell {
flex: 0 0 auto;
width: 220px;
}
}
.product-carousel-widget__arrow {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid var(--border-color, #d3dad9);
background: #fff;
color: var(--text-primary, #1e3c38);
cursor: pointer;
font-size: 1rem;
&:hover:not(:disabled) { background: var(--primary-color, #497671); color: #fff; }
&:disabled { opacity: 0.35; cursor: default; }
&:focus-visible { outline: 2px solid var(--primary-color, #497671); outline-offset: 2px; }
}
`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductCarouselWidgetComponent {
@Input() section: SectionConfig | null = null;
@Input() data: ProductCollectionWidgetData | null = null;
@Output() productSelected = new EventEmitter<unknown>();
@ViewChild('scroller') private scroller?: ElementRef<HTMLDivElement>;
readonly atStart = signal(true);
readonly atEnd = signal(false);
get isCarousel(): boolean {
return this.section?.layout?.strategy === 'carousel';
}
scrollBy(direction: -1 | 1): void {
const el = this.scroller?.nativeElement;
if (!el) {
return;
}
el.scrollBy({ left: direction * el.clientWidth * 0.8, behavior: 'smooth' });
}
onScroll(): void {
const el = this.scroller?.nativeElement;
if (!el) {
return;
}
this.atStart.set(el.scrollLeft <= 4);
this.atEnd.set(el.scrollLeft + el.clientWidth >= el.scrollWidth - 4);
}
}