app/components/Slider/slider.latte
{*
Slider — carousel s šipkami.
Parametry:
- $id (string): unikátní ID slideru
- $items (iterable): kolekce items (kompatibilní s {foreach})
- $itemsPerView (int): počet items zobrazených najednou (default 3)
- $itemTemplate (string|null): cesta k šabloně pro jednu položku
pokud null, item se rendruje jako string
Použití:
{include slider.latte,
id => 'products',
items => $products,
itemsPerView => 4,
itemTemplate => __DIR__ . '/templates/product-card.latte'
}
*}
{var $itemsPerView = $itemsPerView ?? 3}
<div class="slider"
id="{$id}"
data-slider
data-items-per-view="{$itemsPerView}">
<button class="slider__arrow slider__arrow--prev" data-slider-prev aria-label="Předchozí">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"/>
</svg>
</button>
<div class="slider__viewport" data-slider-viewport>
<div class="slider__track" data-slider-track>
{foreach $items as $item}
<div class="slider__slide" data-slider-slide>
{if isset($itemTemplate) && $itemTemplate !== null}
{include $itemTemplate, item => $item}
{else}
{$item|noescape}
{/if}
</div>
{/foreach}
</div>
</div>
<button class="slider__arrow slider__arrow--next" data-slider-next aria-label="Další">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>
</div>
resources/sass/components/_slider.scss
.slider {
position: relative;
display: flex;
align-items: center;
gap: 8px;
&__viewport {
flex: 1;
overflow: hidden;
}
&__track {
display: flex;
transition: transform 0.3s ease-out;
will-change: transform;
}
&__slide {
flex-shrink: 0;
padding: 0 8px;
}
// Default — desktop, dle data-items-per-view
&[data-items-per-view="1"] &__slide { width: 100%; }
&[data-items-per-view="2"] &__slide { width: 50%; }
&[data-items-per-view="3"] &__slide { width: 33.3333%; }
&[data-items-per-view="4"] &__slide { width: 25%; }
// Mobile fallback — vždy 1
@media (max-width: $cgui-bp-small) {
&[data-items-per-view] &__slide { width: 100%; }
}
@media (min-width: $cgui-bp-small) and (max-width: $cgui-bp-medium) {
&[data-items-per-view="3"] &__slide,
&[data-items-per-view="4"] &__slide { width: 50%; }
}
&__arrow {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid $cgui-color-border;
background: $cgui-color-bg;
color: $cgui-color-text;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s;
z-index: 1;
@include cgui-hover() {
background: $cgui-color-primary;
color: white;
border-color: $cgui-color-primary;
}
&:disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
}
}
}
resources/js/components/slider.js
// Slider — carousel s šipkami. Auto-init pro všechny [data-slider] na stránce.
import { $, $$, on, debounce, clamp } from '../base/utils.js';
class Slider {
constructor(root) {
this.root = root;
this.viewport = $('[data-slider-viewport]', root);
this.track = $('[data-slider-track]', root);
this.slides = $$('[data-slider-slide]', root);
this.btnPrev = $('[data-slider-prev]', root);
this.btnNext = $('[data-slider-next]', root);
this.itemsPerView = parseInt(root.dataset.itemsPerView || '3', 10);
this.index = 0;
if (!this.slides.length) return;
on(this.btnPrev, 'click', () => this.go(this.index - 1));
on(this.btnNext, 'click', () => this.go(this.index + 1));
const onResize = debounce(() => this.update(), 100);
on(window, 'resize', onResize);
this.update();
}
get visibleCount() {
const w = window.innerWidth;
if (w < 640) return 1;
if (w < 960 && this.itemsPerView > 2) return 2;
return this.itemsPerView;
}
get maxIndex() {
return Math.max(0, this.slides.length - this.visibleCount);
}
go(target) {
this.index = clamp(target, 0, this.maxIndex);
this.update();
}
update() {
const slideWidth = this.slides[0]?.offsetWidth || 0;
this.track.style.transform = `translateX(${-this.index * slideWidth}px)`;
this.btnPrev.disabled = this.index === 0;
this.btnNext.disabled = this.index >= this.maxIndex;
}
}
export function initSliders(root = document) {
$$('[data-slider]', root).forEach(el => new Slider(el));
}
// Auto-init pokud je modul importován jako side-effect
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => initSliders());
} else {
initSliders();
}
}