@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.
Files changed (99) hide show
  1. package/docs/design/README.md +22 -0
  2. package/docs/design/kinetic-street-concept.html +558 -0
  3. package/package.json +45 -0
  4. package/src/blocks/_shared.tsx +174 -0
  5. package/src/blocks/carrusel.tsx +109 -0
  6. package/src/blocks/category-tiles.test.tsx +59 -0
  7. package/src/blocks/category-tiles.tsx +70 -0
  8. package/src/blocks/editorial-banner.test.tsx +96 -0
  9. package/src/blocks/editorial-banner.tsx +205 -0
  10. package/src/blocks/faq.test.tsx +68 -0
  11. package/src/blocks/faq.tsx +122 -0
  12. package/src/blocks/featured-collection.test.tsx +80 -0
  13. package/src/blocks/featured-collection.tsx +97 -0
  14. package/src/blocks/features.test.tsx +71 -0
  15. package/src/blocks/features.tsx +159 -0
  16. package/src/blocks/hero.test.tsx +75 -0
  17. package/src/blocks/hero.tsx +290 -0
  18. package/src/blocks/index.tsx +35 -0
  19. package/src/blocks/lookbook.test.tsx +80 -0
  20. package/src/blocks/lookbook.tsx +117 -0
  21. package/src/blocks/marquee.test.tsx +54 -0
  22. package/src/blocks/marquee.tsx +114 -0
  23. package/src/blocks/newsletter.test.tsx +60 -0
  24. package/src/blocks/newsletter.tsx +105 -0
  25. package/src/blocks/product-grid.test.tsx +160 -0
  26. package/src/blocks/product-grid.tsx +76 -0
  27. package/src/blocks/promo-countdown.tsx +75 -0
  28. package/src/blocks/promo.test.tsx +71 -0
  29. package/src/blocks/promo.tsx +120 -0
  30. package/src/blocks/rich-text.test.tsx +67 -0
  31. package/src/blocks/rich-text.tsx +75 -0
  32. package/src/blocks/spacer.test.tsx +54 -0
  33. package/src/blocks/spacer.tsx +56 -0
  34. package/src/blocks/testimonials.test.tsx +74 -0
  35. package/src/blocks/testimonials.tsx +104 -0
  36. package/src/blocks/video.test.tsx +159 -0
  37. package/src/blocks/video.tsx +226 -0
  38. package/src/catalog/buy-box.test.tsx +225 -0
  39. package/src/catalog/buy-box.tsx +353 -0
  40. package/src/catalog/config.test.ts +115 -0
  41. package/src/catalog/config.ts +167 -0
  42. package/src/catalog/filter-controls.test.tsx +149 -0
  43. package/src/catalog/filter-controls.tsx +187 -0
  44. package/src/catalog/gallery.test.tsx +201 -0
  45. package/src/catalog/gallery.tsx +212 -0
  46. package/src/catalog/lightbox.test.tsx +95 -0
  47. package/src/catalog/lightbox.tsx +184 -0
  48. package/src/catalog/pdp.test.tsx +309 -0
  49. package/src/catalog/pdp.tsx +354 -0
  50. package/src/catalog/plp-arreglos.test.tsx +235 -0
  51. package/src/catalog/plp-inline.tsx +54 -0
  52. package/src/catalog/plp-overlay.tsx +129 -0
  53. package/src/catalog/plp-parts.test.tsx +146 -0
  54. package/src/catalog/plp-parts.tsx +292 -0
  55. package/src/catalog/plp-pills.tsx +44 -0
  56. package/src/catalog/plp-sidebar.tsx +47 -0
  57. package/src/catalog/plp.test.tsx +319 -0
  58. package/src/catalog/plp.tsx +259 -0
  59. package/src/catalog/product-card.test.tsx +221 -0
  60. package/src/catalog/product-card.tsx +411 -0
  61. package/src/catalog/registry.tsx +16 -0
  62. package/src/catalog/related.test.tsx +52 -0
  63. package/src/catalog/related.tsx +40 -0
  64. package/src/catalog/reviews.test.tsx +56 -0
  65. package/src/catalog/reviews.tsx +116 -0
  66. package/src/catalog/skeletons.test.tsx +61 -0
  67. package/src/catalog/skeletons.tsx +103 -0
  68. package/src/chrome/announcement-bar.test.tsx +86 -0
  69. package/src/chrome/announcement-bar.tsx +173 -0
  70. package/src/chrome/drawer-movil.test.tsx +113 -0
  71. package/src/chrome/drawer-movil.tsx +342 -0
  72. package/src/chrome/footer.test.tsx +133 -0
  73. package/src/chrome/footer.tsx +603 -0
  74. package/src/chrome/header.test.tsx +165 -0
  75. package/src/chrome/header.tsx +592 -0
  76. package/src/chrome/index.ts +26 -0
  77. package/src/chrome/mega-menu.test.tsx +161 -0
  78. package/src/chrome/mega-menu.tsx +317 -0
  79. package/src/chrome/mini-cart.test.tsx +111 -0
  80. package/src/chrome/mini-cart.tsx +320 -0
  81. package/src/content/home.ts +19 -0
  82. package/src/content/seed.test.ts +33 -0
  83. package/src/content/seed.ts +28 -0
  84. package/src/context.ts +104 -0
  85. package/src/icons/icons.test.tsx +118 -0
  86. package/src/icons/index.ts +136 -0
  87. package/src/icons/payment-icons.tsx +241 -0
  88. package/src/icons/social-icons.tsx +108 -0
  89. package/src/icons/svg-base.tsx +68 -0
  90. package/src/index.ts +49 -0
  91. package/src/puck-config.tsx +404 -0
  92. package/src/smoke.test.ts +8 -0
  93. package/src/template.test.tsx +88 -0
  94. package/src/test/mock-ctx.test.tsx +77 -0
  95. package/src/test/mock-ctx.tsx +236 -0
  96. package/src/theme-defaults.test.ts +9 -0
  97. package/src/theme.test.ts +82 -0
  98. package/src/theme.ts +195 -0
  99. 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"