- 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 при повторном открытии
242 lines
20 KiB
Markdown
242 lines
20 KiB
Markdown
# Страница кейса: каркас и блоки контента
|
||
|
||
Инструкция завязана на эталон **`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` после сборки главной.
|