slidev-theme-practicum 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +225 -22
  2. package/components/Slide.vue +12 -58
  3. package/components/Slot.vue +4 -3
  4. package/components/StepsGrid.vue +117 -0
  5. package/components/Text.vue +5 -0
  6. package/composables/deck-decors.ts +74 -0
  7. package/composables/deck-slot-markup.cjs +19 -5
  8. package/composables/decor-sources.ts +43 -0
  9. package/composables/layout-authoring.ts +34 -9
  10. package/composables/layout-recipes.ts +16 -2
  11. package/composables/layout-shorthands.ts +157 -83
  12. package/composables/local-layout-variant-files.ts +106 -0
  13. package/composables/local-layout-variants.ts +73 -0
  14. package/composables/slide-layout.ts +3 -0
  15. package/composables/text-fit-runtime.ts +7 -3
  16. package/composables/theme-foundation.ts +7 -3
  17. package/composables/typography-guard.cjs +59 -0
  18. package/composables/use-theme-config.ts +17 -10
  19. package/composables/validate-deck-layouts.cjs +95 -7
  20. package/composables/validate-deck-typography.cjs +101 -0
  21. package/env.d.ts +18 -0
  22. package/example.md +1 -1
  23. package/package.json +18 -6
  24. package/scripts/browser-smoke.mjs +85 -23
  25. package/scripts/check-accessibility.mjs +364 -0
  26. package/scripts/check-consumer.mjs +159 -0
  27. package/scripts/check-local-layout-variant-build.mjs +189 -0
  28. package/scripts/check-package.mjs +18 -2
  29. package/scripts/check-pixels.mjs +251 -0
  30. package/scripts/check-typography.mjs +108 -0
  31. package/scripts/requirements-illustrations.txt +1 -0
  32. package/scripts/test-typography.mjs +89 -0
  33. package/scripts/trace-line-art.py +422 -0
  34. package/scripts/typography-browser.mjs +134 -0
  35. package/scripts/validate-deck.cjs +23 -3
  36. package/setup/vite-plugins.ts +109 -2
  37. package/skills/slidev-practicum/SKILL.md +19 -4
  38. package/skills/slidev-practicum/references/contour-illustrations.md +114 -0
  39. package/skills/slidev-practicum/references/deck-project-structure.md +136 -0
  40. package/skills/slidev-practicum/references/illustration-examples/balance-scales.png +0 -0
  41. package/skills/slidev-practicum/references/illustration-examples/balance-scales.svg +88 -0
  42. package/skills/slidev-practicum/references/illustration-examples/chainsaw.png +0 -0
  43. package/skills/slidev-practicum/references/illustration-examples/chainsaw.svg +4 -0
  44. package/skills/slidev-practicum/references/illustration-examples/graduation-cap.png +0 -0
  45. package/skills/slidev-practicum/references/illustration-examples/graduation-cap.svg +23 -0
  46. package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.png +0 -0
  47. package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.svg +4 -0
  48. package/skills/slidev-practicum/references/photographic-illustrations.md +100 -0
  49. package/styles/index.css +20 -27
  50. package/styles/vars.css +6 -2
package/README.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  1. Откройте [example.md](example.md) и найдите слайд, похожий по задаче.
8
8
  2. Скопируйте весь блок слайда от `---` до следующего `---`.
9
- 3. Замените заголовок, текст, пункты или числа. Технические поля лучше не трогать, пока не станет понятно, за что они отвечают.
10
- 4. Проверьте, что смысл слайда читается без фотографии и декоративных элементов.
9
+ 3. Замените видимый заголовок и остальной текст в теле слайда, затем пункты или числа. Технические поля лучше не трогать, пока не станет понятно, за что они отвечают.
10
+ 4. Проверьте, что тезис, факт, инструкция и вывод читаются без фотографии и декоративных элементов.
11
11
  5. Если подходящего примера нет, сначала выберите задачу слайда по таблице ниже.
12
12
 
13
13
  ## Агентный скилл
@@ -36,6 +36,64 @@ npm exec -- slidev-practicum-validate path/to/slides.md
36
36
 
37
37
  В `example.md` у канонических слайдов в заметках автора (`<!-- Контракт … -->`) описано, сколько пунктов и какой вложенности ждёт каждый variant.
38
38
 
39
+ ## Строгая проверка типографики
40
+
41
+ Режим включается явно. Обычная проверка и внешний вид существующих макетов сохраняются. Галерея показывает весь API темы, поэтому её варианты с `muted` и несколькими уровнями текста могут не проходить строгую политику.
42
+
43
+ Правила для содержательного текста слайда:
44
+
45
+ - Не более двух размеров обычного текста, включая заголовок, подписи, даты и номера шагов.
46
+ - Третий размер разрешён только для явно отмеченных ключевых чисел: `<Text as="data" size="9">42%</Text>`. В этом режиме `as="data"` означает утверждение автора, что это ключевое число. Все такие числа на слайде должны иметь один размер. Содержимое не проверяется на наличие цифр.
47
+ - В каждом `Slot` один цвет текста, без `muted` и токенов `text-muted*`. Между разными слотами цвета могут различаться.
48
+ - Текст должен помещаться в границах содержимого слота. При переполнении измените текст или композицию.
49
+ - Шапка, номер слайда в шапке, логотипы, фоновые и SVG-иллюстрации исключены. Содержательные подписи к иллюстрациям учитываются.
50
+
51
+ Пример явной композиции: два обычных размера `5` и `3`, отдельный размер `7` для ключевого числа, один основной цвет слота.
52
+
53
+ ```md
54
+ ---
55
+ layout: message
56
+ variant: centered
57
+ ---
58
+
59
+ <Slot role="primary">
60
+ <Text as="h1" size="5">Результат эксперимента</Text>
61
+ <Text as="data" size="7">42%</Text>
62
+ <Text size="3">Участники завершили практическое задание</Text>
63
+ </Slot>
64
+ ```
65
+
66
+ Проверка объявленных атрибутов:
67
+
68
+ ```bash
69
+ npm exec -- slidev-practicum-validate slides.md --typography-guard
70
+ ```
71
+
72
+ Она считает только явно объявленные `Text`, сравнивает токены цветов внутри `Slot` и ловит запрещённый `muted`. Диапазон `size="7-8"` считается одной политикой подбора, `max-size` — политикой `0-12`. Литеральные привязки вроде `:size="7"` и `:muted="false"` поддерживаются. Динамические размеры, цвета, теги, стили и условные ветви получают сообщение о невозможности статической проверки. Они не подменяются значениями по умолчанию.
73
+
74
+ Для итоговой проверки сначала соберите эту же колоду, затем проверьте результат в Chromium:
75
+
76
+ ```bash
77
+ npm install -D playwright-chromium
78
+ npm exec -- slidev build slides.md --out dist
79
+ npm exec -- slidev-practicum-check-typography slides.md --dist dist
80
+ ```
81
+
82
+ Команда ожидает загрузки шрифтов и стабильного подбора текста, проверяет вычисленные размеры и цвета видимых текстовых фрагментов, затем их выход за границы слотов. Учитываются также вложенная разметка, ссылки и текст, созданный сокращёнными Markdown-макетами и компонентами темы. Разные токены одного вычисленного цвета в браузере объединяются. Сообщения содержат номер слайда, фрагмент текста и найденные значения. Нарушение или ошибка отображения дают код выхода `1`.
83
+
84
+ Используйте свежую сборку указанного файла с корнем сайта `/`. Поддерживаются маршруты `hash` и `history`. Проверяется начальное видимое состояние каждого слайда, без перебора кликов, анимаций и интерактивных состояний. Включения слайдов через `src` пока явно отклоняются. Статический результат не заменяет браузерный. При динамических атрибутах итоговый вердикт даёт браузерная команда для показанного состояния.
85
+
86
+ Порядок работы агента:
87
+
88
+ 1. Включайте режим, когда он выбран для колоды. При копировании примера из галереи проверяйте его по строгим правилам: сама галерея не гарантирует их соблюдение.
89
+ 2. Запустите статическую проверку, затем сборку и браузерную проверку. Если строгая статическая проверка не может вычислить динамические атрибуты, отдельно запустите обычный `slidev-practicum-validate` через `npm exec --` для проверки макетов, а типографику подтвердите браузерной командой. В отчёте укажите эту границу проверки.
90
+ 3. Исправляйте замечания в колоде. Если сокращённый макет создаёт `muted` или лишние размеры, используйте явные `Slot` и `Text` либо локальный вариант по контракту темы. Не редактируйте установленный пакет и не маскируйте замечание дополнительным мелким размером.
91
+ 4. После исправлений повторите проверку объявлений, пересоберите колоду и проверьте новую сборку. Успех — отсутствие диагностик в применимых проверках и код выхода `0`. Интерактивные состояния проверьте отдельно.
92
+
93
+ Если команда или флаг отсутствуют, проверьте установленную ревизию `slidev-theme-practicum`: она должна содержать этот режим. Обновите зависимость на нужную ревизию перед проверкой. Отсутствие инструмента не означает, что колода прошла проверку.
94
+
95
+ В репозитории темы доступны `npm run check-typography -- slides.md --dist dist` и `npm run test:accessibility -- --typography-guard`. Последняя команда проверяет готовую сборку галереи `example.md` с дополнительной строгой политикой. Автоматические проверки самого механизма запускаются через `npm run test:typography` и входят в `npm test`.
96
+
39
97
  Для использования в другой Slidev-презентации:
40
98
 
41
99
  ```bash
@@ -46,6 +104,36 @@ npm install -D slidev-theme-practicum
46
104
  theme: practicum
47
105
  ```
48
106
 
107
+ Тема не загружает шрифты из сети. Если `YS Text` установлен в системе, браузер использует его локально. Иначе стек последовательно переходит к локальному `Inter`, системному интерфейсному шрифту и `sans-serif`. Поэтому сборка и показ остаются офлайн-воспроизводимыми, но метрики текста без `YS Text` могут немного отличаться. Перед выпуском проверяйте галерею тем же окружением Chromium, которым сформированы пиксельные эталоны.
108
+
109
+ ## Структура проекта колоды
110
+
111
+ Для презентации, которая использует установленную тему, каноническая точка входа — `slides.md`, а локальные медиа лежат в `public/` и подключаются абсолютным путём от корня сайта:
112
+
113
+ ```text
114
+ <deck>/
115
+ ├── slides.md
116
+ ├── decors.yaml # необязательный внешний каталог декора
117
+ ├── package.json
118
+ ├── package-lock.json
119
+ ├── pages/ # разделы большой колоды
120
+ ├── components/ # только компоненты этой колоды
121
+ │ └── layout-variants/ # локальные варианты тематических layout
122
+ ├── layouts/ # новые нативные layout самой презентации
123
+ ├── snippets/ # импортируемые примеры кода
124
+ ├── styles/index.css # локальные глобальные переопределения
125
+ ├── public/
126
+ │ ├── decor/ # графический декор
127
+ │ ├── photos/ # фотографии
128
+ │ ├── illustrations/ # контурные предметы, парные PNG/SVG
129
+ │ └── figures/ # схемы, графики и снимки интерфейса
130
+ └── reference/ # исходники и источники, не входящие в сборку
131
+ ```
132
+
133
+ Создавайте необязательные каталоги только при появлении содержимого. Не заводите параллельные `assets/`, `images/` или `img/`. Фотография остаётся в `photos`, даже если служит фоном; контурный предмет остаётся в `illustrations`, даже если используется как декор. Для файла колоды пишите `/photos/team-workshop.webp`, а не `public/photos/...` и не `/theme/photos/...`. Префикс `/theme` принадлежит только встроенным файлам темы.
134
+
135
+ Полный контракт структуры, классификации, именования и проверки закреплён в агентном справочнике [deck-project-structure.md](skills/slidev-practicum/references/deck-project-structure.md). Производственный контур сюжетных фотографий описан в [photographic-illustrations.md](skills/slidev-practicum/references/photographic-illustrations.md), а протокол создания парных PNG/SVG для контурных предметов — в [contour-illustrations.md](skills/slidev-practicum/references/contour-illustrations.md).
136
+
49
137
  ## Как выбрать слайд
50
138
 
51
139
  Выбирайте не по теме презентации, а по задаче кадра.
@@ -69,6 +157,8 @@ theme: practicum
69
157
 
70
158
  Заголовок должен отвечать на вопрос «что зритель должен понять сейчас?». Не называйте слайд технически вроде «Метрики» или «Композиция», если можно сразу написать вывод.
71
159
 
160
+ В первом headmatter верхнеуровневое поле `title` разрешено: это служебное название всей колоды для HTML-документа и метаданных Slidev, а не содержимое первого слайда. Видимый заголовок всё равно пишите в теле: как `# …` для сокращённой записи или как `<Text as="h1">…</Text>` для явной композиции, включая `cover`. Со второго слайда верхнеуровневое поле `title` запрещено, чтобы метаданные не принимали за отображаемый текст. Вложенные поля `items[].title`, `comparison.from/to.title` и `person.title` остаются частью видимых моделей компонентов и не относятся к этому запрету.
161
+
72
162
  Поясняющий текст нужен для контекста, ограничения или критерия выбора. Если текст превращается в два разных вывода, разнесите его по двум слайдам.
73
163
 
74
164
  Список работает, когда пункты однотипны: шаги, правила, критерии, темы. Пишите пункты в одинаковой грамматической форме, чтобы их можно было быстро просканировать.
@@ -77,15 +167,19 @@ theme: practicum
77
167
 
78
168
  Цитата должна быть короткой. Авторство добавляет источник, но не должно конкурировать с самой фразой.
79
169
 
80
- ## Семантика декоративных фотографий
170
+ ## Семантические роли изображений
81
171
 
82
- Фотографии в шаблонах Практикума всегда декоративные. Они не являются источником фактов, доказательством, инструкцией или объектом, который зритель должен рассматривать ради ключевой информации.
172
+ Роль изображения определяется не форматом файла и не компонентом, а тем, что зритель должен из него понять.
83
173
 
84
- Смысл слайда должен полностью читаться из текста, чисел, подписей, порядка элементов и выбранной композиции. Если фотографию убрать или заменить другой фотографией, вывод слайда не должен измениться.
174
+ | Роль | Назначение | Требование к смыслу |
175
+ |---|---|---|
176
+ | Декоративная фотография | Настроение, ритм, плотность, брендовый характер или пауза | Взаимозаменяема: если её убрать или заменить, вывод слайда не изменится |
177
+ | Сюжетная фотографическая иллюстрация | Конкретная метафора, действие или сквозные персонажи | Не взаимозаменяема внутри сюжета, но не является единственным носителем факта, инструкции или вывода |
178
+ | Информационная фигура | Схема, график, интерфейс, сравнение или доказательство, которое нужно рассмотреть | Существенные данные и вывод продублированы доступной подписью или текстом слайда |
85
179
 
86
- Фото может задавать настроение, визуальный ритм, плотность, брендовый характер или паузу между текстовыми блоками. Оно не должно объяснять метрику, доказывать тезис, заменять подпись или содержать единственный важный контекст.
180
+ Встроенные фотографии темы и выбор через `decor` всегда декоративные. Колода может добавлять собственные сюжетные фотографии в `public/photos/` и размещать их через `Image` или `Slot.background`. В таком случае конкретный сюжетный смысл должен быть назван заголовком, текстом, подписью или доступным описанием: фотография поддерживает рассказ, но не заменяет его.
87
181
 
88
- Не пишите в примерах и документации: «на фото видно», «фото доказывает», «снимки дали контекст», «фотография объясняет число». Такие формулировки ошибочно превращают декоративный слой в информационный.
182
+ Не пишите о декоративной фотографии: «фото доказывает» или «фотография объясняет число». Если зритель действительно должен рассмотреть данные, интерфейс или причинно-следственную схему, это информационная фигура из `public/figures/`, а не декоративный слой.
89
183
 
90
184
  ## Живая галерея
91
185
 
@@ -107,10 +201,88 @@ layout: cover | message | explainer | collection | none
107
201
 
108
202
  Для `layout: message`, `explainer` и `collection` часть `variant` принимает markdown в default slot слайда (`#` заголовок, списки, blockquote, frontmatter) — тема разворачивает его в `Slot` / `Text`. Реестр: `composables/layout-shorthands.ts`.
109
203
 
204
+ Для `message:centered` тема выбирает самый крупный помещающийся размер заголовка в диапазоне крупных токенов `7-12`.
205
+
110
206
  Для `message:closing` обязателен один заголовок `# …`; под ним можно добавить один необязательный абзац. Дополнительные абзацы, списки и изображения не поддерживаются.
111
207
 
112
208
  `layout: cover` markdown shorthand **не** имеет: для всех `cover:*` нужны явные `<Slot role="...">` и `<Text>` (см. обложки в [example.md](example.md)).
113
209
 
210
+ Сокращённая Markdown-запись и явные `<Slot role="...">` — два взаимоисключающих режима авторинга одного встроенного варианта. Если в основном слоте есть явный ролевой `Slot`, тема использует ролевые компоненты как готовое содержимое и не разворачивает `items`, `comparison` и другие поля сокращённой записи. Исключение — `collection:agenda`, где разрешён один явный `Slot role="media"` как переопределение иллюстрации. Чтобы `items[].title` или `comparison.from/to.title` попали в визуальный результат, оставьте во входе только поля front matter и канонический Markdown-заголовок; не дублируйте те же данные в `<Text>`.
211
+
212
+ ### Локальные варианты презентации
213
+
214
+ Slidev уже автоматически подключает Vue-компоненты из `components/` конечной презентации. Их можно использовать тегами прямо в `slides.md`. Локальный вариант нужен для другого случая: повторяющаяся композиция, или архетип, выбирается привычной парой `layout` + `variant`, а содержимое слайда остаётся обычным Markdown без Vue-тегов.
215
+
216
+ Файл лежит внутри каталога тематического `layout`. Например, `components/layout-variants/explainer/lesson-summary.vue` соответствует `layout: explainer` и `variant: lesson-summary`:
217
+
218
+ ```text
219
+ <deck>/
220
+ ├── slides.md
221
+ ├── components/
222
+ │ ├── CourseBadge.vue
223
+ │ └── layout-variants/
224
+ │ └── explainer/
225
+ │ └── lesson-summary.vue
226
+ └── layouts/
227
+ └── workshop.vue
228
+ ```
229
+
230
+ ```md
231
+ ---
232
+ layout: explainer
233
+ variant: lesson-summary
234
+ badge:
235
+ text: Практика
236
+ ---
237
+
238
+ # Что запомнить
239
+
240
+ - Компоненты принадлежат презентации
241
+ - Локальный вариант выбирается через `layout` и `variant`
242
+ ```
243
+
244
+ Vue-файл получает отдельную копию `frontmatter`, защищённую от записи на верхнем уровне, значения `layout` и `variant`, а также разобранный Markdown через основной слот:
245
+
246
+ ```vue
247
+ <script setup lang="ts">
248
+ import type {
249
+ DeckLayoutVariantProps,
250
+ DeckLayoutVariantSlots,
251
+ } from 'slidev-theme-practicum/composables/local-layout-variants'
252
+
253
+ type LessonSummaryFrontmatter = {
254
+ badge?: {
255
+ text?: string
256
+ }
257
+ }
258
+
259
+ defineProps<DeckLayoutVariantProps<LessonSummaryFrontmatter>>()
260
+ defineSlots<DeckLayoutVariantSlots>()
261
+ </script>
262
+
263
+ <template>
264
+ <Slot area="1 / 1 / 9 / 9" surface="light" margin="4" gap="3">
265
+ <slot />
266
+ </Slot>
267
+
268
+ <Slot area="9 / 9 / -1 / -1" surface="color" margin="3">
269
+ <CourseBadge :text="frontmatter.badge?.text ?? variant" />
270
+ </Slot>
271
+ </template>
272
+ ```
273
+
274
+ Компоненты темы (`Slot`, `Text`, `Image`, `Person` и другие) и остальные компоненты презентации внутри такого файла доступны по обычным правилам автоматического подключения Slidev. Корневые `Slot` локального варианта используют ручные `area`, `col` или `row`: поле `role` принадлежит только встроенным рецептам темы.
275
+
276
+ Правила выбора намеренно строгие:
277
+
278
+ - первый каталог — один из тематических `cover`, `message`, `explainer` или `collection`, а имя Vue-файла и значение `variant` пишутся в `kebab-case`;
279
+ - встроенный вариант темы нельзя затереть локальным файлом: для локального архетипа выбирайте новое имя `variant`;
280
+ - `arrangement` нельзя добавлять к локальному варианту — дополнительные параметры получают собственные поля front matter;
281
+ - неизвестный `variant` останавливает просмотр, сборку и экспорт с подсказкой до ожидаемого файла;
282
+ - для принципиально нового типа слайда используйте нативный механизм Slidev: `layouts/workshop.vue` и `layout: workshop`. Такой layout не проходит через варианты темы.
283
+
284
+ `slidev-practicum-validate` проверяет имя, наличие локального варианта и допустимое сочетание полей front matter. Типы и шаблон конкретного Vue-файла дополнительно проверяются обычной типизацией и сборкой презентации.
285
+
114
286
  ### Варианты collection P0
115
287
 
116
288
  | `variant` | `arrangement` | Контракт |
@@ -118,7 +290,7 @@ layout: cover | message | explainer | collection | none
118
290
  | `comparison` | `before-after` | Markdown heading; `comparison.from` и `comparison.to` с обязательным `title`; необязательные `kicker`, `body` и `relation.label` |
119
291
  | `comparison` | `stable-variable` | Тот же контракт; вертикальное направление задаёт arrangement |
120
292
  | `steps` | `linear` | Markdown heading; `items` из 3–6 объектов с обязательным `title`, необязательными `body`, `label`; не больше одного `active: true` |
121
- | `steps` | `staggered` | Тот же контракт, но ровно 5 элементов |
293
+ | `steps` | `staggered` | Тот же контракт, но от 2 до 6 элементов; label сверху, title и body снизу |
122
294
  | `metrics` | `dashboard` | Markdown heading; ровно 5 `metrics` с обязательными `value` и одним из `body`, `label`, `title`; без `media` и произвольных spans |
123
295
  | `facts` | `numbered-quartet` | Markdown heading; ровно 4 пункта вида «значение → вложенная подпись»; номера 1–4 добавляет тема |
124
296
 
@@ -206,12 +378,14 @@ arrangement: numbered-quartet
206
378
  | `arrangement` | компактная перестановка одной модели данных |
207
379
  | `Slot` | область сетки, поверхность, отступы и медиа-слой |
208
380
  | `Text` | типографика и согласованный подбор размера |
209
- | `Image` | декоративная картинка внутри ручной композиции |
381
+ | `Image` | изображение внутри ручной композиции; роль задаёт автор |
210
382
  | `decor` | семантический выбор декоративного изображения |
211
- | `background` | низкоуровневое размещение декоративной картинки |
383
+ | `background` | низкоуровневое размещение изображения |
212
384
 
213
385
  ## Метаданные слайда
214
386
 
387
+ Первый headmatter может содержать верхнеуровневый `title` как служебное название всей колоды. На остальных слайдах валидатор отклоняет этот ключ; основной текст каждого кадра должен находиться в теле слайда.
388
+
215
389
  | Поле | Значения | По умолчанию | Смысл |
216
390
  | ------------- | ----------------------------------------------------- | --------------- | ---------------------------------------- |
217
391
  | `layout` | `cover`, `message`, `explainer`, `collection`, `none` | значение Slidev | роль слайда или ручная сетка |
@@ -222,6 +396,10 @@ arrangement: numbered-quartet
222
396
  | `header` | `default`, `cover`, `none` | задаёт макет | служебный верхний слой |
223
397
  | `decor` | объект: `id` или семантический запрос | нет | декоративный слой для сокращённой записи |
224
398
 
399
+ Пакет объявляет `slidev.colorSchema: light`: системное переключение светлой и тёмной темы не поддерживается. Значения `mode: dark` и `mode: color` — проверенные семантические варианты отдельных слайдов внутри одной цветовой схемы, а не альтернативные карты для `prefers-color-scheme`. Тёмный и акцентный варианты используют светлый текст, логотип и служебные элементы.
400
+
401
+ Белый текст на ярких фирменных фонах сохраняется как осознанное визуальное правило. На оранжевом, зелёном и особенно жёлтом тоне отдельные пары не достигают порогов WCAG. Автоматическая проверка продолжает измерять их и выводит число `акцентные исключения`, но разрешает низкий контраст только внутри `mode: color`, `surface: color` и активного шага. Во всех остальных контекстах недостаточный контраст остаётся ошибкой.
402
+
225
403
  ## Slot API
226
404
 
227
405
  | Свойство | Тип | По умолчанию | Смысл |
@@ -259,7 +437,7 @@ arrangement: numbered-quartet
259
437
 
260
438
  В `Text` можно писать блочный Markdown темы (списки, абзацы, цитаты) — не полный GFM Slidev. Это не layout shorthand: на `cover:*` по-прежнему нужны явные `Slot` / `Text`.
261
439
 
262
- Inline-разметка `**жирный**` сохраняется как `<strong>` и отображается начертанием YS Text Bold с весом `600`.
440
+ Inline-разметка `**жирный**` сохраняется как `<strong>` и отображается доступным локальным полужирным начертанием с весом `600`.
263
441
 
264
442
  Slidev парсит markdown внутри компонента только если контент **отделён пустой строкой** от открывающего и закрывающего тега (как в [доке Slidev](https://sli.dev/builtin/components)). Без пустых строк список схлопнется в одну строку и сломается. Не добавляйте лишний отступ в 4 пробела у пунктов — Slidev превратит блок в code.
265
443
 
@@ -293,7 +471,8 @@ Slidev парсит markdown внутри компонента только ес
293
471
 
294
472
  | Свойство или поле | Тип | По умолчанию | Смысл |
295
473
  | ------------------ | ------------------------------------------------ | ------------ | --------------------------------- |
296
- | `src` | путь | обязательно | исходное декоративное изображение |
474
+ | `src` | путь | обязательно | исходное изображение |
475
+ | `alt` | строка | пусто | доступное описание недекоративного изображения |
297
476
  | `fit` | `cover`, `contain`, `fill`, `none`, `scale-down` | `cover` | режим заполнения |
298
477
  | `position` | позиция CSS | `center` | позиция изображения |
299
478
  | `zoom` | число | `1` | масштаб |
@@ -321,7 +500,7 @@ Slidev парсит markdown внутри компонента только ес
321
500
 
322
501
  Встроенные файлы темы доступны по префиксу `/theme`: например, `/theme/photos/photo-6.webp` и `/theme/decor/decor-10.svg`. Файлы из `public` самой колоды остаются пользовательскими и задаются от корня, например `/decor/custom-data.png`; префикс `/theme` к ним добавлять не нужно.
323
502
 
324
- Даже когда используется `Image` или `Slot.background`, фотография остаётся декоративной. Ключевые факты, различия, инструкции и выводы должны быть записаны текстом или числом.
503
+ `Image` и `Slot.background` не определяют семантическую роль. Встроенные `/theme/photos/...` и выбор через `decor` остаются декоративными; собственная фотография колоды из `/photos/...` может быть сюжетной. Для сюжетного `Image` заполняйте `alt`. Фон `Slot.background` скрыт от вспомогательных технологий, поэтому его смысл обязательно дублируется видимым заголовком, текстом или подписью. Ключевые факты, различия, инструкции и выводы в любом случае должны быть записаны текстом, числом или доступной подписью.
325
504
 
326
505
  ### Decor
327
506
 
@@ -358,11 +537,18 @@ themeConfig:
358
537
  tone: blue
359
538
  ```
360
539
 
540
+ ```yaml
541
+ themeConfig:
542
+ decors: ./decors.yaml
543
+ ```
544
+
361
545
  ```yaml
362
546
  themeConfig:
363
547
  deckTitle: 'Название колоды'
364
548
  debugGrid: false
365
549
  decors:
550
+ - ./decors/clocks.yaml
551
+ - ./decors/photos.yaml
366
552
  - id: decor-custom-data
367
553
  src: /decor/custom-data.png
368
554
  meaning: data
@@ -372,14 +558,21 @@ themeConfig:
372
558
  ratio: [1.2, 2.7]
373
559
  ```
374
560
 
561
+ `decors` принимает список записей, путь к файлу или смешанный список путей и записей. Файл может быть YAML, JSON или ESM (`decors.yaml`, `.yml`, `.json`, `.mjs`) и содержать массив записей, объект `{ decors: [...] }` или одну запись. Если `decors` не задан, тема подхватывает `decors.yaml` / `.yml` / `.json` / `.mjs` в корне колоды, если такой файл есть. Позже идущие записи с тем же `id` переопределяют более ранние.
562
+
563
+ Файл каталога по умолчанию **заменяет** встроенный каталог темы: слоты видят только записи колоды. Короткие inline-записи по умолчанию по-прежнему добавляются к встроенному каталогу. Явный `replaceDecors` перекрывает оба случая.
564
+
375
565
  В записи каталога `cols` и `rows` описывают допустимый размер слота в сетке 12x12, а `ratio` ограничивает соотношение `cols / rows` для ориентации. Вместо `ratio` можно использовать более явный алиас `aspectRatio`.
376
566
 
377
- | Поле | Тип | По умолчанию | Смысл |
378
- | ----------------- | --------------- | ------------------ | -------------------------------------------------------- |
379
- | `deckTitle` | строка | заголовок слайда | заголовок в шапке |
380
- | `debugGrid` | boolean | `false` | отладочная сетка 12x12 |
381
- | `decors` | записи каталога | встроенный каталог | добавляет или переопределяет записи декора по `decor.id` |
382
- | `decorSaveOrigin` | origin URL | origin dev-сервера | разрешённый origin для сохранения настроек декора |
567
+ | Поле | Тип | По умолчанию | Смысл |
568
+ | ----------------- | --------------------------------- | ------------------ | -------------------------------------------------------- |
569
+ | `deckTitle` | строка | пустая строка | служебное название колоды в шапке |
570
+ | `debugGrid` | boolean | `false` | отладочная сетка 12x12 |
571
+ | `decors` | записи, путь или список путей | встроенный каталог | файл заменяет встроенный каталог; inline-записи добавляют или переопределяют по `decor.id` |
572
+ | `replaceDecors` | boolean | `true` для файла каталога | не подмешивать встроенные картинки темы |
573
+ | `decorSaveOrigin` | origin URL | origin dev-сервера | разрешённый origin для сохранения настроек декора |
574
+
575
+ `themeConfig.deckTitle` задаётся явно и отвечает только за повторяющееся служебное название в шапке. Он не заменяет видимый заголовок конкретного слайда и не берётся из верхнеуровневого `title`.
383
576
 
384
577
  Если dev-сервер работает за reverse proxy с завершением TLS, задайте внешний origin явно:
385
578
 
@@ -398,7 +591,7 @@ Endpoint сохранения принимает только точное зн
398
591
  | `light-1` | `#f0f0f0` |
399
592
  | `dark-0` | `#1e1e1e` |
400
593
  | `dark-1` | `#3c3c3c` |
401
- | `dark-2` | `#969696` |
594
+ | `dark-2` | `#646464` |
402
595
  | `blue-0` | `#027ef2` |
403
596
  | `blue-1` | `#98d2fe` |
404
597
  | `orange-0` | `#ff6c26` |
@@ -418,6 +611,16 @@ npm test
418
611
 
419
612
  - lint и типы;
420
613
  - модульные, контрактные и архитектурные тесты;
421
- - продукционную сборку и состав её артефактов;
614
+ - продукционную сборку, локальный вариант внешней презентации и состав артефактов;
422
615
  - репрезентативные слайды в Chromium, переполнение холста и загрузку медиа по `/theme/...`;
423
- - состав и размер npm-пакета.
616
+ - контраст всех видимых текстовых пар, семантику медиа и `alt`, видимый фокус, `prefers-reduced-motion` и отсутствие внешних запросов;
617
+ - точное совпадение всех 47 слайдов `example.md` с PNG-эталонами при зафиксированных Chromium, viewport, DPR, sRGB и доступности локальных шрифтов;
618
+ - состав и размер npm-пакета, затем установку созданного локального архива во временную внешнюю презентацию, её сборку, показ без внешних запросов и ресурсы `/theme/...`.
619
+
620
+ ## Обновление пиксельных эталонов
621
+
622
+ Пиксельные эталоны меняются только осознанно после визуального просмотра результата:
623
+
624
+ ```bash
625
+ npm run test:pixels:update
626
+ ```
@@ -1,11 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, defineComponent, h, isVNode, onBeforeUnmount, onMounted, onUpdated, shallowRef, unref, useSlots, type PropType, type VNode } from 'vue'
3
3
  import { useSlideContext } from '@slidev/client'
4
+ import { DECK_LAYOUT_VARIANTS } from 'virtual:practicum-deck-layout-variants'
4
5
  import DebugGrid from './DebugGrid.vue'
5
6
  import Header from './Header.vue'
6
7
  import Image from './Image.vue'
7
8
  import Person from './Person.vue'
8
9
  import Slot from './Slot.vue'
10
+ import StepsGrid from './StepsGrid.vue'
9
11
  import Text from './Text.vue'
10
12
  import Timeline from './Timeline.vue'
11
13
  import { createSlideLayout } from '../composables/slide-layout'
@@ -47,9 +49,11 @@ const slideLayout = createSlideLayout({
47
49
  Image,
48
50
  Person,
49
51
  Slot,
52
+ StepsGrid,
50
53
  Text,
51
54
  Timeline,
52
55
  },
56
+ layoutVariants: DECK_LAYOUT_VARIANTS,
53
57
  })
54
58
 
55
59
  const layoutContractLabel = computed(() => {
@@ -60,12 +64,14 @@ const layoutContractLabel = computed(() => {
60
64
 
61
65
  function reportLayoutContractError(error: SlideMarkdownContractError) {
62
66
  const lines = [
63
- `[Slidev] Слайд ${$page ?? '?'}: ${layoutContractLabel.value}`,
67
+ `[Slidev] Слайд ${unref($page) ?? '?'}: ${layoutContractLabel.value}`,
64
68
  error.message,
65
69
  ]
66
70
  if (error.hint)
67
71
  lines.push(` Подсказка: ${error.hint}`)
68
- lines.push(' См. example.md в slidev-theme-practicum (блоки «Контракт»).')
72
+ lines.push(error.hint?.includes('components/layout-variants/')
73
+ ? ' См. README.md в slidev-theme-practicum, раздел «Локальные варианты презентации».'
74
+ : ' См. example.md в slidev-theme-practicum (блоки «Контракт»).')
69
75
  console.error(lines.join('\n'))
70
76
  }
71
77
 
@@ -89,6 +95,7 @@ function compileCurrentAuthoring(children?: readonly VNode[]) {
89
95
  const authored = computed(() => compileCurrentAuthoring())
90
96
  const resolvedLayout = computed(() => authored.value.layout)
91
97
  const isLayoutMode = computed(() => authored.value.mode === 'layout')
98
+ const isCompiledMode = computed(() => authored.value.mode !== 'manual')
92
99
  const resolvedVariant = computed(() => authored.value.variant)
93
100
  const resolvedHeader = computed(() => authored.value.header)
94
101
  const resolvedTheme = computed(() => authored.value.theme)
@@ -187,14 +194,14 @@ const slideClass = computed(() => ({
187
194
  :class="slideClass"
188
195
  :style="slideStyle">
189
196
  <div class="Slide-Header">
190
- <slot v-if="!isLayoutMode && slots.header" name="header" />
197
+ <slot v-if="!isCompiledMode && slots.header" name="header" />
191
198
  <Header v-else-if="resolvedHeader !== 'none'"
192
199
  :variant="resolvedHeader === 'cover' ? 'cover' : 'default'"
193
- :inverted="isContrast" />
200
+ :inverted="resolvedTheme.foreground === 'light'" />
194
201
  </div>
195
202
  <DebugGrid v-if="showDebugGrid" />
196
203
  <div class="Slide-Grid">
197
- <slot v-if="!isLayoutMode" />
204
+ <slot v-if="!isCompiledMode" />
198
205
  <LayoutBody v-else>
199
206
  <slot />
200
207
  </LayoutBody>
@@ -382,59 +389,6 @@ const slideClass = computed(() => ({
382
389
  line-height: var(--theme-text-line-5);
383
390
  }
384
391
 
385
- :deep(.Slide-StepsGrid) {
386
- display: grid;
387
- width: 100%;
388
- height: 100%;
389
- min-width: 0;
390
- min-height: 0;
391
- grid-template-columns: repeat(var(--slide-steps-count), minmax(0, 1fr));
392
- gap: var(--theme-grid-gap);
393
- }
394
-
395
- :deep(.Slide-Step) {
396
- display: flex;
397
- min-width: 0;
398
- min-height: 0;
399
- flex-direction: column;
400
- gap: calc(var(--theme-grid-module) * 2);
401
- padding: var(--theme-slot-margin-3);
402
- border-radius: var(--theme-panel-radius);
403
- background: var(--theme-surface-light);
404
- }
405
-
406
- :deep(.Slide-StepsGrid_staggered) {
407
- grid-template-columns: repeat(6, minmax(0, 1fr));
408
- grid-template-rows: repeat(2, minmax(0, 1fr));
409
- }
410
-
411
- :deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(1)) {
412
- grid-area: 1 / 1 / 2 / 3;
413
- }
414
-
415
- :deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(2)) {
416
- grid-area: 1 / 3 / 2 / 5;
417
- }
418
-
419
- :deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(3)) {
420
- grid-area: 1 / 5 / 2 / 7;
421
- }
422
-
423
- :deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(4)) {
424
- grid-area: 2 / 2 / 3 / 4;
425
- }
426
-
427
- :deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(5)) {
428
- grid-area: 2 / 4 / 3 / 6;
429
- }
430
-
431
- :deep(.Slide-Step_active) {
432
- background: var(--theme-current-color);
433
- color: var(--theme-text-on-dark);
434
- --theme-text: var(--theme-text-on-dark);
435
- --theme-text-muted: var(--theme-text-muted-on-contrast);
436
- }
437
-
438
392
  :deep(.Slide-Quote) {
439
393
  max-width: var(--theme-grid-span-11-width);
440
394
  font-size: var(--theme-text-size-7);
@@ -96,7 +96,7 @@ const props = withDefaults(defineProps<{
96
96
  const slotId = `theme-slot-${Math.random().toString(36).slice(2, 10)}`
97
97
  const instance = getCurrentInstance()
98
98
  const placementSession = useSlotPlacementSession()
99
- const { defaultTone, deckTitle, decors } = useThemeConfig()
99
+ const { defaultTone, deckTitle, decors, replaceDecors } = useThemeConfig()
100
100
  const { $slidev } = useSlideContext()
101
101
  const resolvedRect = shallowRef<ResolvedSlotPlacement['rect'] | null>(null)
102
102
  const resolvedFootprint = shallowRef<ResolvedSlotPlacement['footprint'] | null>(null)
@@ -208,6 +208,7 @@ const decorSeed = computed(() => [
208
208
  ].filter(Boolean).join('|'))
209
209
 
210
210
  const media = computed(() => createThemeMedia({
211
+ ...(replaceDecors.value ? { baseCatalog: [] } : {}),
211
212
  themeCatalog: decors.value,
212
213
  warn: (message: string) => console.warn(message),
213
214
  }))
@@ -333,8 +334,8 @@ const surfaceClass = computed(() => {
333
334
 
334
335
  .Slot_surface_color {
335
336
  background: var(--theme-slot-color, var(--theme-current-color));
336
- --theme-text: var(--theme-text-on-dark);
337
- --theme-text-muted: var(--theme-text-muted-on-contrast);
337
+ --theme-text: var(--theme-text-on-color);
338
+ --theme-text-muted: var(--theme-text-muted-on-color);
338
339
  --theme-inline-code-text: var(--theme-text);
339
340
  color: var(--theme-text);
340
341
  }