@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,603 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import type { ChromeVariantDef } from "@arroyavecommerce/cms-core"
|
|
6
|
+
import type { RenderContext } from "../context"
|
|
7
|
+
import { PAYMENT_ICONS, SOCIAL_ICONS, type PaymentKey, type SocialKey } from "../icons"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Footers de la plantilla de moda — estética "Kinetic Street" (grotesca pesada,
|
|
11
|
+
* cobalto/coral, sombras y radios suaves). Todo se pinta con tokens `--cms-*`
|
|
12
|
+
* CON fallback, así el footer se ve bien aunque el tema no publique el token.
|
|
13
|
+
*
|
|
14
|
+
* 100% editable/combinable desde el CMS: cada variante es un `ChromeVariantDef`
|
|
15
|
+
* con su schema Zod, y el ChromeEditor autogenera los controles. Los campos de
|
|
16
|
+
* iconos usan la convención `.describe()` del form-builder:
|
|
17
|
+
* - `z.array(z.enum(PAYMENT_KEYS)).describe("icons:payment")` → icon-picker
|
|
18
|
+
* - `z.array({ red, href }).describe("icons:social")` → selector red
|
|
19
|
+
* - `z.enum(...).describe("segmented")` → pills
|
|
20
|
+
*
|
|
21
|
+
* A diferencia del editor, el footer SÍ importa la librería de iconos
|
|
22
|
+
* (`../icons`) porque vive dentro de `theme-kinetic`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const PAYMENT_KEYS = Object.keys(PAYMENT_ICONS) as [PaymentKey, ...PaymentKey[]]
|
|
26
|
+
const SOCIAL_KEYS = Object.keys(SOCIAL_ICONS) as [SocialKey, ...SocialKey[]]
|
|
27
|
+
|
|
28
|
+
// ── Schemas compartidos ──────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const logoSchema = z.object({
|
|
31
|
+
tipo: z.enum(["texto", "imagen"]).describe("segmented"),
|
|
32
|
+
texto: z.string(),
|
|
33
|
+
url: z.string(),
|
|
34
|
+
})
|
|
35
|
+
type LogoConfig = z.infer<typeof logoSchema>
|
|
36
|
+
|
|
37
|
+
const enlaceSchema = z.object({ texto: z.string().min(1), href: z.string().min(1) })
|
|
38
|
+
const columnaSchema = z.object({ titulo: z.string().min(1), enlaces: z.array(enlaceSchema) })
|
|
39
|
+
|
|
40
|
+
const pagosSchema = z.object({
|
|
41
|
+
metodos: z.array(z.enum(PAYMENT_KEYS)).describe("icons:payment"),
|
|
42
|
+
modo: z.enum(["icono", "texto"]).describe("segmented"),
|
|
43
|
+
color: z.enum(["marca", "tinta", "acento"]).describe("segmented"),
|
|
44
|
+
})
|
|
45
|
+
type PagosConfig = z.infer<typeof pagosSchema>
|
|
46
|
+
|
|
47
|
+
const redesSchema = z.object({
|
|
48
|
+
items: z
|
|
49
|
+
.array(z.object({ red: z.enum(SOCIAL_KEYS), href: z.string().min(1) }))
|
|
50
|
+
.describe("icons:social"),
|
|
51
|
+
modo: z.enum(["icono", "texto"]).describe("segmented"),
|
|
52
|
+
color: z.enum(["marca", "tinta", "acento"]).describe("segmented"),
|
|
53
|
+
})
|
|
54
|
+
type RedesConfig = z.infer<typeof redesSchema>
|
|
55
|
+
|
|
56
|
+
type ColorIcono = PagosConfig["color"]
|
|
57
|
+
|
|
58
|
+
// ── Utilidades de tintado (convención de `svg-base.tsx`) ─────────────────────
|
|
59
|
+
|
|
60
|
+
function claseColor(color: ColorIcono): string {
|
|
61
|
+
return color === "tinta" ? "icons--tinta" : color === "acento" ? "icons--acento" : ""
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function colorTexto(color: ColorIcono): string {
|
|
65
|
+
return color === "acento" ? "var(--cms-acento,#3b2bff)" : "var(--cms-texto,#151515)"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reglas CSS del tintado monocromo (documentadas en `svg-base.tsx`). No hay
|
|
70
|
+
* hoja global que las defina, así que el footer las emite una vez.
|
|
71
|
+
*/
|
|
72
|
+
function EstiloIconos() {
|
|
73
|
+
return (
|
|
74
|
+
<style
|
|
75
|
+
dangerouslySetInnerHTML={{
|
|
76
|
+
__html:
|
|
77
|
+
".icons--tinta [data-brand]{fill:currentColor;stroke:currentColor}" +
|
|
78
|
+
".icons--acento [data-brand]{fill:var(--cms-acento,#3b2bff);stroke:var(--cms-acento,#3b2bff)}",
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Piezas de render compartidas ─────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/** Logo real del chrome: imagen (via ctx.componentes.Imagen) o wordmark de texto. */
|
|
87
|
+
function LogoChrome({ logo, ctx }: { logo: LogoConfig; ctx: RenderContext }) {
|
|
88
|
+
const { Imagen, Enlace } = ctx.componentes
|
|
89
|
+
return (
|
|
90
|
+
<Enlace href="/" className="inline-flex items-center">
|
|
91
|
+
{logo.tipo === "imagen" && logo.url ? (
|
|
92
|
+
<Imagen src={logo.url} alt={logo.texto || "Logo"} className="h-8 w-auto object-contain" />
|
|
93
|
+
) : (
|
|
94
|
+
<span
|
|
95
|
+
className="text-[24px] leading-none"
|
|
96
|
+
style={{
|
|
97
|
+
fontFamily: "var(--cms-fuente-titulo)",
|
|
98
|
+
fontWeight: "var(--cms-titulo-peso,900)",
|
|
99
|
+
textTransform: "var(--cms-titulo-transform,uppercase)",
|
|
100
|
+
letterSpacing: "var(--cms-titulo-espaciado,-0.04em)",
|
|
101
|
+
color: "var(--cms-texto,#0c0c10)",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{logo.texto || "Tienda"}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</Enlace>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Métodos de pago: icono o texto, con tintado marca/tinta/acento. */
|
|
112
|
+
function Pagos({ pagos }: { pagos: PagosConfig }) {
|
|
113
|
+
if (!pagos.metodos.length) return null
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
className={`flex flex-wrap items-center gap-2 ${claseColor(pagos.color)}`}
|
|
117
|
+
style={{ color: colorTexto(pagos.color) }}
|
|
118
|
+
aria-label="Métodos de pago"
|
|
119
|
+
>
|
|
120
|
+
{pagos.metodos.map((clave) => {
|
|
121
|
+
const entry = PAYMENT_ICONS[clave]
|
|
122
|
+
if (!entry) return null
|
|
123
|
+
const Icono = entry.Icon
|
|
124
|
+
return (
|
|
125
|
+
<span
|
|
126
|
+
key={clave}
|
|
127
|
+
className="inline-flex items-center justify-center"
|
|
128
|
+
style={{
|
|
129
|
+
padding: pagos.modo === "icono" ? "6px 9px" : "5px 9px",
|
|
130
|
+
borderRadius: "var(--cms-radio-md,12px)",
|
|
131
|
+
border: "1px solid var(--cms-borde,#e6e4df)",
|
|
132
|
+
background: "var(--cms-superficie,#fff)",
|
|
133
|
+
boxShadow: "var(--cms-sombra-sm,0 1px 2px rgba(0,0,0,.06))",
|
|
134
|
+
fontSize: "10px",
|
|
135
|
+
lineHeight: 1,
|
|
136
|
+
...(pagos.modo === "texto"
|
|
137
|
+
? {
|
|
138
|
+
fontWeight: 600,
|
|
139
|
+
textTransform: "uppercase" as const,
|
|
140
|
+
letterSpacing: "0.04em",
|
|
141
|
+
fontFamily: "var(--cms-fuente-mono)",
|
|
142
|
+
}
|
|
143
|
+
: {}),
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
{pagos.modo === "icono" ? <Icono size={22} title={entry.label} /> : entry.label}
|
|
147
|
+
</span>
|
|
148
|
+
)
|
|
149
|
+
})}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Redes sociales: cada una enlaza (ctx.componentes.Enlace); icono o texto. */
|
|
155
|
+
function Redes({ redes, ctx }: { redes: RedesConfig; ctx: RenderContext }) {
|
|
156
|
+
if (!redes.items.length) return null
|
|
157
|
+
const { Enlace } = ctx.componentes
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
className={`flex flex-wrap items-center gap-2.5 ${claseColor(redes.color)}`}
|
|
161
|
+
style={{ color: colorTexto(redes.color) }}
|
|
162
|
+
>
|
|
163
|
+
{redes.items.map((item, i) => {
|
|
164
|
+
const entry = SOCIAL_ICONS[item.red]
|
|
165
|
+
if (!entry) return null
|
|
166
|
+
const Icono = entry.Icon
|
|
167
|
+
return (
|
|
168
|
+
<Enlace key={`${item.red}-${i}`} href={item.href} className="inline-flex items-center">
|
|
169
|
+
{redes.modo === "icono" ? (
|
|
170
|
+
<span
|
|
171
|
+
className="inline-flex h-9 w-9 items-center justify-center"
|
|
172
|
+
style={{
|
|
173
|
+
borderRadius: "var(--cms-radio-full,999px)",
|
|
174
|
+
border: "1px solid var(--cms-borde,#e6e4df)",
|
|
175
|
+
background: "var(--cms-superficie,#fff)",
|
|
176
|
+
boxShadow: "var(--cms-sombra-sm,0 1px 2px rgba(0,0,0,.06))",
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
<Icono size={18} title={entry.label} />
|
|
180
|
+
</span>
|
|
181
|
+
) : (
|
|
182
|
+
<span className="text-xs font-semibold uppercase tracking-[0.06em]">{entry.label}</span>
|
|
183
|
+
)}
|
|
184
|
+
</Enlace>
|
|
185
|
+
)
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const claseTituloColumna = "text-[11px] font-semibold uppercase tracking-[0.1em]"
|
|
192
|
+
|
|
193
|
+
function ColumnaDesktop({
|
|
194
|
+
columna,
|
|
195
|
+
ctx,
|
|
196
|
+
}: {
|
|
197
|
+
columna: z.infer<typeof columnaSchema>
|
|
198
|
+
ctx: RenderContext
|
|
199
|
+
}) {
|
|
200
|
+
const { Enlace } = ctx.componentes
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex flex-col gap-2.5 text-[12.5px]">
|
|
203
|
+
<h3 className={claseTituloColumna} style={{ color: "var(--cms-texto-suave,#6b675e)" }}>
|
|
204
|
+
{columna.titulo}
|
|
205
|
+
</h3>
|
|
206
|
+
<nav aria-label={columna.titulo}>
|
|
207
|
+
{columna.enlaces.map((enlace) => (
|
|
208
|
+
<Enlace key={enlace.href} href={enlace.href} className="block">
|
|
209
|
+
{enlace.texto}
|
|
210
|
+
</Enlace>
|
|
211
|
+
))}
|
|
212
|
+
</nav>
|
|
213
|
+
</div>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ColumnaAcordeon({
|
|
218
|
+
columna,
|
|
219
|
+
abierta,
|
|
220
|
+
onToggle,
|
|
221
|
+
ctx,
|
|
222
|
+
}: {
|
|
223
|
+
columna: z.infer<typeof columnaSchema>
|
|
224
|
+
abierta: boolean
|
|
225
|
+
onToggle: () => void
|
|
226
|
+
ctx: RenderContext
|
|
227
|
+
}) {
|
|
228
|
+
const { Enlace } = ctx.componentes
|
|
229
|
+
return (
|
|
230
|
+
<div style={{ borderBottom: "1px solid var(--cms-borde,#dbd6c9)" }}>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={onToggle}
|
|
234
|
+
aria-expanded={abierta}
|
|
235
|
+
className={`flex w-full items-center justify-between bg-transparent py-3.5 ${claseTituloColumna}`}
|
|
236
|
+
style={{ color: "var(--cms-texto,#0c0c10)" }}
|
|
237
|
+
>
|
|
238
|
+
<h3 className="m-0">{columna.titulo}</h3>
|
|
239
|
+
<span className={`transition-transform ${abierta ? "rotate-90" : ""}`}>›</span>
|
|
240
|
+
</button>
|
|
241
|
+
{abierta ? (
|
|
242
|
+
<nav
|
|
243
|
+
aria-label={columna.titulo}
|
|
244
|
+
className="flex flex-col gap-2.5 pb-3.5 text-sm"
|
|
245
|
+
style={{ color: "var(--cms-texto-suave,#6b675e)" }}
|
|
246
|
+
>
|
|
247
|
+
{columna.enlaces.map((enlace) => (
|
|
248
|
+
<Enlace key={enlace.href} href={enlace.href} className="block">
|
|
249
|
+
{enlace.texto}
|
|
250
|
+
</Enlace>
|
|
251
|
+
))}
|
|
252
|
+
</nav>
|
|
253
|
+
) : null}
|
|
254
|
+
</div>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ColumnasMobile({
|
|
259
|
+
columnas,
|
|
260
|
+
ctx,
|
|
261
|
+
}: {
|
|
262
|
+
columnas: z.infer<typeof columnaSchema>[]
|
|
263
|
+
ctx: RenderContext
|
|
264
|
+
}) {
|
|
265
|
+
const [abierta, setAbierta] = useState<string | null>(null)
|
|
266
|
+
return (
|
|
267
|
+
<div className="flex flex-col">
|
|
268
|
+
{columnas.map((columna) => (
|
|
269
|
+
<ColumnaAcordeon
|
|
270
|
+
key={columna.titulo}
|
|
271
|
+
columna={columna}
|
|
272
|
+
abierta={abierta === columna.titulo}
|
|
273
|
+
onToggle={() => setAbierta((a) => (a === columna.titulo ? null : columna.titulo))}
|
|
274
|
+
ctx={ctx}
|
|
275
|
+
/>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Barra fina de degradado kinético en el borde superior del footer. */
|
|
282
|
+
function BarraKinetic() {
|
|
283
|
+
return (
|
|
284
|
+
<div
|
|
285
|
+
aria-hidden
|
|
286
|
+
style={{
|
|
287
|
+
height: "4px",
|
|
288
|
+
background: "var(--cms-degradado,linear-gradient(120deg,#3b2bff,#ff3b2e))",
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ShellFooter({ children }: { children: React.ReactNode }) {
|
|
295
|
+
return (
|
|
296
|
+
<footer
|
|
297
|
+
className="w-full"
|
|
298
|
+
style={{
|
|
299
|
+
background: "var(--cms-superficie,#fff)",
|
|
300
|
+
color: "var(--cms-texto,#0c0c10)",
|
|
301
|
+
borderTop: "1px solid var(--cms-borde,#dbd6c9)",
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
<EstiloIconos />
|
|
305
|
+
<BarraKinetic />
|
|
306
|
+
{children}
|
|
307
|
+
</footer>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Variante: MODA (mejorada) ────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
const modaSchema = z.object({
|
|
314
|
+
logo: logoSchema,
|
|
315
|
+
tagline: z.string(),
|
|
316
|
+
columnas: z.array(columnaSchema),
|
|
317
|
+
pagos: pagosSchema,
|
|
318
|
+
redes: redesSchema,
|
|
319
|
+
legal: z.string().min(1),
|
|
320
|
+
})
|
|
321
|
+
export type ModaProps = z.infer<typeof modaSchema>
|
|
322
|
+
export type FooterProps = ModaProps
|
|
323
|
+
|
|
324
|
+
const columnasDefault: ModaProps["columnas"] = [
|
|
325
|
+
{
|
|
326
|
+
titulo: "Tienda",
|
|
327
|
+
enlaces: [
|
|
328
|
+
{ texto: "Novedades", href: "/novedades" },
|
|
329
|
+
{ texto: "Ropa", href: "/ropa" },
|
|
330
|
+
{ texto: "Zapatos", href: "/zapatos" },
|
|
331
|
+
{ texto: "Accesorios", href: "/accesorios" },
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
titulo: "Ayuda",
|
|
336
|
+
enlaces: [
|
|
337
|
+
{ texto: "Envíos y devoluciones", href: "/ayuda/envios-y-devoluciones" },
|
|
338
|
+
{ texto: "Guía de tallas", href: "/ayuda/guia-de-tallas" },
|
|
339
|
+
{ texto: "Rastrear pedido", href: "/ayuda/rastrear-pedido" },
|
|
340
|
+
{ texto: "Contacto", href: "/ayuda/contacto" },
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
titulo: "Nosotros",
|
|
345
|
+
enlaces: [
|
|
346
|
+
{ texto: "Términos y condiciones", href: "/legal/terminos" },
|
|
347
|
+
{ texto: "Política de datos", href: "/legal/politica-de-datos" },
|
|
348
|
+
{ texto: "PQRS", href: "/legal/pqrs" },
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
const pagosDefault: PagosConfig = {
|
|
354
|
+
metodos: ["visa", "mastercard", "amex", "pse", "nequi", "addi"],
|
|
355
|
+
modo: "icono",
|
|
356
|
+
color: "marca",
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const redesDefault: RedesConfig = {
|
|
360
|
+
items: [
|
|
361
|
+
{ red: "instagram", href: "https://instagram.com/moda" },
|
|
362
|
+
{ red: "tiktok", href: "https://tiktok.com/@moda" },
|
|
363
|
+
{ red: "whatsapp", href: "https://wa.me/573000000000" },
|
|
364
|
+
],
|
|
365
|
+
modo: "icono",
|
|
366
|
+
color: "marca",
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const modaDefaults: ModaProps = {
|
|
370
|
+
logo: { tipo: "texto", texto: "MODA", url: "" },
|
|
371
|
+
tagline: "Moda urbana del día a día. Envíos a toda Colombia.",
|
|
372
|
+
columnas: columnasDefault,
|
|
373
|
+
pagos: pagosDefault,
|
|
374
|
+
redes: redesDefault,
|
|
375
|
+
legal: "© 2026 Moda. Todos los derechos reservados.",
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function FooterModa({ props, ctx }: { props: ModaProps; ctx: RenderContext }) {
|
|
379
|
+
const columnas = props.columnas.length || 1
|
|
380
|
+
return (
|
|
381
|
+
<ShellFooter>
|
|
382
|
+
<div className="mx-auto max-w-6xl px-5 py-10 md:px-10 md:py-14">
|
|
383
|
+
<div className="flex flex-col gap-8 md:flex-row md:items-start md:justify-between">
|
|
384
|
+
<div className="max-w-sm">
|
|
385
|
+
<LogoChrome logo={props.logo} ctx={ctx} />
|
|
386
|
+
<p
|
|
387
|
+
className="mt-3 text-[13px] leading-relaxed"
|
|
388
|
+
style={{ color: "var(--cms-texto-suave,#6b675e)" }}
|
|
389
|
+
>
|
|
390
|
+
{props.tagline}
|
|
391
|
+
</p>
|
|
392
|
+
<div className="mt-5">
|
|
393
|
+
<Redes redes={props.redes} ctx={ctx} />
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div
|
|
397
|
+
className="hidden gap-10 md:grid"
|
|
398
|
+
style={{ gridTemplateColumns: `repeat(${columnas}, minmax(0,1fr))` }}
|
|
399
|
+
>
|
|
400
|
+
{props.columnas.map((columna) => (
|
|
401
|
+
<ColumnaDesktop key={columna.titulo} columna={columna} ctx={ctx} />
|
|
402
|
+
))}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<div className="mt-6 md:hidden">
|
|
407
|
+
<ColumnasMobile columnas={props.columnas} ctx={ctx} />
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<div
|
|
411
|
+
className="mt-10 flex flex-col gap-5 pt-6 md:flex-row md:items-center md:justify-between"
|
|
412
|
+
style={{ borderTop: "1px solid var(--cms-borde,#dbd6c9)" }}
|
|
413
|
+
>
|
|
414
|
+
<span className="text-[11.5px]" style={{ color: "var(--cms-texto-suave,#6b675e)" }}>
|
|
415
|
+
{props.legal}
|
|
416
|
+
</span>
|
|
417
|
+
<Pagos pagos={props.pagos} />
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</ShellFooter>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export const footerModa: ChromeVariantDef<ModaProps, RenderContext> = {
|
|
425
|
+
label: "Moda",
|
|
426
|
+
schema: modaSchema,
|
|
427
|
+
defaults: modaDefaults,
|
|
428
|
+
Component: ({ props, ctx }) => <FooterModa props={props} ctx={ctx} />,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Variante: MINIMAL ────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
const minimalSchema = z.object({
|
|
434
|
+
logo: logoSchema,
|
|
435
|
+
enlaces: z.array(enlaceSchema),
|
|
436
|
+
redes: redesSchema,
|
|
437
|
+
legal: z.string().min(1),
|
|
438
|
+
})
|
|
439
|
+
export type MinimalProps = z.infer<typeof minimalSchema>
|
|
440
|
+
|
|
441
|
+
const minimalDefaults: MinimalProps = {
|
|
442
|
+
logo: { tipo: "texto", texto: "MODA", url: "" },
|
|
443
|
+
enlaces: [
|
|
444
|
+
{ texto: "Tienda", href: "/novedades" },
|
|
445
|
+
{ texto: "Ayuda", href: "/ayuda/contacto" },
|
|
446
|
+
{ texto: "Términos", href: "/legal/terminos" },
|
|
447
|
+
{ texto: "Privacidad", href: "/legal/politica-de-datos" },
|
|
448
|
+
],
|
|
449
|
+
redes: { ...redesDefault, modo: "texto" },
|
|
450
|
+
legal: "© 2026 Moda. Todos los derechos reservados.",
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function FooterMinimal({ props, ctx }: { props: MinimalProps; ctx: RenderContext }) {
|
|
454
|
+
const { Enlace } = ctx.componentes
|
|
455
|
+
return (
|
|
456
|
+
<ShellFooter>
|
|
457
|
+
<div className="mx-auto max-w-6xl px-5 py-8 md:px-10">
|
|
458
|
+
<div className="flex flex-col items-center gap-5 text-center md:flex-row md:justify-between md:text-left">
|
|
459
|
+
<LogoChrome logo={props.logo} ctx={ctx} />
|
|
460
|
+
<nav className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
|
|
461
|
+
{props.enlaces.map((enlace) => (
|
|
462
|
+
<Enlace
|
|
463
|
+
key={enlace.href}
|
|
464
|
+
href={enlace.href}
|
|
465
|
+
className="text-xs font-semibold uppercase tracking-[0.08em]"
|
|
466
|
+
>
|
|
467
|
+
{enlace.texto}
|
|
468
|
+
</Enlace>
|
|
469
|
+
))}
|
|
470
|
+
</nav>
|
|
471
|
+
<Redes redes={props.redes} ctx={ctx} />
|
|
472
|
+
</div>
|
|
473
|
+
<p
|
|
474
|
+
className="mt-6 text-center text-[11.5px] md:text-left"
|
|
475
|
+
style={{ color: "var(--cms-texto-suave,#6b675e)" }}
|
|
476
|
+
>
|
|
477
|
+
{props.legal}
|
|
478
|
+
</p>
|
|
479
|
+
</div>
|
|
480
|
+
</ShellFooter>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export const footerMinimal: ChromeVariantDef<MinimalProps, RenderContext> = {
|
|
485
|
+
label: "Minimal",
|
|
486
|
+
schema: minimalSchema,
|
|
487
|
+
defaults: minimalDefaults,
|
|
488
|
+
Component: ({ props, ctx }) => <FooterMinimal props={props} ctx={ctx} />,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ── Variante: NEWSLETTER ─────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
const newsletterSchema = z.object({
|
|
494
|
+
logo: logoSchema,
|
|
495
|
+
boletin: z.object({
|
|
496
|
+
titulo: z.string().min(1),
|
|
497
|
+
texto: z.string(),
|
|
498
|
+
placeholder: z.string(),
|
|
499
|
+
boton: z.string().min(1),
|
|
500
|
+
nota: z.string(),
|
|
501
|
+
}),
|
|
502
|
+
redes: redesSchema,
|
|
503
|
+
pagos: pagosSchema,
|
|
504
|
+
legal: z.string().min(1),
|
|
505
|
+
})
|
|
506
|
+
export type NewsletterProps = z.infer<typeof newsletterSchema>
|
|
507
|
+
|
|
508
|
+
const newsletterDefaults: NewsletterProps = {
|
|
509
|
+
logo: { tipo: "texto", texto: "MODA", url: "" },
|
|
510
|
+
boletin: {
|
|
511
|
+
titulo: "Sumate al drop",
|
|
512
|
+
texto: "Novedades, lanzamientos y ofertas antes que nadie. Sin spam.",
|
|
513
|
+
placeholder: "tu@correo.com",
|
|
514
|
+
boton: "Suscribirme",
|
|
515
|
+
nota: "Al suscribirte aceptás nuestra política de datos.",
|
|
516
|
+
},
|
|
517
|
+
redes: redesDefault,
|
|
518
|
+
pagos: pagosDefault,
|
|
519
|
+
legal: "© 2026 Moda. Todos los derechos reservados.",
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function FooterNewsletter({ props, ctx }: { props: NewsletterProps; ctx: RenderContext }) {
|
|
523
|
+
// Envío = placeholder (sin backend en esta fase): solo evita el submit real.
|
|
524
|
+
return (
|
|
525
|
+
<ShellFooter>
|
|
526
|
+
<div className="mx-auto grid max-w-6xl gap-8 px-5 py-10 md:grid-cols-2 md:px-10 md:py-14">
|
|
527
|
+
<div>
|
|
528
|
+
<LogoChrome logo={props.logo} ctx={ctx} />
|
|
529
|
+
<h2
|
|
530
|
+
className="mt-4 text-[26px] leading-[1.05]"
|
|
531
|
+
style={{
|
|
532
|
+
fontFamily: "var(--cms-fuente-titulo)",
|
|
533
|
+
fontWeight: "var(--cms-titulo-peso,900)",
|
|
534
|
+
textTransform: "var(--cms-titulo-transform,uppercase)",
|
|
535
|
+
letterSpacing: "var(--cms-titulo-espaciado,-0.04em)",
|
|
536
|
+
}}
|
|
537
|
+
>
|
|
538
|
+
{props.boletin.titulo}
|
|
539
|
+
</h2>
|
|
540
|
+
<p
|
|
541
|
+
className="mt-2 max-w-md text-[13px] leading-relaxed"
|
|
542
|
+
style={{ color: "var(--cms-texto-suave,#6b675e)" }}
|
|
543
|
+
>
|
|
544
|
+
{props.boletin.texto}
|
|
545
|
+
</p>
|
|
546
|
+
<form
|
|
547
|
+
className="mt-5 flex max-w-md flex-col gap-2 sm:flex-row"
|
|
548
|
+
onSubmit={(evento) => evento.preventDefault()}
|
|
549
|
+
>
|
|
550
|
+
<input
|
|
551
|
+
type="email"
|
|
552
|
+
placeholder={props.boletin.placeholder}
|
|
553
|
+
aria-label="Correo electrónico"
|
|
554
|
+
className="min-w-0 flex-1 px-3.5 py-2.5 text-sm outline-none"
|
|
555
|
+
style={{
|
|
556
|
+
borderRadius: "var(--cms-radio-md,12px)",
|
|
557
|
+
border: "1px solid var(--cms-borde,#dbd6c9)",
|
|
558
|
+
background: "var(--cms-fondo,#f1eee6)",
|
|
559
|
+
color: "var(--cms-texto,#0c0c10)",
|
|
560
|
+
}}
|
|
561
|
+
/>
|
|
562
|
+
<button
|
|
563
|
+
type="submit"
|
|
564
|
+
className="px-5 py-2.5 text-sm font-semibold"
|
|
565
|
+
style={{
|
|
566
|
+
borderRadius: "var(--cms-radio-md,12px)",
|
|
567
|
+
background: "var(--cms-acento,#3b2bff)",
|
|
568
|
+
color: "var(--cms-primario-texto,#fff)",
|
|
569
|
+
boxShadow: "var(--cms-sombra-md,0 10px 30px -12px rgba(0,0,0,.18))",
|
|
570
|
+
}}
|
|
571
|
+
>
|
|
572
|
+
{props.boletin.boton}
|
|
573
|
+
</button>
|
|
574
|
+
</form>
|
|
575
|
+
<p className="mt-2 text-[11px]" style={{ color: "var(--cms-texto-suave,#6b675e)" }}>
|
|
576
|
+
{props.boletin.nota}
|
|
577
|
+
</p>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div className="flex flex-col gap-6 md:items-end">
|
|
581
|
+
<Redes redes={props.redes} ctx={ctx} />
|
|
582
|
+
<Pagos pagos={props.pagos} />
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div
|
|
587
|
+
className="mx-auto max-w-6xl px-5 py-4 md:px-10"
|
|
588
|
+
style={{ borderTop: "1px solid var(--cms-borde,#dbd6c9)" }}
|
|
589
|
+
>
|
|
590
|
+
<span className="text-[11.5px]" style={{ color: "var(--cms-texto-suave,#6b675e)" }}>
|
|
591
|
+
{props.legal}
|
|
592
|
+
</span>
|
|
593
|
+
</div>
|
|
594
|
+
</ShellFooter>
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export const footerNewsletter: ChromeVariantDef<NewsletterProps, RenderContext> = {
|
|
599
|
+
label: "Newsletter",
|
|
600
|
+
schema: newsletterSchema,
|
|
601
|
+
defaults: newsletterDefaults,
|
|
602
|
+
Component: ({ props, ctx }) => <FooterNewsletter props={props} ctx={ctx} />,
|
|
603
|
+
}
|