glide-slider — Wrapper kolem Glide.js — šipky, bullets, autoplay, breakpoints. Auto-init přes data-attribute.

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 };