@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,292 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import type { RenderContext } from "../context"
|
|
5
|
+
|
|
6
|
+
type ListadoPlp = NonNullable<NonNullable<RenderContext["catalogo"]>["listado"]>
|
|
7
|
+
|
|
8
|
+
/** <nav aria-label="Ruta"> con enlaces via ctx.componentes.Enlace; el ultimo item queda como texto plano. */
|
|
9
|
+
export function Breadcrumb({
|
|
10
|
+
items,
|
|
11
|
+
ctx,
|
|
12
|
+
}: {
|
|
13
|
+
items: { texto: string; href: string }[]
|
|
14
|
+
ctx: RenderContext
|
|
15
|
+
}) {
|
|
16
|
+
const { Enlace } = ctx.componentes
|
|
17
|
+
return (
|
|
18
|
+
<nav
|
|
19
|
+
aria-label="Ruta"
|
|
20
|
+
className="flex flex-wrap items-center gap-1.5 text-[11px] font-medium"
|
|
21
|
+
style={{ fontFamily: "var(--cms-fuente-mono)", color: "var(--cms-texto-suave)" }}
|
|
22
|
+
>
|
|
23
|
+
{items.map((item, index) => {
|
|
24
|
+
const esUltimo = index === items.length - 1
|
|
25
|
+
return (
|
|
26
|
+
<span key={item.href} className="flex items-center gap-1.5">
|
|
27
|
+
{esUltimo ? (
|
|
28
|
+
<span style={{ color: "var(--cms-texto)" }}>{item.texto}</span>
|
|
29
|
+
) : (
|
|
30
|
+
<Enlace href={item.href} className="[color:inherit] [text-decoration:none]">
|
|
31
|
+
{item.texto}
|
|
32
|
+
</Enlace>
|
|
33
|
+
)}
|
|
34
|
+
{esUltimo ? null : <span aria-hidden="true">/</span>}
|
|
35
|
+
</span>
|
|
36
|
+
)
|
|
37
|
+
})}
|
|
38
|
+
</nav>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Opciones de orden fijas del template (mockup PLP l.37: "Ordenar: Relevancia ▾"). */
|
|
43
|
+
const OPCIONES_ORDEN: { valor: string; etiqueta: string }[] = [
|
|
44
|
+
{ valor: "relevancia", etiqueta: "Relevancia" },
|
|
45
|
+
{ valor: "novedades", etiqueta: "Novedades" },
|
|
46
|
+
{ valor: "precio-asc", etiqueta: "Precio: menor a mayor" },
|
|
47
|
+
{ valor: "precio-desc", etiqueta: "Precio: mayor a menor" },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
function etiquetaOrden(valor: string): string {
|
|
51
|
+
return OPCIONES_ORDEN.find((opcion) => opcion.valor === valor)?.etiqueta ?? valor
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Boton "Ordenar: <actual> ▾" que abre un panel de opciones (client) que navega por listado.urlOrden. */
|
|
55
|
+
function DropdownOrden({ listado, ctx }: { listado: ListadoPlp; ctx: RenderContext }) {
|
|
56
|
+
const [abierto, setAbierto] = useState(false)
|
|
57
|
+
const { Enlace } = ctx.componentes
|
|
58
|
+
return (
|
|
59
|
+
<div className="relative inline-block text-[12px]">
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => setAbierto((valor) => !valor)}
|
|
63
|
+
aria-expanded={abierto}
|
|
64
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 font-medium [background:var(--cms-superficie)] [border:var(--cms-grosor-borde)_solid_var(--cms-borde)] [border-radius:var(--cms-radio)] [color:var(--cms-texto)]"
|
|
65
|
+
style={{ fontFamily: "var(--cms-fuente-texto)" }}
|
|
66
|
+
>
|
|
67
|
+
Ordenar: {etiquetaOrden(listado.orden)} ▾
|
|
68
|
+
</button>
|
|
69
|
+
{abierto ? (
|
|
70
|
+
<ul className="absolute right-0 top-full z-10 m-0 mt-1.5 flex min-w-[190px] list-none flex-col gap-1 p-2 text-[12.5px] [background:var(--cms-superficie)] [border:var(--cms-grosor-borde)_solid_var(--cms-borde)] [border-radius:var(--cms-radio)]">
|
|
71
|
+
{OPCIONES_ORDEN.map((opcion) => {
|
|
72
|
+
const activa = opcion.valor === listado.orden
|
|
73
|
+
return (
|
|
74
|
+
<li key={opcion.valor}>
|
|
75
|
+
<Enlace
|
|
76
|
+
href={listado.urlOrden(opcion.valor)}
|
|
77
|
+
aria-current={activa ? "true" : undefined}
|
|
78
|
+
className={`inline-block w-full [text-decoration:none] ${
|
|
79
|
+
activa ? "font-semibold [color:var(--cms-texto)]" : "font-normal [color:var(--cms-texto-suave)]"
|
|
80
|
+
}`}
|
|
81
|
+
>
|
|
82
|
+
{opcion.etiqueta}
|
|
83
|
+
</Enlace>
|
|
84
|
+
</li>
|
|
85
|
+
)
|
|
86
|
+
})}
|
|
87
|
+
</ul>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Toggle de vista ▦/☰: indicador presentacional. La FUENTE DE VERDAD del layout
|
|
95
|
+
* es `config.vista` (resuelto server-side y aplicado por `FrameProductos`); este
|
|
96
|
+
* toggle solo refleja ese valor inicial (`vistaActual`) y da feedback local. No
|
|
97
|
+
* re-renderiza la grilla ni cambia la URL.
|
|
98
|
+
*/
|
|
99
|
+
function ToggleVista({ vistaActual = "grilla" }: { vistaActual?: "grilla" | "lista" }) {
|
|
100
|
+
const [vista, setVista] = useState<"grilla" | "lista">(vistaActual)
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
className="flex items-center gap-0.5 px-2 py-1.5"
|
|
104
|
+
style={{ border: "var(--cms-grosor-borde) solid var(--cms-borde)", borderRadius: "var(--cms-radio)" }}
|
|
105
|
+
>
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => setVista("grilla")}
|
|
109
|
+
aria-pressed={vista === "grilla"}
|
|
110
|
+
aria-label="Vista en cuadricula"
|
|
111
|
+
className="px-1 text-[13px]"
|
|
112
|
+
style={{ fontWeight: vista === "grilla" ? 700 : 400, color: vista === "grilla" ? "var(--cms-texto)" : "var(--cms-texto-suave)" }}
|
|
113
|
+
>
|
|
114
|
+
▦
|
|
115
|
+
</button>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => setVista("lista")}
|
|
119
|
+
aria-pressed={vista === "lista"}
|
|
120
|
+
aria-label="Vista en lista"
|
|
121
|
+
className="px-1 text-[13px]"
|
|
122
|
+
style={{ fontWeight: vista === "lista" ? 700 : 400, color: vista === "lista" ? "var(--cms-texto)" : "var(--cms-texto-suave)" }}
|
|
123
|
+
>
|
|
124
|
+
☰
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Contador de productos + "Ordenar ▾" + toggle de vista (mockup PLP l.35-39). */
|
|
131
|
+
export function ToolbarPlp({
|
|
132
|
+
listado,
|
|
133
|
+
ctx,
|
|
134
|
+
mostrarContador = true,
|
|
135
|
+
vista = "grilla",
|
|
136
|
+
}: {
|
|
137
|
+
listado: ListadoPlp
|
|
138
|
+
ctx: RenderContext
|
|
139
|
+
mostrarContador?: boolean
|
|
140
|
+
vista?: "grilla" | "lista"
|
|
141
|
+
}) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex items-center justify-end gap-3.5">
|
|
144
|
+
{mostrarContador ? (
|
|
145
|
+
<span className="text-[12px]" style={{ color: "var(--cms-texto-suave)" }}>
|
|
146
|
+
{listado.total} productos
|
|
147
|
+
</span>
|
|
148
|
+
) : null}
|
|
149
|
+
<DropdownOrden listado={listado} ctx={ctx} />
|
|
150
|
+
<ToggleVista vistaActual={vista} />
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function etiquetaFiltro(listado: ListadoPlp, facetId: string, valor: string): string {
|
|
156
|
+
return listado.opcionesFaceta[facetId]?.find((opcion) => opcion.valor === valor)?.etiqueta ?? valor
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Un chip por cada (facetId, valor) en listado.seleccion, con "×" que navega a
|
|
161
|
+
* urlFiltro(facetId, valor) (togglea = quita) + "Limpiar todo". `null` si no hay seleccion.
|
|
162
|
+
*
|
|
163
|
+
* "Limpiar todo": en 4d-i (aislamiento) navega a "?" (ruta sin query). La cascara real
|
|
164
|
+
* (sub-proyecto 5) es quien conoce la URL canonica de la pagina y debe reemplazar este
|
|
165
|
+
* href por la ruta sin ningun parametro de faceta.
|
|
166
|
+
*/
|
|
167
|
+
export function PillsActivas({ listado, ctx }: { listado: ListadoPlp; ctx: RenderContext }) {
|
|
168
|
+
const entradas = Object.entries(listado.seleccion).flatMap(([facetId, valores]) =>
|
|
169
|
+
valores.map((valor) => ({ facetId, valor }))
|
|
170
|
+
)
|
|
171
|
+
if (entradas.length === 0) return null
|
|
172
|
+
|
|
173
|
+
const { Enlace } = ctx.componentes
|
|
174
|
+
return (
|
|
175
|
+
<ul role="list" className="m-0 flex flex-wrap items-center gap-2 p-0">
|
|
176
|
+
{entradas.map(({ facetId, valor }) => {
|
|
177
|
+
const etiqueta = etiquetaFiltro(listado, facetId, valor)
|
|
178
|
+
return (
|
|
179
|
+
<li key={`${facetId}:${valor}`}>
|
|
180
|
+
<Enlace
|
|
181
|
+
href={listado.urlFiltro(facetId, valor)}
|
|
182
|
+
className="inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-[11.5px] [background:var(--cms-superficie)] [border:var(--cms-grosor-borde)_solid_var(--cms-borde)] [color:var(--cms-texto)] [text-decoration:none]"
|
|
183
|
+
>
|
|
184
|
+
<span aria-hidden="true">{etiqueta}</span>
|
|
185
|
+
<b aria-hidden="true">×</b>
|
|
186
|
+
<span className="sr-only">Quitar filtro {etiqueta}</span>
|
|
187
|
+
</Enlace>
|
|
188
|
+
</li>
|
|
189
|
+
)
|
|
190
|
+
})}
|
|
191
|
+
<li>
|
|
192
|
+
<Enlace
|
|
193
|
+
href="?"
|
|
194
|
+
className="inline-block px-1.5 py-1.5 text-[11.5px] underline [color:var(--cms-texto-suave)] [text-decoration:underline]"
|
|
195
|
+
>
|
|
196
|
+
Limpiar todo
|
|
197
|
+
</Enlace>
|
|
198
|
+
</li>
|
|
199
|
+
</ul>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Construye la secuencia de paginas a mostrar, con "…" si totalPaginas > 7. */
|
|
204
|
+
function rangoPaginas(pagina: number, totalPaginas: number): (number | "…")[] {
|
|
205
|
+
if (totalPaginas <= 7) {
|
|
206
|
+
return Array.from({ length: totalPaginas }, (_, indice) => indice + 1)
|
|
207
|
+
}
|
|
208
|
+
const candidatas = new Set<number>([1, totalPaginas, pagina - 1, pagina, pagina + 1])
|
|
209
|
+
const ordenadas = [...candidatas].filter((p) => p >= 1 && p <= totalPaginas).sort((a, b) => a - b)
|
|
210
|
+
|
|
211
|
+
const resultado: (number | "…")[] = []
|
|
212
|
+
let anterior = 0
|
|
213
|
+
for (const p of ordenadas) {
|
|
214
|
+
if (anterior !== 0 && p - anterior > 1) resultado.push("…")
|
|
215
|
+
resultado.push(p)
|
|
216
|
+
anterior = p
|
|
217
|
+
}
|
|
218
|
+
return resultado
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** <nav aria-label="Paginación"> numerada con hrefPara + aria-current="page" en la actual; elipsis si >7. */
|
|
222
|
+
function PaginacionNumerada({ listado, ctx }: { listado: ListadoPlp; ctx: RenderContext }) {
|
|
223
|
+
const { Enlace } = ctx.componentes
|
|
224
|
+
const { pagina, totalPaginas, hrefPara } = listado.paginacion
|
|
225
|
+
const paginas = rangoPaginas(pagina, totalPaginas)
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<nav
|
|
229
|
+
aria-label="Paginación"
|
|
230
|
+
className="flex items-center justify-center gap-1.5 text-[12px] font-medium"
|
|
231
|
+
style={{ fontFamily: "var(--cms-fuente-mono)" }}
|
|
232
|
+
>
|
|
233
|
+
{paginas.map((p, indice) =>
|
|
234
|
+
p === "…" ? (
|
|
235
|
+
<span
|
|
236
|
+
key={`elipsis-${indice}`}
|
|
237
|
+
aria-hidden="true"
|
|
238
|
+
className="flex h-[34px] w-[34px] items-center justify-center"
|
|
239
|
+
style={{ color: "var(--cms-texto-suave)" }}
|
|
240
|
+
>
|
|
241
|
+
…
|
|
242
|
+
</span>
|
|
243
|
+
) : (
|
|
244
|
+
<Enlace
|
|
245
|
+
key={p}
|
|
246
|
+
href={hrefPara(p)}
|
|
247
|
+
aria-current={p === pagina ? "page" : undefined}
|
|
248
|
+
className={`flex h-[34px] w-[34px] items-center justify-center [text-decoration:none] ${
|
|
249
|
+
p === pagina
|
|
250
|
+
? "[background:var(--cms-primario)] [color:var(--cms-primario-texto)]"
|
|
251
|
+
: "[border:var(--cms-grosor-borde)_solid_var(--cms-borde)] [color:var(--cms-texto)]"
|
|
252
|
+
}`}
|
|
253
|
+
>
|
|
254
|
+
{p}
|
|
255
|
+
</Enlace>
|
|
256
|
+
)
|
|
257
|
+
)}
|
|
258
|
+
</nav>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Boton "Cargar más" presentacional (stub en aislamiento; la cascara conecta la carga real). */
|
|
263
|
+
function BotonCargarMas() {
|
|
264
|
+
return (
|
|
265
|
+
<button
|
|
266
|
+
type="button"
|
|
267
|
+
className="mx-auto block px-6 py-2.5 text-[11.5px] font-semibold uppercase tracking-[0.08em]"
|
|
268
|
+
style={{
|
|
269
|
+
background: "var(--cms-primario)",
|
|
270
|
+
color: "var(--cms-primario-texto)",
|
|
271
|
+
borderRadius: "var(--cms-radio)",
|
|
272
|
+
fontFamily: "var(--cms-fuente-texto)",
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
Cargar más
|
|
276
|
+
</button>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** modo "numerada" (default): nav numerada por hrefPara. modo "load-more": boton client presentacional. */
|
|
281
|
+
export function Paginacion({
|
|
282
|
+
listado,
|
|
283
|
+
ctx,
|
|
284
|
+
modo = "numerada",
|
|
285
|
+
}: {
|
|
286
|
+
listado: ListadoPlp
|
|
287
|
+
ctx: RenderContext
|
|
288
|
+
modo?: "numerada" | "load-more"
|
|
289
|
+
}) {
|
|
290
|
+
if (modo === "load-more") return <BotonCargarMas />
|
|
291
|
+
return <PaginacionNumerada listado={listado} ctx={ctx} />
|
|
292
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import type { RenderContext } from "../context"
|
|
3
|
+
import { ControlDeFiltro } from "./filter-controls"
|
|
4
|
+
import { facetsValidas } from "./plp"
|
|
5
|
+
|
|
6
|
+
type ListadoPlp = NonNullable<NonNullable<RenderContext["catalogo"]>["listado"]>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Arreglo desktop "pills" (mockup PLP l.107-132): barra horizontal de
|
|
10
|
+
* chips/dropdowns (uno por faceta válida, via ControlDeFiltro) sobre
|
|
11
|
+
* `children` (la grilla, provista por `PlpLayout`) a todo el ancho. Solo la
|
|
12
|
+
* barra de chips es desktop-only (`hidden md:block`): el filtrado mobile va
|
|
13
|
+
* por el bottom-sheet. `children` NO queda dentro de ningún contenedor `hidden`.
|
|
14
|
+
*/
|
|
15
|
+
export function ArregloPills({
|
|
16
|
+
listado,
|
|
17
|
+
ctx,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
listado: ListadoPlp
|
|
21
|
+
ctx: RenderContext
|
|
22
|
+
children: ReactNode
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<div data-arreglo="pills" className="flex flex-col gap-4">
|
|
26
|
+
<div
|
|
27
|
+
className="hidden flex-wrap items-center gap-2 py-3 [border-top:var(--cms-grosor-borde)_solid_var(--cms-borde)] [border-bottom:var(--cms-grosor-borde)_solid_var(--cms-borde)] md:flex"
|
|
28
|
+
>
|
|
29
|
+
{facetsValidas(listado).map((facet) => (
|
|
30
|
+
<div key={facet.id} data-facet={facet.id}>
|
|
31
|
+
<ControlDeFiltro
|
|
32
|
+
facet={facet}
|
|
33
|
+
opciones={listado.opcionesFaceta[facet.id] ?? []}
|
|
34
|
+
seleccionados={listado.seleccion[facet.id] ?? []}
|
|
35
|
+
urlFiltro={listado.urlFiltro}
|
|
36
|
+
ctx={ctx}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import type { RenderContext } from "../context"
|
|
3
|
+
import { ControlDeFiltro } from "./filter-controls"
|
|
4
|
+
import { facetsValidas } from "./plp"
|
|
5
|
+
|
|
6
|
+
type ListadoPlp = NonNullable<NonNullable<RenderContext["catalogo"]>["listado"]>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Arreglo desktop "sidebar" (mockup PLP l.41-105): aside 240px a la izquierda
|
|
10
|
+
* con una sección por faceta válida + `children` (la grilla, provista por
|
|
11
|
+
* `PlpLayout`) a la derecha. Solo el `<aside>` es desktop-only
|
|
12
|
+
* (`hidden md:flex`): el filtrado mobile va por el bottom-sheet. `children`
|
|
13
|
+
* NO queda dentro de ningún contenedor `hidden`, así la grilla se muestra
|
|
14
|
+
* full-width en mobile.
|
|
15
|
+
*/
|
|
16
|
+
export function ArregloSidebar({
|
|
17
|
+
listado,
|
|
18
|
+
ctx,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
listado: ListadoPlp
|
|
22
|
+
ctx: RenderContext
|
|
23
|
+
children: ReactNode
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<div data-arreglo="sidebar" className="flex flex-col gap-7 md:grid md:grid-cols-[240px_1fr]">
|
|
27
|
+
<aside className="hidden flex-col gap-5 text-[13px] md:flex">
|
|
28
|
+
{facetsValidas(listado).map((facet, indice) => (
|
|
29
|
+
<div
|
|
30
|
+
key={facet.id}
|
|
31
|
+
data-facet={facet.id}
|
|
32
|
+
className={indice === 0 ? "" : "[border-top:var(--cms-grosor-borde)_solid_var(--cms-borde)] pt-4"}
|
|
33
|
+
>
|
|
34
|
+
<ControlDeFiltro
|
|
35
|
+
facet={facet}
|
|
36
|
+
opciones={listado.opcionesFaceta[facet.id] ?? []}
|
|
37
|
+
seleccionados={listado.seleccion[facet.id] ?? []}
|
|
38
|
+
urlFiltro={listado.urlFiltro}
|
|
39
|
+
ctx={ctx}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</aside>
|
|
44
|
+
{children}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|