feat: варианты анимации слайдера (slide/fade/none), пилюли-точки, blur-фон просмотрщика, фикс открытия в natural-слайдерах

This commit is contained in:
root committed 2026-09-10 19:58:21 +02:00
1 parent c14bb1f543
commit 2d0f3f3989
21 files changed
+552 -23

No files matched your search

+8
View File
@@ -154,6 +154,14 @@
- ленивая подгрузка соседних кадров: у `<img>` можно задать `data-src` (временный `src` — прозрачный GIF), скрипт подставит при подходе, как у галереи; пока есть `data-src`, клик по viewer не открывается;
- клик по активному кадру открывает общий viewer кейса (`data-image-viewer`).
**Ширина слайдера подстраивается под ширину картинок** (высота — автоматически по контенту). Модификаторы как у статичных `.case-media`:
- базовый (без модификатора) / `.case-media--content` — ширина = `--slider-natural-w` (по умолчанию 100% колонки контента). Задайте на `<figure>` inline-стиль `--slider-natural-w: <ширина картинки>px`, чтобы слайдер точно встал в размер кадров и не превышал его; `max-width` ограничен вьюпортом, поэтому на узких экранах сжимается;
- `.case-media--wide` — слайдер выходит за колонку контента (как одиночные wide-картинки), центрируется по вьюпорту;
- `.case-media--natural` — как базовый, но `max-width` по умолчанию `600px` (переопределите `--slider-natural-w`).
Кнопки «пред/след» и точки находятся **внутри** `.case-media-slider__viewport`, поэтому не участвуют в расчёте ширины блока.
**Разметка** (число `data-slider-dot` должно равняться числу `.case-media-slider__slide`; `aria-label` точек — «Перейти к изображению N»):
```html
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -470,6 +470,196 @@
</div>
</figure>
<p class="section__prose">
Тот же слайдер, но с модификатором <code>case-media--wide</code> — выходит за колонку
контента, как одиночные широкие картинки. Кнопки пред/след и точки ведут себя так же.
</p>
<figure class="case-media case-media-slider case-media--wide" data-case-media-slider>
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-1.webp" alt="Слайдер wide, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-2.webp" alt="Слайдер wide, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-3.webp" alt="Слайдер wide, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-4.webp" alt="Слайдер wide, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-5.webp" alt="Слайдер wide, кадр 5" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-6.webp" alt="Слайдер wide, кадр 6" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-7.webp" alt="Слайдер wide, кадр 7" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 5"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 6"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 7"></button>
</div>
</figure>
<p class="section__prose">
Слайдер с горизонтальными (широкими) кадрами одного размера — все картинки
<code>1024×800</code>.
</p>
<figure class="case-media case-media-slider case-media--natural" data-case-media-slider style="--slider-natural-w: 1024px">
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-1.webp" alt="Широкий слайдер, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-2.webp" alt="Широкий слайдер, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-3.webp" alt="Широкий слайдер, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-4.webp" alt="Широкий слайдер, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-5.webp" alt="Широкий слайдер, кадр 5" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-6.webp" alt="Широкий слайдер, кадр 6" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-wide-7.webp" alt="Широкий слайдер, кадр 7" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 5"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 6"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 7"></button>
</div>
</figure>
<p class="section__prose">
Тот же слайдер, но с вертикальными кадрами одного размера — все картинки
<code>600×800</code>.
</p>
<figure class="case-media case-media-slider case-media--natural" data-case-media-slider style="--slider-natural-w: 600px">
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-1.webp" alt="Вертикальный слайдер, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-2.webp" alt="Вертикальный слайдер, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-3.webp" alt="Вертикальный слайдер, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-4.webp" alt="Вертикальный слайдер, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-5.webp" alt="Вертикальный слайдер, кадр 5" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-6.webp" alt="Вертикальный слайдер, кадр 6" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-portrait-7.webp" alt="Вертикальный слайдер, кадр 7" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 5"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 6"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 7"></button>
</div>
</figure>
<p class="section__prose">
Отдельный пример: переключатель режима анимации смены кадров —
<code>slide</code> (сдвиг), <code>fade</code> (кроссфейд) и <code>none</code> (без перехода).
Режим задаётся атрибутом <code>data-slider-anim</code> на фигуре.
</p>
<figure class="case-media case-media-slider" data-case-media-slider data-slider-anim="slide">
<div class="case-media-slider__anim-switch" data-slider-anim-switch role="group" aria-label="Режим анимации слайдера">
<button type="button" data-mode="slide" aria-pressed="true">Слайд</button>
<button type="button" data-mode="fade" aria-pressed="false">Fade</button>
<button type="button" data-mode="none" aria-pressed="false">None</button>
</div>
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-demo-1.webp" alt="Пример анимации, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-demo-2.webp" alt="Пример анимации, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-demo-3.webp" alt="Пример анимации, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../../assets/template-elements-demo/slider-demo-4.webp" alt="Пример анимации, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
</div>
</figure>
<h3>Callout: важный абзац с эмоджи</h3>
<aside class="case-callout" data-callout>
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
+104 -19
View File
@@ -2106,9 +2106,9 @@ img[data-image-viewer]:not([data-src]):focus-visible {
position: fixed;
inset: 0;
z-index: 1400;
background: color-mix(in srgb, var(--bg) 58%, black);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
background: color-mix(in srgb, var(--bg) 72%, transparent);
backdrop-filter: blur(22px) saturate(130%);
-webkit-backdrop-filter: blur(22px) saturate(130%);
opacity: 0;
transition: opacity 0.18s ease;
}
@@ -2369,8 +2369,8 @@ html[data-theme="dark"] {
}
[class*="btn"]:active:not(:disabled) .icon-press,
[class*="nav"]:active:not(:disabled) .icon-press,
.btn:active:not(:disabled) .icon-press,
.case-image-viewer__btn--nav:active:not(:disabled) .icon-press,
.case-media-slider__nav.is-pressed:not(:disabled) .icon-press {
transform: scale(0.82);
}
@@ -2471,14 +2471,14 @@ html[data-theme="dark"] {
}
.case-image-viewer__dot {
width: 0.5rem;
height: 0.5rem;
width: 0.375rem;
height: 0.375rem;
padding: 0;
border: none;
border-radius: 50%;
background: color-mix(in srgb, var(--text-muted) 40%, transparent);
cursor: pointer;
transition: background 0.18s ease, transform 0.18s ease;
transition: background 0.22s ease, width 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), border-radius 0.28s ease;
}
.case-image-viewer__dot:hover,
@@ -2488,7 +2488,8 @@ html[data-theme="dark"] {
.case-image-viewer__dot.is-active {
background: var(--accent);
transform: scale(1.25);
width: calc(0.375rem * 2.6);
border-radius: 999px;
}
@media (max-width: 640px) {
@@ -5345,8 +5346,8 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
--slider-radius: var(--radius-card);
--slider-ctrl-size: 2.5rem;
--slider-ctrl-inset: 0.75rem;
--slider-dot-size: 0.5rem;
--slider-dot-gap: 0.5rem;
--slider-dot-size: 0.375rem;
--slider-dot-gap: 0.375rem;
position: relative;
margin: 0.85rem 0 1.2rem;
border-radius: var(--slider-radius);
@@ -5521,7 +5522,7 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
border-radius: 50%;
background: color-mix(in srgb, var(--text-muted) 40%, transparent);
cursor: pointer;
transition: background 0.18s ease, transform 0.18s ease;
transition: background 0.22s ease, width 0.28s cubic-bezier(0.22, 0.61, 0.36, 1), border-radius 0.28s ease;
}
.case-media-slider__dot:hover,
@@ -5531,7 +5532,8 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
.case-media-slider__dot.is-active {
background: var(--accent);
transform: scale(1.35);
width: calc(var(--slider-dot-size) * 2.6);
border-radius: 999px;
}
/* Неактивные слайды: скрыты от скринридеров (aria-hidden в JS), но остаются видимыми,
@@ -5541,30 +5543,52 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
}
/* Настраиваемая ширина — как у статичных .case-media (--wide / --content / --natural).
Кнопки пред/след внутри viewport, поэтому не участвуют в расчёте ширины. */
.case-media-slider.case-media--content,
.case-media-slider.case-media--wide {
Кнопки пред/след внутри viewport, поэтому не участвуют в расчёте ширины.
Высота — автоматически по контенту. */
/* Базовый / --content: встроен в колонку контента, не шире её */
.case-media-slider,
.case-media-slider.case-media--content {
display: block;
}
.case-media-slider.case-media--content .case-media-slider__viewport,
.case-media-slider.case-media--wide .case-media-slider__viewport {
width: 100%;
max-width: 100%;
margin-inline: auto;
}
/* --wide: как одиночные wide-картинки страницы — выходит за колонку, центр по вьюпорту */
.case-media-slider.case-media--wide {
width: min(calc(100vw - (max(var(--space), env(safe-area-inset-left)) + max(var(--space), env(safe-area-inset-right)))), calc(100% + 8rem));
max-width: none;
margin-left: 50%;
transform: translateX(-50%);
}
/* --natural / auto: ширина по картинке (--slider-natural-w или авто из JS), не больше ширины экрана */
.case-media-slider.case-media--natural {
display: block;
width: min(var(--slider-natural-w, 100%), 100vw);
max-width: 100vw;
margin-left: 50%;
transform: translateX(-50%);
}
.case-media-slider .case-media-slider__viewport,
.case-media-slider.case-media--content .case-media-slider__viewport,
.case-media-slider.case-media--wide .case-media-slider__viewport,
.case-media-slider.case-media--natural .case-media-slider__viewport {
width: auto;
max-width: 100%;
margin-inline: auto;
}
.case-media-slider .case-media-slider__slide img,
.case-media-slider.case-media--content .case-media-slider__slide img,
.case-media-slider.case-media--wide .case-media-slider__slide img,
.case-media-slider.case-media--natural .case-media-slider__slide img {
width: auto;
max-width: 100%;
max-height: none;
height: auto;
margin-inline: auto;
}
@@ -5573,3 +5597,64 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
transition: none;
}
}
/* Режимы анимации переключения кадров.
slide — сдвиг трека (по умолчанию); fade — кроссфейд активного слайда;
none — мгновенная смена без переходов. */
.case-media-slider--anim-fade .case-media-slider__track {
display: block;
transform: none !important;
}
.case-media-slider--anim-fade .case-media-slider__slide {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.45s ease;
pointer-events: none;
}
.case-media-slider--anim-fade .case-media-slider__slide.is-active {
position: relative;
opacity: 1;
pointer-events: auto;
}
.case-media-slider--anim-none .case-media-slider__track {
transition: none !important;
}
.case-media-slider--anim-none .case-media-slider__slide {
transition: none !important;
}
/* Переключатель режима анимации (для демо-примера) */
.case-media-slider__anim-switch {
display: inline-flex;
gap: 0.4rem;
margin: 0.4rem 0 0.2rem;
padding: 0;
}
.case-media-slider__anim-switch button {
font: inherit;
font-size: 0.8rem;
padding: 0.25rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
}
.case-media-slider__anim-switch button:hover {
color: var(--accent);
border-color: var(--accent);
}
.case-media-slider__anim-switch button[aria-pressed="true"] {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
+1 -1
View File
@@ -324,7 +324,7 @@ export function initCaseImageViewer() {
const t = e.target;
if (!(t instanceof Element)) return;
const img = t.closest("img[data-image-viewer]");
if (!img || img.closest(".case-media--natural")) return;
if (!img) return;
if (img.hasAttribute("data-src")) return;
const src = img.currentSrc || img.getAttribute("src") || "";
if (!src || isViewerThumbnailPlaceholder(src)) return;
+56 -2
View File
@@ -59,6 +59,22 @@ export function initCaseMediaSlider() {
const dotsWrap = root.querySelector("[data-slider-dots]");
if (!viewport || !track) return;
// Авто-подстройка ширины под первую картинку — только для --natural (auto) слайдеров,
// и только если --slider-natural-w не задана вручную. Базовый/wide управляются CSS.
const firstImg = root.querySelector(".case-media-slider__slide img");
const applyAutoWidth = (img) => {
if (root.style.getPropertyValue("--slider-natural-w")) return;
const nw = img.naturalWidth || 0;
if (nw > 0) root.style.setProperty("--slider-natural-w", `${nw}px`);
};
if (root.classList.contains("case-media--natural") && firstImg) {
if (firstImg.complete && firstImg.naturalWidth > 0) {
applyAutoWidth(firstImg);
} else {
firstImg.addEventListener("load", () => applyAutoWidth(firstImg), { once: true });
}
}
const slides = Array.from(track.querySelectorAll(":scope > .case-media-slider__slide"));
const dots = dotsWrap
? Array.from(dotsWrap.querySelectorAll(":scope > [data-slider-dot]"))
@@ -72,11 +88,33 @@ export function initCaseMediaSlider() {
"(prefers-reduced-motion: reduce)"
).matches;
// Режим анимации переключения: slide (по умолчанию) / fade / none
let mode = root.getAttribute("data-slider-anim") || "slide";
function applyMode(m) {
mode = m;
root.classList.remove(
"case-media-slider--anim-slide",
"case-media-slider--anim-fade",
"case-media-slider--anim-none"
);
root.classList.add(`case-media-slider--anim-${m}`);
if (m === "fade") {
track.style.transform = "none";
} else {
track.style.transform = `translateX(${-100 * current}%)`;
}
}
applyMode(mode);
function goTo(index, behavior) {
const next = clampIndex(index, count);
current = next;
const offset = -100 * next;
track.style.transform = `translateX(${offset}%)`;
if (mode !== "fade") {
const offset = -100 * next;
track.style.transform = `translateX(${offset}%)`;
} else {
track.style.transform = "none";
}
track.style.transition = reduceMotion || behavior === "auto"
? "none"
: "";
@@ -151,5 +189,21 @@ export function initCaseMediaSlider() {
s.classList.toggle("is-active", i === 0);
});
goTo(0, "auto");
// UI-переключатель режима анимации (для демо-примера), если есть в разметке.
const animSwitch = root.querySelector("[data-slider-anim-switch]");
if (animSwitch) {
const btns = Array.from(animSwitch.querySelectorAll("button"));
const sync = () => btns.forEach((b) => {
b.setAttribute("aria-pressed", String(b.dataset.mode === mode));
});
sync();
btns.forEach((b) => {
b.addEventListener("click", () => {
applyMode(b.dataset.mode || "slide");
sync();
});
});
}
});
}
@@ -1,5 +1,7 @@
export function initCaseNaturalImagesHalfSize() {
const images = Array.from(document.querySelectorAll(".case-media--natural img"));
const images = Array.from(
document.querySelectorAll(".case-media--natural img")
).filter((img) => !img.closest(".case-media-slider"));
if (!images.length) return;
function applyHalfSize(img) {
+190
View File
@@ -281,6 +281,196 @@
</div>
</figure>
<p class="section__prose">
Тот же слайдер, но с модификатором <code>case-media--wide</code> — выходит за колонку
контента, как одиночные широкие картинки. Кнопки пред/след и точки ведут себя так же.
</p>
<figure class="case-media case-media-slider case-media--wide" data-case-media-slider>
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-1.webp" alt="Слайдер wide, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-2.webp" alt="Слайдер wide, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-3.webp" alt="Слайдер wide, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-4.webp" alt="Слайдер wide, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-5.webp" alt="Слайдер wide, кадр 5" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-6.webp" alt="Слайдер wide, кадр 6" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-7.webp" alt="Слайдер wide, кадр 7" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 5"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 6"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 7"></button>
</div>
</figure>
<p class="section__prose">
Слайдер с горизонтальными (широкими) кадрами одного размера — все картинки
<code>1024×800</code>.
</p>
<figure class="case-media case-media-slider case-media--natural" data-case-media-slider style="--slider-natural-w: 1024px">
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-1.webp" alt="Широкий слайдер, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-2.webp" alt="Широкий слайдер, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-3.webp" alt="Широкий слайдер, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-4.webp" alt="Широкий слайдер, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-5.webp" alt="Широкий слайдер, кадр 5" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-6.webp" alt="Широкий слайдер, кадр 6" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-wide-7.webp" alt="Широкий слайдер, кадр 7" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 5"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 6"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 7"></button>
</div>
</figure>
<p class="section__prose">
Тот же слайдер, но с вертикальными кадрами одного размера — все картинки
<code>600×800</code>.
</p>
<figure class="case-media case-media-slider case-media--natural" data-case-media-slider style="--slider-natural-w: 600px">
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-1.webp" alt="Вертикальный слайдер, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-2.webp" alt="Вертикальный слайдер, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-3.webp" alt="Вертикальный слайдер, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-4.webp" alt="Вертикальный слайдер, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-5.webp" alt="Вертикальный слайдер, кадр 5" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-6.webp" alt="Вертикальный слайдер, кадр 6" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-portrait-7.webp" alt="Вертикальный слайдер, кадр 7" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 5"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 6"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 7"></button>
</div>
</figure>
<p class="section__prose">
Отдельный пример: переключатель режима анимации смены кадров —
<code>slide</code> (сдвиг), <code>fade</code> (кроссфейд) и <code>none</code> (без перехода).
Режим задаётся атрибутом <code>data-slider-anim</code> на фигуре.
</p>
<figure class="case-media case-media-slider" data-case-media-slider data-slider-anim="slide">
<div class="case-media-slider__anim-switch" data-slider-anim-switch role="group" aria-label="Режим анимации слайдера">
<button type="button" data-mode="slide" aria-pressed="true">Слайд</button>
<button type="button" data-mode="fade" aria-pressed="false">Fade</button>
<button type="button" data-mode="none" aria-pressed="false">None</button>
</div>
<div class="case-media-slider__viewport" data-slider-viewport>
<div class="case-media-slider__track" data-slider-track>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-1.webp" alt="Пример анимации, кадр 1" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-2.webp" alt="Пример анимации, кадр 2" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-3.webp" alt="Пример анимации, кадр 3" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="{{assetPrefix}}assets/template-elements-demo/slider-demo-4.webp" alt="Пример анимации, кадр 4" loading="lazy" decoding="async" data-image-viewer />
</div>
</div>
<div class="case-media-slider__nav-group">
<button type="button" class="case-media-slider__nav case-media-slider__nav--prev" data-slider-prev aria-label="Предыдущее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--left icon-press" aria-hidden="true"></span>
</button>
<button type="button" class="case-media-slider__nav case-media-slider__nav--next" data-slider-next aria-label="Следующее изображение">
<span class="case-media-slider__nav-ico case-media-slider__nav-ico--right icon-press" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="case-media-slider__dots" data-slider-dots role="tablist" aria-label="Выбор изображения">
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 1"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 2"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 3"></button>
<button type="button" class="case-media-slider__dot" data-slider-dot aria-label="Перейти к изображению 4"></button>
</div>
</figure>
<h3>Callout: важный абзац с эмоджи</h3>
<aside class="case-callout" data-callout>
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>