feat: enhance store header layout for better responsiveness and add mobile menu functionality

This commit is contained in:
2026-08-05 12:18:50 -03:00
parent 8aea54959f
commit 21786d7b34
6 changed files with 171 additions and 82 deletions

View File

@@ -1,11 +1,11 @@
<header class="store-layout__header">
<div class="store-layout__header-main">
<div
class="container-xl px-3 px-md-4 py-3 py-lg-4 d-flex flex-row flex-wrap align-items-center justify-content-between gap-3"
>
<div class="container-xl px-3 px-md-4 py-3 py-lg-4">
<div class="row align-items-center gy-3 gy-md-0">
<div class="col-auto col-md-auto order-1">
<a
routerLink="/"
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 flex-shrink-0 text-decoration-none"
class="store-layout__brand-slot d-inline-flex align-items-center gap-3 text-decoration-none"
data-testid="store-header-brand-slot"
aria-label="Logo del comercio"
>
@@ -17,11 +17,14 @@
</div>
}
</a>
</div>
<div
class="d-flex flex-row align-items-center gap-3 ms-auto flex-nowrap flex-grow-1 justify-content-between justify-content-md-end"
>
<div class="store-layout__search-wrapper">
<div class="col-12 px-0 d-md-none order-3">
<hr class="store-layout__mobile-separator">
</div>
<div class="col-12 col-md order-4 order-md-2">
<div class="store-layout__search-wrapper w-100 ms-md-auto me-md-3">
<div class="input-group store-layout__search" role="search">
<label class="visually-hidden" for="store-search-input">Buscar productos</label>
<input
@@ -53,11 +56,13 @@
</p>
}
</div>
</div>
<div class="col-auto col-md-auto order-2 order-md-3 ms-auto d-flex flex-row align-items-center justify-content-end gap-2 gap-md-3">
@if (ticketsMenu(); as menu) {
<app-button
variant="primary"
hostClass="flex-shrink-0"
hostClass="store-layout__tickets-btn-host"
data-testid="store-header-tickets"
(click)="ticketsClick.emit()"
>
@@ -65,17 +70,14 @@
</app-button>
}
<div
class="d-flex align-items-center justify-content-end gap-1"
aria-label="Acciones de usuario"
>
<app-cart-icon
[quantity]="cartQuantity()"
ariaLabel="Carrito de compras"
title="Carrito"
(click)="cartClick.emit()"
/>
<div class="store-layout__user-menu">
<div class="store-layout__user-menu d-none d-md-inline-flex position-relative">
<app-icon-button
variant="user"
ariaLabel="Mi cuenta"
@@ -94,12 +96,46 @@
/>
}
</div>
<div class="store-layout__mobile-menu d-inline-flex d-md-none position-relative">
<app-icon-button
variant="bars"
ariaLabel="Menu"
ariaControls="store-mobile-menu"
[ariaExpanded]="isMobileMenuOpen()"
title="Menu"
(clicked)="toggleMobileMenu()"
/>
@if (isMobileMenuOpen()) {
<div id="store-mobile-menu" class="position-absolute end-0 bg-white rounded shadow mt-2 py-2 store-layout__mobile-dropdown-menu">
<a
class="dropdown-item d-flex align-items-center gap-2 px-3 py-2 text-decoration-none store-layout__mobile-dropdown-item"
(click)="onMobileAccountClick()"
>
<i class="fa-regular fa-circle-user fa-lg"></i>
<span class="fw-medium">Mi cuenta</span>
</a>
<hr class="my-1 border-secondary opacity-25">
@for (category of categories(); track category.id) {
<a
class="dropdown-item px-3 py-2 text-decoration-none store-layout__mobile-dropdown-item"
(click)="onCategorySelect(category)"
>
<span class="fw-medium">{{ category.nombre }}</span>
</a>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
<div class="store-layout__categories">
<div class="store-layout__categories d-none d-md-block">
<div class="container-xl px-3 px-md-4">
<div class="store-layout__category-menu">
<button

View File

@@ -10,7 +10,6 @@
.store-layout__brand-slot {
min-width: 150px;
max-width:40px;
}
.store-layout__brand-logo-shell {
@@ -42,10 +41,16 @@
}
.store-layout__search-wrapper {
width: min(100%, 260px);
width: 100%;
min-width: 0;
}
@media (min-width: 768px) {
.store-layout__search-wrapper {
width: min(100%, 260px);
}
}
.store-layout__search {
width: 100%;
}
@@ -137,5 +142,35 @@
@media (max-width: 767.98px) {
.store-layout__brand-slot {
min-width: 0;
max-width: 150px;
}
.store-layout__tickets-btn-host {
::ng-deep .btn {
min-width: 0 !important;
padding-inline: 1rem;
}
}
.store-layout__mobile-separator {
border-color: #DDDDDD;
opacity: 1;
margin: 0 -1rem;
}
}
.store-layout__mobile-dropdown-menu {
top: 100%;
min-width: 220px;
z-index: 1050;
}
.store-layout__mobile-dropdown-item {
cursor: pointer;
color: #8a8a8a;
&:hover,
&:focus-visible {
color: #666666;
}
}

View File

@@ -1,5 +1,5 @@
import { Component, ElementRef, HostListener, inject, input, output, signal } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Router, RouterLink } from '@angular/router';
import { AuthUser } from '../../../services/auth/auth.interfaces';
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
import { IconButtonComponent } from '../../../../shared/components/icon-button/icon-button.component';
@@ -26,6 +26,7 @@ import { CategoryDropdownComponent } from './category-dropdown/category-dropdown
})
export class StoreHeaderComponent {
private readonly elementRef = inject(ElementRef<HTMLElement>);
private readonly router = inject(Router);
readonly logoUrl = input<string | null>(null);
readonly cartQuantity = input<number>(0);
@@ -43,13 +44,14 @@ export class StoreHeaderComponent {
protected readonly isUserDropdownOpen = signal(false);
protected readonly isCategoryDropdownOpen = signal(false);
protected readonly isMobileMenuOpen = signal(false);
protected readonly minSearchLength = 3;
protected readonly showSearchError = signal(false);
protected readonly searchControl = new FormControl('', { nonNullable: true });
@HostListener('document:click', ['$event'])
protected onDocumentClick(event: MouseEvent): void {
if (!this.isUserDropdownOpen() && !this.isCategoryDropdownOpen()) {
if (!this.isUserDropdownOpen() && !this.isCategoryDropdownOpen() && !this.isMobileMenuOpen()) {
return;
}
@@ -57,6 +59,7 @@ export class StoreHeaderComponent {
if (target instanceof Node && !this.elementRef.nativeElement.contains(target)) {
this.isUserDropdownOpen.set(false);
this.isCategoryDropdownOpen.set(false);
this.isMobileMenuOpen.set(false);
}
}
@@ -64,6 +67,7 @@ export class StoreHeaderComponent {
protected onEscapeKey(): void {
this.isUserDropdownOpen.set(false);
this.isCategoryDropdownOpen.set(false);
this.isMobileMenuOpen.set(false);
}
protected toggleUserDropdown(): void {
@@ -109,6 +113,25 @@ export class StoreHeaderComponent {
protected onCategorySelect(category: Category): void {
this.isCategoryDropdownOpen.set(false);
this.isMobileMenuOpen.set(false);
this.categorySelect.emit(category);
}
protected toggleMobileMenu(): void {
this.isMobileMenuOpen.update((isOpen) => !isOpen);
}
protected onMobileAccountClick(): void {
this.isMobileMenuOpen.set(false);
if (!this.isAuthenticated()) {
this.loginClick.emit();
} else {
const menus = this.accountNavigationMenus();
if (menus.length > 0) {
this.router.navigate([menus[0].route]);
} else {
this.router.navigate(['/cuenta']);
}
}
}
}

View File

@@ -1,8 +1,4 @@
:host {
display: inline-block;
}
:host(.product-detail__cta) {
display: block;
width: 100%;
}
@@ -11,10 +7,6 @@
min-height: 40px;
font-weight: 700;
font-size: 15px;
min-width: 216px;
}
:host(.product-detail__cta) .btn {
width: 100%;
}

View File

@@ -69,7 +69,8 @@
color: var(--tenant-danger);
}
.icon-btn--user {
.icon-btn--user,
.icon-btn--bars {
i {
width: 23px;
height: 23px;

View File

@@ -10,7 +10,8 @@ export type IconButtonVariant =
| 'angle-down'
| 'copy'
| 'download'
| 'share';
| 'share'
| 'bars';
@Component({
selector: 'app-icon-button',
@@ -38,6 +39,7 @@ export class IconButtonComponent {
copy: 'fa-solid fa-copy',
download: 'fa-solid fa-download',
share: 'fa-solid fa-share-nodes',
bars: 'fa-solid fa-bars',
};
return classes[this.variant()];
});