app/components/Slider/glide-slider.latte
{*
Glide Slider — wrapper kolem Glide.js s šipkami a bullets.
Parametry:
- $id (string) unikátní ID slideru (povinné pro auto-init)
- $perView (int) kolik slidů viditelných najednou (default 1)
- $gap (int) mezera mezi slidy v px (default 16)
- $autoplay (int|false) interval autoplay v ms, false = off (default false)
- $loop (bool) nekonečné loopování (default true)
- $arrows (bool) zobrazit šipky (default true)
- $bullets (bool) zobrazit bullets (default true)
- $breakpoints (array|null) Glide breakpoints config (např. [1024 => ['perView' => 2]])
- $items (array) pole položek
- $itemTemplate (string|null) cesta k template pro slide
*}
{var $id = $id ?? 'glide-' . substr(md5(random_bytes(8)), 0, 8)}
{var $perView = $perView ?? 1}
{var $gap = $gap ?? 16}
{var $autoplay = $autoplay ?? false}
{var $loop = $loop ?? true}
{var $arrows = $arrows ?? true}
{var $bullets = $bullets ?? true}
{var $breakpoints = $breakpoints ?? null}
{var $items = $items ?? []}
{var $itemTemplate = $itemTemplate ?? null}
<div class="glide-slider glide"
id="{$id}"
data-glide-slider
data-per-view="{$perView}"
data-gap="{$gap}"
data-autoplay="{$autoplay !== false ? $autoplay : ''}"
data-loop="{$loop ? 'true' : 'false'}"
{if $breakpoints}data-breakpoints="{json_encode($breakpoints)}"{/if}>
<div class="glide-slider__track glide__track" data-glide-el="track">
<ul class="glide-slider__slides glide__slides">
{foreach $items as $item}
<li class="glide-slider__slide glide__slide">
{if $itemTemplate}
{include $itemTemplate, item => $item}
{else}
{$item}
{/if}
</li>
{/foreach}
</ul>
</div>
{if $arrows}
<div class="glide-slider__arrows glide__arrows" data-glide-el="controls">
<button class="glide-slider__arrow glide-slider__arrow--prev" data-glide-dir="<" type="button" aria-label="Předchozí">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
<button class="glide-slider__arrow glide-slider__arrow--next" data-glide-dir=">" type="button" aria-label="Další">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
</button>
</div>
{/if}
{if $bullets}
<div class="glide-slider__bullets glide__bullets" data-glide-el="controls[nav]">
{for $i = 0; $i < count($items); $i++}
<button class="glide-slider__bullet glide__bullet" data-glide-dir="={$i}" type="button" aria-label="Slide {$i+1}"></button>
{/for}
</div>
{/if}
</div>
resources/sass/components/_glide-slider.scss
// ─── Configurable variables (override before @import) ──────────────────
$cgui-glide-slider-arrow-size: 64px !default;
$cgui-glide-slider-arrow-bg: #fff !default;
$cgui-glide-slider-arrow-color: $cgui-color-text !default;
$cgui-glide-slider-arrow-bg-hover: $cgui-color-primary !default;
$cgui-glide-slider-arrow-color-hover: #fff !default;
$cgui-glide-slider-arrow-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !default;
$cgui-glide-slider-arrow-offset: -32px !default;
$cgui-glide-slider-bullet-size: 13px !default;
$cgui-glide-slider-bullet-gap: 6px !default;
$cgui-glide-slider-bullet-color: $cgui-color-border !default;
$cgui-glide-slider-bullet-active-color: $cgui-color-primary !default;
$cgui-glide-slider-bullets-margin-top: 32px !default;
$cgui-glide-slider-track-padding: 0 !default;
// ───────────────────────────────────────────────────────────────────────
.glide-slider {
position: relative;
&__track {
overflow: hidden;
padding: $cgui-glide-slider-track-padding;
}
&__slides {
display: flex;
list-style: none;
margin: 0;
padding: 0;
will-change: transform;
}
&__slide {
flex-shrink: 0;
}
&__arrows {
position: absolute;
top: 50%;
left: $cgui-glide-slider-arrow-offset;
right: $cgui-glide-slider-arrow-offset;
display: flex;
justify-content: space-between;
pointer-events: none;
transform: translateY(-50%);
z-index: 2;
}
&__arrow {
display: inline-flex;
align-items: center;
justify-content: center;
width: $cgui-glide-slider-arrow-size;
height: $cgui-glide-slider-arrow-size;
background: $cgui-glide-slider-arrow-bg;
color: $cgui-glide-slider-arrow-color;
border: none;
border-radius: 50%;
box-shadow: $cgui-glide-slider-arrow-shadow;
cursor: pointer;
pointer-events: auto;
transition: background 0.15s, color 0.15s;
&:hover,
&:focus {
background: $cgui-glide-slider-arrow-bg-hover;
color: $cgui-glide-slider-arrow-color-hover;
}
&[disabled] {
opacity: 0.4;
cursor: not-allowed;
}
svg {
width: 22px;
height: 22px;
}
}
&__bullets {
display: flex;
justify-content: center;
gap: $cgui-glide-slider-bullet-gap;
margin-top: $cgui-glide-slider-bullets-margin-top;
}
&__bullet {
width: $cgui-glide-slider-bullet-size;
height: $cgui-glide-slider-bullet-size;
padding: 0;
border: none;
border-radius: 50%;
background: $cgui-glide-slider-bullet-color;
cursor: pointer;
transition: background 0.15s;
&--active,
&.glide__bullet--active {
background: $cgui-glide-slider-bullet-active-color;
}
}
}
resources/js/components/glide-slider.js
// Glide Slider — auto-init pro [data-glide-slider]. Wraps Glide.js.
// Vyžaduje: npm install @glidejs/glide
// import Glide from '@glidejs/glide';
import Glide from '@glidejs/glide';
const instances = new WeakMap();
function init(root) {
if (instances.has(root)) return instances.get(root);
const perView = parseInt(root.dataset.perView || '1', 10);
const gap = parseInt(root.dataset.gap || '16', 10);
const autoplay = root.dataset.autoplay ? parseInt(root.dataset.autoplay, 10) : false;
const loop = root.dataset.loop !== 'false';
let breakpoints = null;
if (root.dataset.breakpoints) {
try {
breakpoints = JSON.parse(root.dataset.breakpoints);
} catch (e) {
console.warn('[glide-slider] invalid breakpoints JSON', e);
}
}
const options = {
type: loop ? 'carousel' : 'slider',
perView,
gap,
autoplay,
hoverpause: !!autoplay,
...(breakpoints ? { breakpoints } : {}),
};
const glide = new Glide(root, options);
glide.mount();
instances.set(root, glide);
return glide;
}
function destroy(root) {
const glide = instances.get(root);
if (glide) {
glide.destroy();
instances.delete(root);
}
}
export function initAll(scope = document) {
scope.querySelectorAll('[data-glide-slider]').forEach(init);
}
export function destroyAll(scope = document) {
scope.querySelectorAll('[data-glide-slider]').forEach(destroy);
}
// Auto-init
if (typeof document !== 'undefined') {
if (document.readyState !== 'loading') {
initAll();
} else {
document.addEventListener('DOMContentLoaded', () => initAll());
}
}
export { init, destroy };