fix: доработки слайдера и просмотрщика

- case-media-slider: обрамление+hover как у карточек, стрелки только при наведении (fade-in),
  группа пред/след-пилюля в правом нижнем углу, disabled-кнопки видны (не скрываются),
  убран border-radius у img/figcaption (скругление у viewport), неактивные слайды не скрываются
  visibility (нет 'дырки' при перелистывании), настраиваемая ширина --wide/--content/--natural
- image-load-placeholder: исключён .case-media-slider (нет блика/серого фона)
- icon-press: универсальный эффект 'продавливания' иконки при нажатии (.icon-press + :active/.is-pressed),
  применён к слайдеру и всем кнопкам просмотрщика
This commit is contained in:
root committed 2026-09-09 19:29:30 +02:00
1 parent 95b4eaca93
commit c14bb1f543
6 files changed
+132 -40

No files matched your search

@@ -453,13 +453,15 @@
<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>
<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" 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" aria-hidden="true"></span>
</button>
<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>
+96 -22
View File
@@ -2363,7 +2363,17 @@ html[data-theme="dark"] {
.tui-spinner > span:nth-child(1) { opacity: 1; }
}
/* Лоадер изображения в просмотрщике (обёртка вокруг .tui-spinner) */
/* Универсальный эффект «продавливания» иконки при нажатии кнопки */
.icon-press {
transition: transform 0.12s ease;
}
[class*="btn"]:active:not(:disabled) .icon-press,
[class*="nav"]:active:not(:disabled) .icon-press,
.btn:active:not(:disabled) .icon-press,
.case-media-slider__nav.is-pressed:not(:disabled) .icon-press {
transform: scale(0.82);
}
.case-image-viewer__loader {
position: absolute;
left: 50%;
@@ -5338,16 +5348,28 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
--slider-dot-size: 0.5rem;
--slider-dot-gap: 0.5rem;
position: relative;
margin: 0;
overflow: hidden;
margin: 0.85rem 0 1.2rem;
border-radius: var(--slider-radius);
}
.case-media-slider__viewport {
position: relative;
overflow: hidden;
width: 100%;
border: 1px solid var(--line);
border-radius: var(--slider-radius);
box-sizing: border-box;
background: color-mix(in srgb, var(--surface) 55%, transparent);
touch-action: pan-y;
cursor: grab;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.case-media-slider:hover .case-media-slider__viewport {
border-color: var(--accent);
box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 18%, transparent);
}
.case-media-slider__viewport:active {
@@ -5376,7 +5398,6 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
height: auto;
max-height: 80vh;
object-fit: contain;
border-radius: var(--slider-radius);
}
/* Подпись под активным слайдом (как у case-media) */
@@ -5392,48 +5413,67 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
text-align: center;
background: color-mix(in srgb, var(--surface) 78%, transparent);
backdrop-filter: blur(6px);
border-bottom-left-radius: var(--slider-radius);
border-bottom-right-radius: var(--slider-radius);
}
/* Стрелки */
.case-media-slider__nav {
/* Группа кнопок пред/след — слитая «пилюля» в нижнем правом углу */
.case-media-slider__nav-group {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: var(--slider-ctrl-inset);
bottom: var(--slider-ctrl-inset);
z-index: 3;
display: inline-flex;
border-radius: 999px;
overflow: hidden;
opacity: 0;
transition: opacity 0.18s ease;
}
.case-media-slider:hover .case-media-slider__nav-group,
.case-media-slider__nav-group:focus-within {
opacity: 1;
}
.case-media-slider__nav {
width: var(--slider-ctrl-size);
height: var(--slider-ctrl-size);
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--line);
border-radius: 50%;
border-radius: 0;
background: color-mix(in srgb, var(--surface) 86%, transparent);
color: var(--text-muted);
cursor: pointer;
z-index: 3;
transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease,
opacity 0.18s ease;
transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.case-media-slider__nav--prev {
left: var(--slider-ctrl-inset);
/* Соседняя кнопка перекрывает разделительный бордер */
.case-media-slider__nav + .case-media-slider__nav {
margin-left: -1px;
}
.case-media-slider__nav--next {
right: var(--slider-ctrl-inset);
/* Скругления только на крайних кнопках группы */
.case-media-slider__nav:first-child {
border-radius: 999px 0 0 999px;
}
.case-media-slider__nav:last-child {
border-radius: 0 999px 999px 0;
}
.case-media-slider__nav:hover:not(:disabled),
.case-media-slider__nav:focus-visible:not(:disabled) {
color: var(--accent-text);
border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
background: var(--accent-soft);
position: relative;
z-index: 1;
}
.case-media-slider__nav:disabled {
opacity: 0.35;
opacity: 0.4;
cursor: default;
pointer-events: none;
}
.case-media-slider__nav-ico {
@@ -5447,8 +5487,11 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
-webkit-mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-position: center;
transition: transform 0.12s ease;
}
/* Нажатие: иконка слегка «продавливается» внутрь (общий .icon-press) */
.case-media-slider__nav-ico--left {
mask-image: url("../assets/shared/icons/shevron-left.svg");
-webkit-mask-image: url("../assets/shared/icons/shevron-left.svg");
@@ -5463,9 +5506,11 @@ body.content-edit-shift-active.content-edit-mode-active .content-edit-fab-stack
.case-media-slider__dots {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: var(--slider-dot-gap);
margin-top: 0.9rem;
padding: 0.2rem 0;
}
.case-media-slider__dot {
@@ -5486,12 +5531,41 @@ 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.25);
transform: scale(1.35);
}
/* Неактивные слайды скрыты от скринридеров и не ловят клики по viewer */
/* Неактивные слайды: скрыты от скринридеров (aria-hidden в JS), но остаются видимыми,
чтобы при сдвиге трека не было «дырки» от мгновенно исчезнувшего кадра. */
.case-media-slider__slide:not(.is-active) {
visibility: hidden;
pointer-events: none;
}
/* Настраиваемая ширина — как у статичных .case-media (--wide / --content / --natural).
Кнопки пред/след внутри viewport, поэтому не участвуют в расчёте ширины. */
.case-media-slider.case-media--content,
.case-media-slider.case-media--wide {
display: block;
}
.case-media-slider.case-media--content .case-media-slider__viewport,
.case-media-slider.case-media--wide .case-media-slider__viewport {
width: 100%;
}
.case-media-slider.case-media--natural {
display: block;
}
.case-media-slider.case-media--natural .case-media-slider__viewport {
width: auto;
max-width: 100%;
margin-inline: auto;
}
.case-media-slider.case-media--natural .case-media-slider__slide img {
width: auto;
max-width: 100%;
margin-inline: auto;
}
@media (prefers-reduced-motion: reduce) {
+5 -5
View File
@@ -17,10 +17,10 @@ export function initCaseImageViewer() {
<div class="case-image-viewer__toolbar glass-neutral" role="toolbar" aria-label="Просмотр изображения">
<div class="case-image-viewer__nav-group" role="group" aria-label="Навигация по изображениям">
<button type="button" class="btn btn--secondary btn--flat case-image-viewer__btn case-image-viewer__btn--nav" data-imgv-prev aria-label="Предыдущее изображение" hidden>
<span class="case-image-viewer__btn-icon" style="--imgv-icon:url('../assets/shared/icons/shevron-left.svg')" aria-hidden="true"></span>
<span class="case-image-viewer__btn-icon icon-press" style="--imgv-icon:url('../assets/shared/icons/shevron-left.svg')" aria-hidden="true"></span>
</button>
<button type="button" class="btn btn--secondary btn--flat case-image-viewer__btn case-image-viewer__btn--nav" data-imgv-next aria-label="Следующее изображение" hidden>
<span class="case-image-viewer__btn-icon" style="--imgv-icon:url('../assets/shared/icons/shevron-right.svg')" aria-hidden="true"></span>
<span class="case-image-viewer__btn-icon icon-press" style="--imgv-icon:url('../assets/shared/icons/shevron-right.svg')" aria-hidden="true"></span>
</button>
</div>
<div class="case-image-viewer__dots" data-imgv-dots role="tablist" aria-label="Выбор изображения" hidden></div>
@@ -29,13 +29,13 @@ export function initCaseImageViewer() {
<span class="case-image-viewer__zoom" data-imgv-zoom-label>100%</span>
</div>
<button type="button" class="btn btn--secondary btn--flat case-image-viewer__btn" data-imgv-fit aria-label="Уместить на экран">
<span class="case-image-viewer__btn-icon" style="--imgv-icon:url('../assets/shared/icons/zoom_to_fit.svg')" aria-hidden="true"></span>
<span class="case-image-viewer__btn-icon icon-press" style="--imgv-icon:url('../assets/shared/icons/zoom_to_fit.svg')" aria-hidden="true"></span>
</button>
<button type="button" class="btn btn--secondary btn--flat case-image-viewer__btn" data-imgv-original aria-label="Оригинальный размер">
<span class="case-image-viewer__btn-icon" style="--imgv-icon:url('../assets/shared/icons/original_size.svg')" aria-hidden="true"></span>
<span class="case-image-viewer__btn-icon icon-press" style="--imgv-icon:url('../assets/shared/icons/original_size.svg')" aria-hidden="true"></span>
</button>
<button type="button" class="btn btn--secondary btn--flat case-image-viewer__btn" data-imgv-close aria-label="Закрыть просмотр">
<span class="case-image-viewer__btn-icon" style="--imgv-icon:url('../assets/shared/icons/close.svg')" aria-hidden="true"></span>
<span class="case-image-viewer__btn-icon icon-press" style="--imgv-icon:url('../assets/shared/icons/close.svg')" aria-hidden="true"></span>
</button>
</div>
<div class="case-image-viewer__stage" data-imgv-stage tabindex="0">
+14
View File
@@ -42,6 +42,20 @@ export function initCaseMediaSlider() {
const track = root.querySelector("[data-slider-track]");
const prevBtn = root.querySelector("[data-slider-prev]");
const nextBtn = root.querySelector("[data-slider-next]");
const navBtns = [prevBtn, nextBtn].filter(Boolean);
// Имитация :active для иконки (swipe-обработчик viewport сбрасывает нативный :active)
navBtns.forEach((btn) => {
const press = () => btn.classList.add("is-pressed");
const release = () => btn.classList.remove("is-pressed");
btn.addEventListener("pointerdown", press);
btn.addEventListener("mousedown", press);
btn.addEventListener("pointerup", release);
btn.addEventListener("mouseup", release);
btn.addEventListener("pointerleave", release);
btn.addEventListener("mouseleave", release);
btn.addEventListener("pointercancel", release);
});
const dotsWrap = root.querySelector("[data-slider-dots]");
if (!viewport || !track) return;
@@ -3,7 +3,7 @@ import { isTinyPlaceholderSrc } from "../shared/image-src.js";
const HOST_SELECTOR =
".case-media, .case-media-gallery__item, .case-link-card__media, .case-media-tall__clip, .mp-card__media, .portfolio-card__cover, figure";
const EXCLUDE_ANCESTOR = ".contact-card__icon, .cookie-notice, .case-image-viewer, [data-no-img-placeholder]";
const EXCLUDE_ANCESTOR = ".contact-card__icon, .cookie-notice, .case-image-viewer, .case-media-slider, [data-no-img-placeholder]";
function shouldWatch(img) {
if (!(img instanceof HTMLImageElement)) return false;
+8 -6
View File
@@ -264,13 +264,15 @@
<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>
<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" 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" aria-hidden="true"></span>
</button>
<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>