The Shadow #1
Primer número de la serie noir. Nueva edición restaurada con arte original.
Grid Workshop
Cards de producto en fila scrolleable con scroll-snap, flechas, dots de posición y navegación por teclado. Los indicadores muestran el grupo visible.
Primer número de la serie noir. Nueva edición restaurada con arte original.
Edición de colección con portada alternativa y extras de making of.
Tapa dura con sobrecubierta impresa y guardas con arte inédito.
Pack con el libro y una lámina firmada numerada de edición limitada.
Número con argumento y guion de la edición original de los años 40.
Caja recopilatoria de la saga completa con índice ilustrado.
Edición bilingüe con ensayo crítico y cronología de la serie.
Último número de la serie, con epílogo y galería de portadas.
.scroll__track con overflow-x:auto y scroll-snap-type:x mandatory; cada card scroll-snap-align:start.
initDots() genera un button.dot por grupo visible (items que entran en el track). Cada dot salta con track.scrollTo({ left: i*step*visible }).
scroll actualiza el dot activo (scrollLeft / (step*visible)), tanto al usar flechas como teclado como clics.
aria-label="Ir al grupo N de M" y el grupo .dots tiene role="group".
<div class="dots" data-group="tu-grupo" role="group" aria-label="Posición"></div> después del .scroll.
.products .scroll__row { display:grid; grid-auto-flow:column; grid-auto-columns:280px; gap:12px; }
initScroll('products') detecta el .dots[data-group] adyacente y arma los dots automáticamente.
min-width:0 —
en los contenedores flex/grid que envuelven al track; si no, el scroll interno no se activa y los dots no aparecen.
Snippets autocontenidos: copiá cada bloque y pegá en tu proyecto. Los dots se generan por JS según cuántos items entran en el track.
<div class="products">
<h2>Destacados</h2>
<div class="scroll scroll--gutter" data-group="products">
<button class="arrow arrow--left" aria-label="Anteriores">◀</button>
<div class="scroll__track" role="list" tabindex="0">
<div class="scroll__row">
<article class="product-card" role="listitem">
<div class="product-card__image"><img src="001.jpg" alt="Portada"></div>
<div class="product-card__body">
<h3>The Shadow #1</h3>
<span class="product-card__price">$12.990</span>
<button class="product-card__btn">Agregar al carrito</button>
</div>
</article>
<!-- mas items… -->
</div>
</div>
<button class="arrow arrow--right" aria-label="Siguientes">▶</button>
</div>
<div class="dots" data-group="products" role="group" aria-label="Posición"></div>
</div>
:root{
--surface:hsl(215,11%,3%);
--border:hsl(214,35%,22%);
--text:hsl(215,25%,85%);
--accent:hsl(22,100%,62%);
}
.products{
display:flex;
flex-direction:column;
gap:0.75rem;
min-width:0;
}
.scroll{
position:relative;
}
.scroll__track{
overflow-x:auto;
scroll-snap-type:x mandatory;
padding:4px 0 8px;
}
.scroll__row{
display:grid;
grid-auto-flow:column;
grid-auto-columns:280px;
gap:12px;
}
.product-card{
scroll-snap-align:start;
}
.dots{
display:flex;
justify-content:center;
gap:6px;
margin-top:12px;
}
.dot{
width:6px;
height:6px;
border-radius:50%;
background:var(--border);
border:none;
cursor:pointer;
}
.dot.is-active{
background:var(--accent);
width:18px;
border-radius:3px;
}
const wrap = document.querySelector('.scroll[data-group="products"]');
const track = wrap.querySelector('.scroll__track');
const items = wrap.querySelectorAll('.scroll__row > *');
const row = wrap.querySelector('.scroll__row');
const step = items[0].getBoundingClientRect().width +
parseFloat(getComputedStyle(row).columnGap || 12);
const visible = Math.max(1, Math.floor(track.clientWidth / step));
const steps = Math.ceil(items.length / visible);
const dotsWrap = document.querySelector('.dots[data-group="products"]');
for (let i = 0; i < steps; i++) {
const dot = document.createElement('button');
dot.className = 'dot' + (i === 0 ? ' is-active' : '');
dot.setAttribute('aria-label', 'Ir al grupo ' + (i + 1) + ' de ' + steps);
dot.addEventListener('click', () =>
track.scrollTo({ left: i * step * visible, behavior: 'smooth' }));
dotsWrap.appendChild(dot);
}
track.addEventListener('scroll', () => {
const idx = Math.round(track.scrollLeft / (step * visible));
dotsWrap.querySelectorAll('.dot').forEach((d, i) =>
d.classList.toggle('is-active', i === idx));
});