fix(storefront): composition audit fixes for cart, catalog, product, compare, wishlist, static pages

- Replace hand-rolled loading/error/empty markup with shared app-skeleton,
  app-empty-state, and app-button across catalog, product details, cart,
  compare, wishlist, and the public static-page renderer
- Fix hardcoded hex colors that bypassed theme CSS variables (catalog,
  product details), restoring multi-tenant theme correctness
- Remove ~1100 lines of dead "alt" cart theme CSS (never applied by the
  template) from cart.component.scss, bringing it back under the 40kB
  build budget (89.49kB -> 59.39kB cart-component chunk)
- Swap legacy global .btn/.btn-ghost/.btn-primary classes for app-button
  in compare and wishlist empty/toolbar actions
This commit is contained in:
sdarbinyan
2026-07-23 10:37:06 +04:00
parent f261800159
commit 2e31e80e28
18 changed files with 202 additions and 1317 deletions

View File

@@ -1,24 +1,36 @@
<main class="product-details-page">
@if (loading()) {
<section class="product-details-loading" role="status" aria-live="polite">
<div class="product-details-spinner"></div>
<p>{{ 'productDetails.loading' | translate }}</p>
<section class="product-details-loading" role="status" aria-live="polite" aria-busy="true">
<div class="product-details-loading-layout">
<app-skeleton shape="rect" class="product-details-loading-gallery" />
<div class="product-details-loading-main">
<app-skeleton shape="text" width="60%" height="28px" />
<app-skeleton shape="text" width="30%" height="20px" />
<app-skeleton shape="rect" height="120px" />
<app-skeleton shape="rect" height="44px" />
</div>
</div>
<span class="sr-only">{{ 'productDetails.loading' | translate }}</span>
</section>
}
@if (error()) {
<section class="product-details-message error" role="alert">
<h1>{{ 'productDetails.errorTitle' | translate }}</h1>
<p>{{ error()! | translate }}</p>
<button type="button" (click)="retry()">{{ 'productDetails.retry' | translate }}</button>
<app-empty-state [title]="'productDetails.errorTitle' | translate" [description]="error()! | translate">
<div slot="actions">
<app-button variant="primary" (click)="retry()">{{ 'productDetails.retry' | translate }}</app-button>
</div>
</app-empty-state>
</section>
}
@if (missing() && !loading() && !error()) {
<section class="product-details-message">
<h1>{{ 'productDetails.missingTitle' | translate }}</h1>
<p>{{ 'productDetails.missingDescription' | translate }}</p>
<a [routerLink]="'/catalog' | langRoute">{{ 'productDetails.backToCatalog' | translate }}</a>
<app-empty-state [title]="'productDetails.missingTitle' | translate" [description]="'productDetails.missingDescription' | translate">
<div slot="actions">
<app-button variant="primary" [routerLink]="'/catalog' | langRoute">{{ 'productDetails.backToCatalog' | translate }}</app-button>
</div>
</app-empty-state>
</section>
}

View File

@@ -35,58 +35,51 @@
gap: 16px;
}
.product-details-loading,
.product-details-loading {
min-height: 420px;
}
.product-details-message {
min-height: 420px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
text-align: center;
color: #697777;
h1 {
margin: 0;
color: #1e3c38;
}
p {
margin: 0;
max-width: 420px;
}
button,
a {
min-height: 42px;
padding: 0 22px;
border: 0;
border-radius: var(--radius-lg, 13px);
background: #497671;
color: #fff;
text-decoration: none;
display: inline-flex;
align-items: center;
font-weight: 800;
cursor: pointer;
}
}
.product-details-message.error {
color: #991b1b;
::ng-deep .app-empty-state__title {
color: var(--error-color);
}
}
.product-details-spinner {
width: 46px;
height: 46px;
border: 4px solid #d3dad9;
border-top-color: #497671;
border-radius: 50%;
animation: spin 0.8s linear infinite;
.product-details-loading-layout {
display: grid;
grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1fr);
gap: 28px;
align-items: start;
}
@keyframes spin {
to { transform: rotate(360deg); }
.product-details-loading-gallery {
aspect-ratio: 1 / 1;
}
.product-details-loading-main {
display: flex;
flex-direction: column;
gap: 16px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 860px) {
@@ -95,7 +88,8 @@
gap: 24px;
}
.product-details-layout {
.product-details-layout,
.product-details-loading-layout {
grid-template-columns: 1fr;
gap: 18px;
}

View File

@@ -28,6 +28,9 @@ import { ProductVariantSelectorComponent, VariantOptionGroup } from '../componen
import { ProductTabItem, ProductTabKey, ProductTabsComponent } from '../engagement/components/product-tabs/product-tabs.component';
import { QuestionListComponent } from '../engagement/components/question-list/question-list.component';
import { ReviewListComponent } from '../engagement/components/review-list/review-list.component';
import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component';
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
@Component({
selector: 'app-product-details-container',
@@ -48,6 +51,9 @@ import { ReviewListComponent } from '../engagement/components/review-list/review
ReviewListComponent,
QuestionListComponent,
RelatedProductsComponent,
EmptyStateComponent,
SkeletonComponent,
ButtonComponent,
],
templateUrl: './product-details-container.component.html',
styleUrls: ['./product-details-container.component.scss'],