@arroyavecommerce/theme-kinetic 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/design/README.md +22 -0
- package/docs/design/kinetic-street-concept.html +558 -0
- package/package.json +45 -0
- package/src/blocks/_shared.tsx +174 -0
- package/src/blocks/carrusel.tsx +109 -0
- package/src/blocks/category-tiles.test.tsx +59 -0
- package/src/blocks/category-tiles.tsx +70 -0
- package/src/blocks/editorial-banner.test.tsx +96 -0
- package/src/blocks/editorial-banner.tsx +205 -0
- package/src/blocks/faq.test.tsx +68 -0
- package/src/blocks/faq.tsx +122 -0
- package/src/blocks/featured-collection.test.tsx +80 -0
- package/src/blocks/featured-collection.tsx +97 -0
- package/src/blocks/features.test.tsx +71 -0
- package/src/blocks/features.tsx +159 -0
- package/src/blocks/hero.test.tsx +75 -0
- package/src/blocks/hero.tsx +290 -0
- package/src/blocks/index.tsx +35 -0
- package/src/blocks/lookbook.test.tsx +80 -0
- package/src/blocks/lookbook.tsx +117 -0
- package/src/blocks/marquee.test.tsx +54 -0
- package/src/blocks/marquee.tsx +114 -0
- package/src/blocks/newsletter.test.tsx +60 -0
- package/src/blocks/newsletter.tsx +105 -0
- package/src/blocks/product-grid.test.tsx +160 -0
- package/src/blocks/product-grid.tsx +76 -0
- package/src/blocks/promo-countdown.tsx +75 -0
- package/src/blocks/promo.test.tsx +71 -0
- package/src/blocks/promo.tsx +120 -0
- package/src/blocks/rich-text.test.tsx +67 -0
- package/src/blocks/rich-text.tsx +75 -0
- package/src/blocks/spacer.test.tsx +54 -0
- package/src/blocks/spacer.tsx +56 -0
- package/src/blocks/testimonials.test.tsx +74 -0
- package/src/blocks/testimonials.tsx +104 -0
- package/src/blocks/video.test.tsx +159 -0
- package/src/blocks/video.tsx +226 -0
- package/src/catalog/buy-box.test.tsx +225 -0
- package/src/catalog/buy-box.tsx +353 -0
- package/src/catalog/config.test.ts +115 -0
- package/src/catalog/config.ts +167 -0
- package/src/catalog/filter-controls.test.tsx +149 -0
- package/src/catalog/filter-controls.tsx +187 -0
- package/src/catalog/gallery.test.tsx +201 -0
- package/src/catalog/gallery.tsx +212 -0
- package/src/catalog/lightbox.test.tsx +95 -0
- package/src/catalog/lightbox.tsx +184 -0
- package/src/catalog/pdp.test.tsx +309 -0
- package/src/catalog/pdp.tsx +354 -0
- package/src/catalog/plp-arreglos.test.tsx +235 -0
- package/src/catalog/plp-inline.tsx +54 -0
- package/src/catalog/plp-overlay.tsx +129 -0
- package/src/catalog/plp-parts.test.tsx +146 -0
- package/src/catalog/plp-parts.tsx +292 -0
- package/src/catalog/plp-pills.tsx +44 -0
- package/src/catalog/plp-sidebar.tsx +47 -0
- package/src/catalog/plp.test.tsx +319 -0
- package/src/catalog/plp.tsx +259 -0
- package/src/catalog/product-card.test.tsx +221 -0
- package/src/catalog/product-card.tsx +411 -0
- package/src/catalog/registry.tsx +16 -0
- package/src/catalog/related.test.tsx +52 -0
- package/src/catalog/related.tsx +40 -0
- package/src/catalog/reviews.test.tsx +56 -0
- package/src/catalog/reviews.tsx +116 -0
- package/src/catalog/skeletons.test.tsx +61 -0
- package/src/catalog/skeletons.tsx +103 -0
- package/src/chrome/announcement-bar.test.tsx +86 -0
- package/src/chrome/announcement-bar.tsx +173 -0
- package/src/chrome/drawer-movil.test.tsx +113 -0
- package/src/chrome/drawer-movil.tsx +342 -0
- package/src/chrome/footer.test.tsx +133 -0
- package/src/chrome/footer.tsx +603 -0
- package/src/chrome/header.test.tsx +165 -0
- package/src/chrome/header.tsx +592 -0
- package/src/chrome/index.ts +26 -0
- package/src/chrome/mega-menu.test.tsx +161 -0
- package/src/chrome/mega-menu.tsx +317 -0
- package/src/chrome/mini-cart.test.tsx +111 -0
- package/src/chrome/mini-cart.tsx +320 -0
- package/src/content/home.ts +19 -0
- package/src/content/seed.test.ts +33 -0
- package/src/content/seed.ts +28 -0
- package/src/context.ts +104 -0
- package/src/icons/icons.test.tsx +118 -0
- package/src/icons/index.ts +136 -0
- package/src/icons/payment-icons.tsx +241 -0
- package/src/icons/social-icons.tsx +108 -0
- package/src/icons/svg-base.tsx +68 -0
- package/src/index.ts +49 -0
- package/src/puck-config.tsx +404 -0
- package/src/smoke.test.ts +8 -0
- package/src/template.test.tsx +88 -0
- package/src/test/mock-ctx.test.tsx +77 -0
- package/src/test/mock-ctx.tsx +236 -0
- package/src/theme-defaults.test.ts +9 -0
- package/src/theme.test.ts +82 -0
- package/src/theme.ts +195 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import type { ChromeConfig, ChromeVariantDef } from "@arroyavecommerce/cms-core"
|
|
6
|
+
import type { RenderContext } from "../context"
|
|
7
|
+
import { AnnouncementBar, anuncioSchema, anuncioDefaults } from "./announcement-bar"
|
|
8
|
+
import { DrawerMovil } from "./drawer-movil"
|
|
9
|
+
import { MegaMenu, megaMenuSchema, megaMenuDefaults, type MegaMenuData } from "./mega-menu"
|
|
10
|
+
import { MiniCart } from "./mini-cart"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Header de la plantilla de moda — estética "Kinetic Street" (grotesca pesada,
|
|
14
|
+
* cobalto/coral, sombras y radios suaves). Todo se pinta con tokens `--cms-*`
|
|
15
|
+
* CON fallback, así el header se ve bien aunque el tema no publique el token.
|
|
16
|
+
*
|
|
17
|
+
* 100% editable/combinable desde el CMS: hay 4 variantes de LAYOUT
|
|
18
|
+
* (`inline`/`centrado`/`split`/`minimal`, cada una un `ChromeVariantDef`) y
|
|
19
|
+
* props COMBINABLES que aplican a cualquier layout y que el form-builder
|
|
20
|
+
* autogenera desde el schema Zod:
|
|
21
|
+
* - `logo` → texto o imagen, con tamaño configurable.
|
|
22
|
+
* - `sticky` → fijo / hide-on-scroll / shrink (`.describe("segmented")`).
|
|
23
|
+
* - `acciones` → toggles buscar / cuenta / favoritos / carrito.
|
|
24
|
+
* - `anuncio` → schema re-usado de `announcement-bar` (anidado).
|
|
25
|
+
* - `megaMenu` → panel en hover del nav (sin cambios respecto al original).
|
|
26
|
+
*
|
|
27
|
+
* El disparador de búsqueda navega a `/search?q=` (form GET nativo); la página
|
|
28
|
+
* de resultados la crea otro stream.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
type MenuItem = ChromeConfig["menu"]["items"][number]
|
|
32
|
+
|
|
33
|
+
// ── Iconos SVG inline (sin dependencias; reemplazan los glifos ⌕ ☰ ◫) ────────
|
|
34
|
+
|
|
35
|
+
type IconoProps = { size?: number; title?: string; className?: string }
|
|
36
|
+
|
|
37
|
+
function SvgIcono({ title, size = 20, className, children }: IconoProps & { children: React.ReactNode }) {
|
|
38
|
+
return (
|
|
39
|
+
<svg
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
width={size}
|
|
42
|
+
height={size}
|
|
43
|
+
fill="none"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth="1.8"
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
className={className}
|
|
49
|
+
role="img"
|
|
50
|
+
aria-label={title}
|
|
51
|
+
aria-hidden={title ? undefined : true}
|
|
52
|
+
focusable="false"
|
|
53
|
+
>
|
|
54
|
+
{title ? <title>{title}</title> : null}
|
|
55
|
+
{children}
|
|
56
|
+
</svg>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const IconoBuscar = (p: IconoProps) => (
|
|
61
|
+
<SvgIcono {...p}>
|
|
62
|
+
<circle cx="11" cy="11" r="7" />
|
|
63
|
+
<path d="M21 21l-4.3-4.3" />
|
|
64
|
+
</SvgIcono>
|
|
65
|
+
)
|
|
66
|
+
const IconoMenu = (p: IconoProps) => (
|
|
67
|
+
<SvgIcono {...p}>
|
|
68
|
+
<path d="M3 6h18M3 12h18M3 18h18" />
|
|
69
|
+
</SvgIcono>
|
|
70
|
+
)
|
|
71
|
+
const IconoBolsa = (p: IconoProps) => (
|
|
72
|
+
<SvgIcono {...p}>
|
|
73
|
+
<path d="M6 8h12l-.8 12H6.8L6 8Z" />
|
|
74
|
+
<path d="M9 8V6a3 3 0 0 1 6 0v2" />
|
|
75
|
+
</SvgIcono>
|
|
76
|
+
)
|
|
77
|
+
const IconoCuenta = (p: IconoProps) => (
|
|
78
|
+
<SvgIcono {...p}>
|
|
79
|
+
<circle cx="12" cy="8" r="3.5" />
|
|
80
|
+
<path d="M5 20a7 7 0 0 1 14 0" />
|
|
81
|
+
</SvgIcono>
|
|
82
|
+
)
|
|
83
|
+
const IconoCorazon = (p: IconoProps) => (
|
|
84
|
+
<SvgIcono {...p}>
|
|
85
|
+
<path d="M12 20s-7-4.4-7-9.3A3.7 3.7 0 0 1 12 8a3.7 3.7 0 0 1 7 2.7C19 15.6 12 20 12 20Z" />
|
|
86
|
+
</SvgIcono>
|
|
87
|
+
)
|
|
88
|
+
const IconoCerrar = (p: IconoProps) => (
|
|
89
|
+
<SvgIcono {...p}>
|
|
90
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
91
|
+
</SvgIcono>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// ── Schema (props combinables) ───────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
const logoSchema = z.object({
|
|
97
|
+
modo: z.enum(["texto", "imagen"]).describe("segmented"),
|
|
98
|
+
valor: z.string(),
|
|
99
|
+
/** Alto en px (imagen) o tamaño de fuente en px (texto). */
|
|
100
|
+
tamaño: z.number(),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const accionesSchema = z.object({
|
|
104
|
+
buscar: z.boolean(),
|
|
105
|
+
cuenta: z.boolean(),
|
|
106
|
+
favoritos: z.boolean(),
|
|
107
|
+
carrito: z.boolean(),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const schema = z.object({
|
|
111
|
+
logo: logoSchema,
|
|
112
|
+
sticky: z.enum(["fijo", "hide-on-scroll", "shrink"]).describe("segmented"),
|
|
113
|
+
acciones: accionesSchema,
|
|
114
|
+
anuncio: anuncioSchema,
|
|
115
|
+
megaMenu: megaMenuSchema,
|
|
116
|
+
})
|
|
117
|
+
export type HeaderProps = z.infer<typeof schema>
|
|
118
|
+
type LogoConfig = HeaderProps["logo"]
|
|
119
|
+
type AccionesConfig = HeaderProps["acciones"]
|
|
120
|
+
type LayoutHeader = "inline" | "centrado" | "split" | "minimal"
|
|
121
|
+
|
|
122
|
+
const defaults: HeaderProps = {
|
|
123
|
+
logo: { modo: "texto", valor: "MODA", tamaño: 24 },
|
|
124
|
+
sticky: "fijo",
|
|
125
|
+
acciones: { buscar: true, cuenta: true, favoritos: false, carrito: true },
|
|
126
|
+
anuncio: anuncioDefaults,
|
|
127
|
+
megaMenu: megaMenuDefaults,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const claseNav = "text-xs font-semibold uppercase tracking-[0.1em]"
|
|
131
|
+
const claseAccion =
|
|
132
|
+
"inline-flex items-center justify-center leading-none transition-opacity hover:opacity-70"
|
|
133
|
+
|
|
134
|
+
// ── Comportamiento sticky (fijo / hide-on-scroll / shrink) ───────────────────
|
|
135
|
+
|
|
136
|
+
function useSticky(modo: HeaderProps["sticky"]) {
|
|
137
|
+
const [oculto, setOculto] = useState(false)
|
|
138
|
+
const [compacto, setCompacto] = useState(false)
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (modo === "fijo") return
|
|
142
|
+
let ultimo = window.scrollY
|
|
143
|
+
const onScroll = () => {
|
|
144
|
+
const y = window.scrollY
|
|
145
|
+
if (modo === "hide-on-scroll") setOculto(y > 80 && y > ultimo)
|
|
146
|
+
if (modo === "shrink") setCompacto(y > 40)
|
|
147
|
+
ultimo = y
|
|
148
|
+
}
|
|
149
|
+
window.addEventListener("scroll", onScroll, { passive: true })
|
|
150
|
+
return () => window.removeEventListener("scroll", onScroll)
|
|
151
|
+
}, [modo])
|
|
152
|
+
|
|
153
|
+
return { oculto, compacto }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Piezas compartidas ───────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
/** Logo real del chrome: imagen (via ctx.componentes.Imagen) o wordmark de texto. */
|
|
159
|
+
function Marca({
|
|
160
|
+
logo,
|
|
161
|
+
ctx,
|
|
162
|
+
compacto,
|
|
163
|
+
className,
|
|
164
|
+
}: {
|
|
165
|
+
logo: LogoConfig
|
|
166
|
+
ctx: RenderContext
|
|
167
|
+
compacto?: boolean
|
|
168
|
+
className?: string
|
|
169
|
+
}) {
|
|
170
|
+
const { Imagen, Enlace } = ctx.componentes
|
|
171
|
+
const alto = Math.max(12, logo.tamaño - (compacto ? 4 : 0))
|
|
172
|
+
return (
|
|
173
|
+
<Enlace href="/" className={`inline-flex shrink-0 items-center ${className ?? ""}`}>
|
|
174
|
+
{logo.modo === "imagen" && logo.valor ? (
|
|
175
|
+
<span className="inline-block" style={{ height: `${alto}px` }}>
|
|
176
|
+
<Imagen src={logo.valor} alt="Logo" className="h-full w-auto object-contain" />
|
|
177
|
+
</span>
|
|
178
|
+
) : (
|
|
179
|
+
<span
|
|
180
|
+
className="leading-none"
|
|
181
|
+
style={{
|
|
182
|
+
fontSize: `${alto}px`,
|
|
183
|
+
fontFamily: "var(--cms-fuente-titulo)",
|
|
184
|
+
fontWeight: "var(--cms-titulo-peso,900)",
|
|
185
|
+
textTransform: "var(--cms-titulo-transform,uppercase)",
|
|
186
|
+
letterSpacing: "var(--cms-titulo-espaciado,-0.04em)",
|
|
187
|
+
color: "var(--cms-texto,#0c0c10)",
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{logo.valor || "Tienda"}
|
|
191
|
+
</span>
|
|
192
|
+
)}
|
|
193
|
+
</Enlace>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Bolsa con contador (badge via ctx.cartQuantities — back-compat). Abre el
|
|
199
|
+
* mini-cart drawer en vez de navegar a /cart; el contenido del drawer se
|
|
200
|
+
* alimenta de `ctx.carrito`.
|
|
201
|
+
*/
|
|
202
|
+
function BotonCarrito({ cantidad, onAbrir }: { cantidad: number; onAbrir: () => void }) {
|
|
203
|
+
return (
|
|
204
|
+
<button type="button" onClick={onAbrir} aria-label="Bolsa" className={`relative ${claseAccion}`}>
|
|
205
|
+
<IconoBolsa />
|
|
206
|
+
{cantidad > 0 ? (
|
|
207
|
+
<span
|
|
208
|
+
className="absolute -right-2 -top-2 inline-flex h-4 min-w-4 items-center justify-center px-1 text-[9px] font-bold leading-none"
|
|
209
|
+
style={{
|
|
210
|
+
borderRadius: "var(--cms-radio-full,999px)",
|
|
211
|
+
background: "var(--cms-acento,#3b2bff)",
|
|
212
|
+
color: "var(--cms-primario-texto,#fff)",
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{cantidad > 99 ? "99+" : cantidad}
|
|
216
|
+
</span>
|
|
217
|
+
) : null}
|
|
218
|
+
</button>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Acciones del header (toggles): buscar / favoritos / cuenta / carrito. */
|
|
223
|
+
function AccionesBar({
|
|
224
|
+
acciones,
|
|
225
|
+
ctx,
|
|
226
|
+
onBuscar,
|
|
227
|
+
onAbrirCarrito,
|
|
228
|
+
cartCount,
|
|
229
|
+
className,
|
|
230
|
+
}: {
|
|
231
|
+
acciones: AccionesConfig
|
|
232
|
+
ctx: RenderContext
|
|
233
|
+
onBuscar: () => void
|
|
234
|
+
onAbrirCarrito: () => void
|
|
235
|
+
cartCount: number
|
|
236
|
+
className?: string
|
|
237
|
+
}) {
|
|
238
|
+
const { Enlace } = ctx.componentes
|
|
239
|
+
return (
|
|
240
|
+
<div className={`flex items-center gap-3.5 ${className ?? ""}`}>
|
|
241
|
+
{acciones.buscar ? (
|
|
242
|
+
<button type="button" onClick={onBuscar} aria-label="Buscar" className={claseAccion}>
|
|
243
|
+
<IconoBuscar />
|
|
244
|
+
</button>
|
|
245
|
+
) : null}
|
|
246
|
+
{acciones.favoritos ? (
|
|
247
|
+
<Enlace href="/favoritos" className={claseAccion}>
|
|
248
|
+
<IconoCorazon title="Favoritos" />
|
|
249
|
+
</Enlace>
|
|
250
|
+
) : null}
|
|
251
|
+
{acciones.cuenta ? (
|
|
252
|
+
<Enlace href="/account" className={claseAccion}>
|
|
253
|
+
<IconoCuenta title="Cuenta" />
|
|
254
|
+
</Enlace>
|
|
255
|
+
) : null}
|
|
256
|
+
{acciones.carrito ? <BotonCarrito cantidad={cartCount} onAbrir={onAbrirCarrito} /> : null}
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Nav horizontal de escritorio + MegaMenu en hover/focus (patrón `group`). */
|
|
262
|
+
function NavDesktop({
|
|
263
|
+
items,
|
|
264
|
+
megaMenu,
|
|
265
|
+
ctx,
|
|
266
|
+
conMega = true,
|
|
267
|
+
className,
|
|
268
|
+
}: {
|
|
269
|
+
items: MenuItem[]
|
|
270
|
+
megaMenu: MegaMenuData
|
|
271
|
+
ctx: RenderContext
|
|
272
|
+
conMega?: boolean
|
|
273
|
+
className?: string
|
|
274
|
+
}) {
|
|
275
|
+
const { Enlace } = ctx.componentes
|
|
276
|
+
return (
|
|
277
|
+
<nav className={`group relative hidden items-center gap-6 md:flex ${className ?? ""}`}>
|
|
278
|
+
<div className="flex items-center gap-6">
|
|
279
|
+
{items.map((item, i) => (
|
|
280
|
+
<Enlace key={`${item.href}-${i}`} href={item.href} className={claseNav}>
|
|
281
|
+
{item.etiqueta}
|
|
282
|
+
</Enlace>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
{conMega ? (
|
|
286
|
+
<div className="absolute left-0 top-full z-20 hidden w-max min-w-[420px] pt-3 group-hover:block group-focus-within:block">
|
|
287
|
+
<MegaMenu megaMenu={megaMenu} ctx={ctx} />
|
|
288
|
+
</div>
|
|
289
|
+
) : null}
|
|
290
|
+
</nav>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Overlay de búsqueda: form GET nativo que navega a `/search?q=…`. */
|
|
295
|
+
function BuscadorOverlay({ abierto, onCerrar }: { abierto: boolean; onCerrar: () => void }) {
|
|
296
|
+
if (!abierto) return null
|
|
297
|
+
return (
|
|
298
|
+
<div
|
|
299
|
+
className="fixed inset-0 z-50 flex flex-col px-5 py-5"
|
|
300
|
+
style={{ background: "var(--cms-fondo,#f1eee6)", color: "var(--cms-texto,#0c0c10)" }}
|
|
301
|
+
>
|
|
302
|
+
<div className="flex items-center justify-end">
|
|
303
|
+
<button type="button" onClick={onCerrar} aria-label="Cerrar búsqueda" className={claseAccion}>
|
|
304
|
+
<IconoCerrar size={24} />
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
<form className="mx-auto mt-8 w-full max-w-2xl" action="/search" method="get" role="search">
|
|
308
|
+
<input
|
|
309
|
+
type="search"
|
|
310
|
+
name="q"
|
|
311
|
+
placeholder="Buscar productos"
|
|
312
|
+
aria-label="Buscar productos"
|
|
313
|
+
autoFocus
|
|
314
|
+
className="w-full border-0 border-b bg-transparent py-3 text-lg outline-none"
|
|
315
|
+
style={{ borderBottomWidth: "var(--cms-grosor-borde,1px)", borderColor: "var(--cms-borde,#dbd6c9)" }}
|
|
316
|
+
/>
|
|
317
|
+
<button type="submit" className="sr-only">
|
|
318
|
+
Buscar
|
|
319
|
+
</button>
|
|
320
|
+
</form>
|
|
321
|
+
</div>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Barra mobile común (inline/centrado/split): hamburguesa · logo · acciones. */
|
|
326
|
+
function BarraMobile({
|
|
327
|
+
props,
|
|
328
|
+
ctx,
|
|
329
|
+
onAbrirMenu,
|
|
330
|
+
onBuscar,
|
|
331
|
+
onAbrirCarrito,
|
|
332
|
+
cartCount,
|
|
333
|
+
padY,
|
|
334
|
+
}: {
|
|
335
|
+
props: HeaderProps
|
|
336
|
+
ctx: RenderContext
|
|
337
|
+
onAbrirMenu: () => void
|
|
338
|
+
onBuscar: () => void
|
|
339
|
+
onAbrirCarrito: () => void
|
|
340
|
+
cartCount: number
|
|
341
|
+
padY: string
|
|
342
|
+
}) {
|
|
343
|
+
return (
|
|
344
|
+
<div className={`flex items-center justify-between px-4 md:hidden ${padY}`}>
|
|
345
|
+
<button type="button" onClick={onAbrirMenu} aria-label="Abrir menú" className={claseAccion}>
|
|
346
|
+
<IconoMenu />
|
|
347
|
+
</button>
|
|
348
|
+
<Marca logo={props.logo} ctx={ctx} />
|
|
349
|
+
<div className="flex items-center gap-3.5">
|
|
350
|
+
{props.acciones.buscar ? (
|
|
351
|
+
<button type="button" onClick={onBuscar} aria-label="Buscar" className={claseAccion}>
|
|
352
|
+
<IconoBuscar />
|
|
353
|
+
</button>
|
|
354
|
+
) : null}
|
|
355
|
+
{props.acciones.carrito ? <BotonCarrito cantidad={cartCount} onAbrir={onAbrirCarrito} /> : null}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Layouts de escritorio ────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
function DesktopInline({
|
|
364
|
+
props,
|
|
365
|
+
ctx,
|
|
366
|
+
menu,
|
|
367
|
+
acciones,
|
|
368
|
+
padY,
|
|
369
|
+
}: {
|
|
370
|
+
props: HeaderProps
|
|
371
|
+
ctx: RenderContext
|
|
372
|
+
menu: MenuItem[]
|
|
373
|
+
acciones: React.ReactNode
|
|
374
|
+
padY: string
|
|
375
|
+
}) {
|
|
376
|
+
return (
|
|
377
|
+
<div className={`hidden items-center justify-between gap-6 px-7 md:flex ${padY}`}>
|
|
378
|
+
<Marca logo={props.logo} ctx={ctx} />
|
|
379
|
+
<NavDesktop items={menu} megaMenu={props.megaMenu} ctx={ctx} className="flex-1 justify-center" />
|
|
380
|
+
{acciones}
|
|
381
|
+
</div>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function DesktopCentrado({
|
|
386
|
+
props,
|
|
387
|
+
ctx,
|
|
388
|
+
menu,
|
|
389
|
+
acciones,
|
|
390
|
+
padY,
|
|
391
|
+
onBuscar,
|
|
392
|
+
}: {
|
|
393
|
+
props: HeaderProps
|
|
394
|
+
ctx: RenderContext
|
|
395
|
+
menu: MenuItem[]
|
|
396
|
+
acciones: React.ReactNode
|
|
397
|
+
padY: string
|
|
398
|
+
onBuscar: () => void
|
|
399
|
+
}) {
|
|
400
|
+
return (
|
|
401
|
+
<div className="hidden flex-col md:flex">
|
|
402
|
+
<div className={`grid grid-cols-3 items-center px-7 ${padY}`}>
|
|
403
|
+
<div className="flex items-center">
|
|
404
|
+
{props.acciones.buscar ? (
|
|
405
|
+
<button type="button" onClick={onBuscar} aria-label="Buscar" className={claseAccion}>
|
|
406
|
+
<IconoBuscar />
|
|
407
|
+
</button>
|
|
408
|
+
) : null}
|
|
409
|
+
</div>
|
|
410
|
+
<Marca logo={props.logo} ctx={ctx} className="mx-auto" />
|
|
411
|
+
<div className="flex justify-end">{acciones}</div>
|
|
412
|
+
</div>
|
|
413
|
+
<NavDesktop
|
|
414
|
+
items={menu}
|
|
415
|
+
megaMenu={props.megaMenu}
|
|
416
|
+
ctx={ctx}
|
|
417
|
+
className="justify-center px-7 py-3"
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function DesktopSplit({
|
|
424
|
+
props,
|
|
425
|
+
ctx,
|
|
426
|
+
menu,
|
|
427
|
+
acciones,
|
|
428
|
+
padY,
|
|
429
|
+
}: {
|
|
430
|
+
props: HeaderProps
|
|
431
|
+
ctx: RenderContext
|
|
432
|
+
menu: MenuItem[]
|
|
433
|
+
acciones: React.ReactNode
|
|
434
|
+
padY: string
|
|
435
|
+
}) {
|
|
436
|
+
const mitad = Math.ceil(menu.length / 2)
|
|
437
|
+
const izquierda = menu.slice(0, mitad)
|
|
438
|
+
const derecha = menu.slice(mitad)
|
|
439
|
+
return (
|
|
440
|
+
<div className={`hidden grid-cols-[1fr_auto_1fr] items-center px-7 md:grid ${padY}`}>
|
|
441
|
+
<NavDesktop items={izquierda} megaMenu={props.megaMenu} ctx={ctx} className="justify-start" />
|
|
442
|
+
<Marca logo={props.logo} ctx={ctx} className="mx-auto" />
|
|
443
|
+
<div className="flex items-center justify-end gap-6">
|
|
444
|
+
<NavDesktop items={derecha} megaMenu={props.megaMenu} ctx={ctx} conMega={false} className="justify-end" />
|
|
445
|
+
{acciones}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Minimal: todo en drawer. Barra única (todas las medidas): menú · logo · acciones. */
|
|
452
|
+
function BarraMinimal({
|
|
453
|
+
props,
|
|
454
|
+
ctx,
|
|
455
|
+
acciones,
|
|
456
|
+
padY,
|
|
457
|
+
onAbrirMenu,
|
|
458
|
+
}: {
|
|
459
|
+
props: HeaderProps
|
|
460
|
+
ctx: RenderContext
|
|
461
|
+
acciones: React.ReactNode
|
|
462
|
+
padY: string
|
|
463
|
+
onAbrirMenu: () => void
|
|
464
|
+
}) {
|
|
465
|
+
return (
|
|
466
|
+
<div className={`grid grid-cols-3 items-center px-4 md:px-7 ${padY}`}>
|
|
467
|
+
<div className="flex justify-start">
|
|
468
|
+
<button type="button" onClick={onAbrirMenu} aria-label="Abrir menú" className={claseAccion}>
|
|
469
|
+
<IconoMenu />
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
<Marca logo={props.logo} ctx={ctx} className="mx-auto" />
|
|
473
|
+
<div className="flex justify-end">{acciones}</div>
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ── Base ─────────────────────────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
function HeaderBase({
|
|
481
|
+
props,
|
|
482
|
+
ctx,
|
|
483
|
+
menu,
|
|
484
|
+
layout,
|
|
485
|
+
}: {
|
|
486
|
+
props: HeaderProps
|
|
487
|
+
ctx: RenderContext
|
|
488
|
+
menu: MenuItem[]
|
|
489
|
+
layout: LayoutHeader
|
|
490
|
+
}) {
|
|
491
|
+
const [drawerAbierto, setDrawerAbierto] = useState(false)
|
|
492
|
+
const [busquedaAbierta, setBusquedaAbierta] = useState(false)
|
|
493
|
+
const [carritoAbierto, setCarritoAbierto] = useState(false)
|
|
494
|
+
const { oculto, compacto } = useSticky(props.sticky)
|
|
495
|
+
|
|
496
|
+
const cartCount = Object.values(ctx.cartQuantities ?? {}).reduce((suma, n) => suma + n, 0)
|
|
497
|
+
const abrirBusqueda = () => setBusquedaAbierta(true)
|
|
498
|
+
const abrirMenu = () => setDrawerAbierto(true)
|
|
499
|
+
const abrirCarrito = () => setCarritoAbierto(true)
|
|
500
|
+
const menuDrawer = menu.map((item) => ({ texto: item.etiqueta, href: item.href }))
|
|
501
|
+
const padY = compacto ? "py-2" : "py-4"
|
|
502
|
+
|
|
503
|
+
const acciones = (
|
|
504
|
+
<AccionesBar
|
|
505
|
+
acciones={props.acciones}
|
|
506
|
+
ctx={ctx}
|
|
507
|
+
onBuscar={abrirBusqueda}
|
|
508
|
+
onAbrirCarrito={abrirCarrito}
|
|
509
|
+
cartCount={cartCount}
|
|
510
|
+
className="justify-end"
|
|
511
|
+
/>
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<>
|
|
516
|
+
<AnnouncementBar anuncio={props.anuncio} ctx={ctx} />
|
|
517
|
+
<header
|
|
518
|
+
className="sticky top-0 z-30 w-full transition-transform duration-300"
|
|
519
|
+
style={{
|
|
520
|
+
background: "var(--cms-fondo,#f1eee6)",
|
|
521
|
+
color: "var(--cms-texto,#0c0c10)",
|
|
522
|
+
borderBottom: "var(--cms-grosor-borde,1px) solid var(--cms-borde,#dbd6c9)",
|
|
523
|
+
boxShadow: compacto ? "var(--cms-sombra-md,0 10px 30px -12px rgba(0,0,0,.18))" : "none",
|
|
524
|
+
transform: oculto ? "translateY(-100%)" : "translateY(0)",
|
|
525
|
+
}}
|
|
526
|
+
>
|
|
527
|
+
{layout === "minimal" ? (
|
|
528
|
+
<BarraMinimal props={props} ctx={ctx} acciones={acciones} padY={padY} onAbrirMenu={abrirMenu} />
|
|
529
|
+
) : (
|
|
530
|
+
<>
|
|
531
|
+
{layout === "inline" ? (
|
|
532
|
+
<DesktopInline props={props} ctx={ctx} menu={menu} acciones={acciones} padY={padY} />
|
|
533
|
+
) : layout === "centrado" ? (
|
|
534
|
+
<DesktopCentrado
|
|
535
|
+
props={props}
|
|
536
|
+
ctx={ctx}
|
|
537
|
+
menu={menu}
|
|
538
|
+
acciones={acciones}
|
|
539
|
+
padY={padY}
|
|
540
|
+
onBuscar={abrirBusqueda}
|
|
541
|
+
/>
|
|
542
|
+
) : (
|
|
543
|
+
<DesktopSplit props={props} ctx={ctx} menu={menu} acciones={acciones} padY={padY} />
|
|
544
|
+
)}
|
|
545
|
+
<BarraMobile
|
|
546
|
+
props={props}
|
|
547
|
+
ctx={ctx}
|
|
548
|
+
onAbrirMenu={abrirMenu}
|
|
549
|
+
onBuscar={abrirBusqueda}
|
|
550
|
+
onAbrirCarrito={abrirCarrito}
|
|
551
|
+
cartCount={cartCount}
|
|
552
|
+
padY={padY}
|
|
553
|
+
/>
|
|
554
|
+
</>
|
|
555
|
+
)}
|
|
556
|
+
</header>
|
|
557
|
+
<DrawerMovil
|
|
558
|
+
abierto={drawerAbierto}
|
|
559
|
+
onCerrar={() => setDrawerAbierto(false)}
|
|
560
|
+
logo={props.logo}
|
|
561
|
+
menu={menuDrawer}
|
|
562
|
+
secciones={props.megaMenu.columnas}
|
|
563
|
+
ctx={ctx}
|
|
564
|
+
/>
|
|
565
|
+
<BuscadorOverlay abierto={busquedaAbierta} onCerrar={() => setBusquedaAbierta(false)} />
|
|
566
|
+
{props.acciones.carrito ? (
|
|
567
|
+
<MiniCart
|
|
568
|
+
abierto={carritoAbierto}
|
|
569
|
+
onCerrar={() => setCarritoAbierto(false)}
|
|
570
|
+
carrito={ctx.carrito}
|
|
571
|
+
ctx={ctx}
|
|
572
|
+
/>
|
|
573
|
+
) : null}
|
|
574
|
+
</>
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ── Variantes registrables ───────────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
function variante(label: string, layout: LayoutHeader): ChromeVariantDef<HeaderProps, RenderContext> {
|
|
581
|
+
return {
|
|
582
|
+
label,
|
|
583
|
+
schema,
|
|
584
|
+
defaults,
|
|
585
|
+
Component: ({ props, ctx, menu }) => <HeaderBase props={props} ctx={ctx} menu={menu} layout={layout} />,
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export const headerInline = variante("Inline", "inline")
|
|
590
|
+
export const headerCentrado = variante("Centrado", "centrado")
|
|
591
|
+
export const headerSplit = variante("Split", "split")
|
|
592
|
+
export const headerMinimal = variante("Minimal", "minimal")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineChrome } from "@arroyavecommerce/cms-core"
|
|
2
|
+
import type { RenderContext } from "../context"
|
|
3
|
+
import { headerInline, headerCentrado, headerSplit, headerMinimal } from "./header"
|
|
4
|
+
import { footerModa, footerMinimal, footerNewsletter } from "./footer"
|
|
5
|
+
|
|
6
|
+
export const chromeRegistry = defineChrome<RenderContext>()({
|
|
7
|
+
headers: {
|
|
8
|
+
inline: headerInline,
|
|
9
|
+
centrado: headerCentrado,
|
|
10
|
+
split: headerSplit,
|
|
11
|
+
minimal: headerMinimal,
|
|
12
|
+
},
|
|
13
|
+
footers: { moda: footerModa, minimal: footerMinimal, newsletter: footerNewsletter },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
headerInline,
|
|
18
|
+
headerCentrado,
|
|
19
|
+
headerSplit,
|
|
20
|
+
headerMinimal,
|
|
21
|
+
footerModa,
|
|
22
|
+
footerMinimal,
|
|
23
|
+
footerNewsletter,
|
|
24
|
+
}
|
|
25
|
+
export type { HeaderProps } from "./header"
|
|
26
|
+
export type { FooterProps, ModaProps, MinimalProps, NewsletterProps } from "./footer"
|