Files
site-portfolio/docs/case-page-blocks.md
T
root 95b4eaca93 feat: галерея-слайдер, листалка в просмотрщике и TUI braille-спиннер
- case-media-slider: переиспользуемый слайдер (prev/next + точки, swipe, клавиатура, lazy)
- image-viewer: листалка prev/next в тулбаре как пилюли, пагинация-точки в тулбаре слева,
  группа масштаба (slider + текст) с gap 0.25rem, текст масштаба --text-muted/400
- tui-spinner: переиспользуемый braille-спиннер (чистый CSS), лоадер просмотрщика
- design-system.html: живой пример TUI braille-спиннера
- fix: удалён мёртвый textContent='Загрузка...', убивавший .tui-spinner при повторном открытии
2026-09-09 17:49:56 +02:00

242 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Страница кейса: каркас и блоки контента
Инструкция завязана на эталон **`src/pages/cases/template-elements-demo.html`**: оттуда копируются готовые куски разметки. Текстовый дубль структуры для редактуры — `texts/cases/template-elements-demo.md`.
## Где править
| Что | Путь |
|-----|------|
| Разметка кейса | `src/pages/cases/<имя>.html` |
| Общие вставки (шапка кейса, футер, скрипты) | `src/partials/*.html` |
| Сборка в статику | `npm run build:pages` → `public/cases/<имя>.html` |
**`public/cases/*.html` не редактировать вручную** — они перезаписываются сборкой.
Локальные инструкции агенту без правки HTML в браузере: режим правок при `npm run serve` → [`docs/content-edit-mode.md`](content-edit-mode.md) → `texts/content-edits/*.json` → apply в `src` по запросу в чате.
## Каркас `body` (как в template)
Порядок важен для единообразия с остальными кейсами:
1. `{{> header-case}}` — шапка с ссылкой на главную (внутри partial используется `{{assetPrefix}}`).
2. `{{> fab-stack}}` — плавающие кнопки темы и т.п.
3. `<main id="case-main" class="section section--tight">` → внутри `<div class="section__inner case-article">` — **весь уникальный контент кейса**.
4. Нижние секции как на главной: `{{> projects-section}}`, `{{> contact-section}}`, `{{> footer-spacer}}`, `{{> footer}}`, `{{> cookie-notice}}`, `{{> nav-more-floating}}`.
5. Скрипты подключаются в `<head>` через `{{> head-links}}` (preload + `main.js`, `skill-connectors.js`).
`head` у кейсов: `meta`, `viewport`, `description`, `title`, затем `{{> head-links}}` (preconnect/preload шрифтов и CSS/JS, стили, скрипты с `defer`), при необходимости локальный `<style>` для `.case-article`.
## Пути к CSS и медиа
Файл страницы лежит в **`cases/`**, один уровень ниже корня сайта. В итоговом HTML пути от страницы:
- Стили: `../css/main.css`
- Картинки и видео проекта: `../assets/<папка-проекта>/файл.ext`
В **partials** для ссылок на корень сайта используй **`{{assetPrefix}}`** (при сборке из `cases/*` подставится `../`). В самой странице кейса чаще пишут литерал `../`, как в template.
## Текст и заголовки
Внутри `.case-article`:
- Вводная строка: `<p class="hero__eyebrow">…</p>`
- Заголовок: `<h1 class="case-page__h1">…</h1>`
- Подзаголовок (по желанию): `<p class="case-page__sub">…</p>`
- Обычные абзацы: `<p class="section__prose">…</p>`
- Секции: `<h2>`, при необходимости `<h3>`
Списки — обычные `<ul>` / `<ol>` без обязательных классов (стили задаёт вложенный CSS страницы для `.case-article`).
## Блоки (копировать из template)
### Сетка карточек ссылок
- Обертка: `<div class="case-links-grid">`.
- Одна карточка: `<a class="case-link-card" href="…" target="_blank" rel="noopener noreferrer">`.
- Внутри: `<span class="case-link-card__media">` (превью-картинка или favicon) и `<span class="case-link-card__body">` с `case-link-card__title`, `case-link-card__meta`, `case-link-card__url`.
- Вариант без тяжёлого превью: на `<a>` добавить модификатор **`case-link-card--favicon`**, в `media` — картинка с `https://www.google.com/s2/favicons?domain=…`.
### Изображения (`figure.case-media`)
| Модификатор | Поведение |
|-------------|-----------|
| `case-media--natural` | Натуральный масштаб (визуально ~половина логического размера, см. `initCaseNaturalImagesHalfSize` в `main.js`). **Без** `data-image-viewer`. |
| `case-media--content` | По ширине текстовой колонки; клик — полноэкранный просмотр, если на `<img>` есть **`data-image-viewer`** (обводка и hover как у `.case-video-shell`). |
| `case-media--wide` | Чуть шире колонки; тоже с **`data-image-viewer`** при необходимости просмотра. |
Общие атрибуты для картинок в кейсе: `loading="lazy"`, `decoding="async"`, осмысленный `alt`, при известных размерах — `width` и `height`.
Подпись: `<figcaption>…</figcaption>`. У блока с `.case-video-shell` подпись по центру под видео (как у кадров с `data-image-viewer`).
### Длинный скрин (`data-case-tall-media`)
Для **очень высокого** кадра (полный экран приложения, длинная страница): в свёрнутом виде показывается только верх, внизу превью — плотный градиент («туман»), чтобы было видно, что картинка **не целиком**. Рамка как у кликабельных кадров. Кнопка «Развернуть» / «Свернуть» с шевроном поверх нижней части превью (внутри рамки); после сворачивания — прокрутка к превью (`scrollIntoView` на clip).
**Ширина:** на `figure` с `data-case-tall-media` задай **`data-case-tall-width="content"`** (по ширине текстовой колонки) или **`data-case-tall-width="wide"`** (как у обычной «широкой» картинки: вынос за колонку). Допустим и старый вариант без атрибута: **`class="case-media case-media--wide"`** на `figure` — эквивалентно `wide`. Не смешивай `data-case-tall-width="content"` с классом `case-media--wide` на одном `figure`.
Стили: `.case-media-tall*` и блок ширины в `public/css/main.css`. Логика: [`public/js/modules/cases/tall-media-expand.js`](public/js/modules/cases/tall-media-expand.js) (`initCaseTallMediaExpand` уже вызывается из [`public/js/main.js`](public/js/main.js)).
**Как вставить в кейс**
1. На **`figure`** поставь **`class="case-media"`**, **`data-case-tall-media`**, **`data-case-tall-width="content"`** или **`wide`**.
2. Внутри — разметка ниже: уникальный **`id`** у `.case-media-tall__clip`, тот же id в **`aria-controls`** у кнопки; подписи «Развернуть»/«Свернуть» в span с **`data-case-tall-label-more`** / **`data-case-tall-label-less`** (у «Свернуть» изначально **`hidden`**).
3. На **`img`** оставь **`data-image-viewer`**, если нужен полноэкранный просмотр по клику.
4. Высоту превью (обрезки) задай на clip, например inline: **`style="--case-tall-max: min(60vh, 28rem)"`**. Если не задавать, в CSS по умолчанию `min(70vh, 32rem)`.
5. Файлы клади в **`public/assets/<папка-кейса>/…`**; в HTML страниц кейсов пути **`../assets/<папка>/файл.webp`** (относительно `public/cases/`).
**Сниппет** (подставь `УНИКАЛЬНЫЙ-ID`, путь к WebP, `alt`, при необходимости замени **`wide`** на **`content`**):
```html
<figure class="case-media" data-case-tall-media data-case-tall-width="wide">
<div class="case-media-tall" data-case-tall-root>
<div
class="case-media-tall__clip"
id="УНИКАЛЬНЫЙ-ID"
data-case-tall-clip
style="--case-tall-max: min(70vh, 32rem)"
>
<img
src="../assets/<папка-кейса>/<файл>.webp"
alt="…"
width="1200"
loading="lazy"
decoding="async"
data-image-viewer
/>
<div class="case-media-tall__fog" aria-hidden="true"></div>
</div>
<div class="case-media-tall__actions">
<button
type="button"
class="case-media-tall__btn btn btn--ghost btn--flat"
data-case-tall-toggle
aria-expanded="false"
aria-controls="УНИКАЛЬНЫЙ-ID"
>
<span class="case-media-tall__btn-chev" aria-hidden="true"></span>
<span data-case-tall-label-more>Развернуть</span>
<span data-case-tall-label-less hidden>Свернуть</span>
</button>
</div>
</div>
<figcaption>…</figcaption>
</figure>
```
Живой пример: [`src/pages/cases/template-elements-demo.html`](src/pages/cases/template-elements-demo.html) (секция «Длинный скрин»); второй кейс — [`src/pages/cases/ai-platform-primo-rpa.html`](src/pages/cases/ai-platform-primo-rpa.html).
### Галерея изображений сеткой (`case-media-gallery`)
Сетка из миниатюр с тем же полноэкранным просмотром, что у одиночных картинок (`data-image-viewer` на каждом `<img>`). Стили в `public/css/main.css` (блок `.case-media-gallery`).
**Разметка:**
- Обертка: `figure.case-media.case-media-gallery` (+ опционально `case-media-gallery--wide`).
- Внутри: `div.case-media-gallery__grid` → для каждой ячейки `div.case-media-gallery__item` → `<img … data-image-viewer />`.
- Число колонок на **широком** экране задаётся на `figure`, например: `style="--case-gallery-cols: 3"` (целое ≥ 1). Если атрибут не указан, по умолчанию в CSS задано `3`.
- **Ширина:** без модификатора — как обычный блок в колонке статьи; класс **`case-media-gallery--wide`** — выход за колонку по той же схеме, что у `case-media--wide` (viewport с боковыми отступами). На узких экранах (`max-width: 640px`) wide-галерея схлопывается в ширину колонки.
- **Адаптив сетки:** примерно до `48rem` — не больше двух колонок (`min(2, var(--case-gallery-cols))`); до `28rem` — одна колонка.
Пример целиком см. в [`src/pages/cases/template-elements-demo.html`](src/pages/cases/template-elements-demo.html) (два варианта: контентная ширина и wide).
**Сетка + отложенный `src`:** в ячейках можно задать реальный URL в **`data-src`** и плейсхолдер в `src` (крошечный `data:image/gif;base64,…`), чтобы [`initCaseSequentialImages`](public/js/modules/cases/sequential-images.js) подставлял картинки **по порядку в DOM** (в т.ч. строка за строкой в галерее). Пока у `img` есть `data-src`, просмотр по клику не открывается. Пример: [`src/pages/cases/lunacy.html`](src/pages/cases/lunacy.html).
### Слайдер изображений с точками (`case-media-slider`)
Одна картинка во весь блок, навигация стрелками «пред/след» и пагинация точками (активная — акцентным цветом). Переиспользует маски стрелок `shevron-left/right.svg` и токены темы. Логика: [`public/js/modules/cases/media-slider.js`](public/js/modules/cases/media-slider.js) (`initCaseMediaSlider()` вызывается из [`public/js/main.js`](public/js/main.js)).
**Возможности:**
- стрелки `data-slider-prev` / `data-slider-next` — переключают соседний слайд, блокируются (`disabled`) на краях;
- точки `data-slider-dot` (в `data-slider-dots`) — переход на конкретный слайд, активная получает `is-active` + `aria-selected`;
- клавиатура: ←/→ внутри блока, Home/End на точках;
- тач/мышь-свайп (только горизонтальный);
- ленивая подгрузка соседних кадров: у `<img>` можно задать `data-src` (временный `src` — прозрачный GIF), скрипт подставит при подходе, как у галереи; пока есть `data-src`, клик по viewer не открывается;
- клик по активному кадру открывает общий viewer кейса (`data-image-viewer`).
**Разметка** (число `data-slider-dot` должно равняться числу `.case-media-slider__slide`; `aria-label` точек — «Перейти к изображению N»):
```html
<figure class="case-media case-media-slider" 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/<папка>/<файл-1>.webp" alt="…" loading="lazy" decoding="async" data-image-viewer />
</div>
<div class="case-media-slider__slide">
<img src="../assets/<папка>/<файл-2>.webp" alt="…" loading="lazy" decoding="async" data-image-viewer />
</div>
</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>
</div>
</figure>
```
Живой пример: [`src/pages/cases/template-elements-demo.html`](src/pages/cases/template-elements-demo.html) (секция «Слайдер»).
### Последовательная подгрузка (`data-case-sequential-images`)
- Оберни фрагмент статьи с картинками (часто весь `.case-article`): **`data-case-sequential-images`** на контейнере.
- У каждого кадра: **`data-src="../assets/…/файл.webp"`** и временный **`src`** — прозрачный GIF 1×1 (тот же паттерн, что в Lunacy-кейсе).
- Логика: [`public/js/modules/cases/sequential-images.js`](public/js/modules/cases/sequential-images.js), вызов **`initCaseSequentialImages()`** в [`public/js/main.js`](public/js/main.js) **до** `initCaseImageViewer()`.
- Просмотр: [`public/js/modules/cases/image-viewer.js`](public/js/modules/cases/image-viewer.js) слушает клики **делегированием**; не открывает плейсхолдер и не открывает, пока не снят `data-src`.
### Callout
```html
<aside class="case-callout" data-callout>
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
<div class="case-callout__content">
<p>…</p>
</div>
</aside>
```
- **Ширина:** как у wide-медиа — блок шире колонки статьи (`min(100vw − 2×padding, content + 8rem)`), на экранах до 640px — ширина колонки.
- **Иконка:** в `.case-callout__emoji` — цветные emoji (📌 💡 ✨ ✅ ⭐), не текстовые символы (✎ ✓ ◆ →); рендер через emoji-шрифт в `main.css`.
- **Типографика:** текст в `.case-callout__content` чуть крупнее основного абзаца (lead).
- Живой пример: [`src/pages/cases/design-system.html`](src/pages/cases/design-system.html) (секция Callout).
### Видео
- Обертка: `<figure class="case-media">` (часто с inline-отступами как в template) → `<div class="case-video-shell">` → `<video controls preload="metadata" playsinline … data-case-video-player>`.
- Подпись `<figcaption>` под видео — по центру колонки (стили в `main.css`).
- Видео в карточке и в fullscreen — `object-fit: contain` (без обрезки); fullscreen через Plyr (`iosNative: false`), не нативные `controls` на `<video>`.
- Постер: WebP в `poster`.
- Источники: `<source type="video/webm">`, затем `video/mp4`.
Про конвертацию и постеры: `docs/video-conversion.md`.
## Оптимизация изображений
PNG/JPEG → WebP по папке: `docs/image-optimization.md`, скрипт `scripts/optimize-images.mjs`.
## Связь с JS
Подключаемые через `{{> head-links}}` сценарии уже инициализируют:
- последовательную подгрузку `data-src` внутри `[data-case-sequential-images]` (`initCaseSequentialImages`);
- просмотр картинок по клику (`data-image-viewer`, делегирование на документ); при открытии из **галереи** (`.case-media-gallery`) или **слайдера** (`.case-media-slider`) в нижней панели просмотрщика появляются **кнопки-пилюли «пред/след»** и **точки-пагинация** — переключают только картинки этого блока (клавиши ←/→, свайп по горизонтали, клик по точке); у одиночной картинки кнопки и точки скрыты;
- плеер видео кейса (`data-case-video-player`);
- натуральный размер для `.case-media--natural img`;
- сворачивание длинного скрина (`[data-case-tall-media]`, `initCaseTallMediaExpand`).
Новые интерактивные паттерны — по правилам из `docs/frontend-js-architecture.md` (модуль в `public/js/modules/cases/`, вызов из `main.js`).
## Чеклист нового кейса
1. Скопировать `template-elements-demo.html` → `src/pages/cases/<slug>.html`, заменить `meta`, `title`, контент.
2. Положить медиа в **`public/assets/<slug>/`** (см. `public/assets/README.md` и `docs/image-optimization.md`).
3. Проверить все пути `../assets/…` и `../css/…`.
4. Выполнить `npm run build:pages` и открыть `public/cases/<slug>.html` локально.
5. При необходимости завести текстовый слепок в `texts/cases/<slug>.md` и ссылку на кейс в `src/partials/projects-section.html` после сборки главной.