slidev-theme-practicum 0.1.4 → 0.3.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 (36) hide show
  1. package/README.md +238 -18
  2. package/components/Slide.vue +27 -4
  3. package/components/Slot.vue +2 -1
  4. package/components/StepsGrid.vue +112 -0
  5. package/composables/deck-decors.ts +74 -0
  6. package/composables/decor-sources.ts +43 -0
  7. package/composables/layout-authoring.ts +34 -9
  8. package/composables/layout-recipes.ts +83 -2
  9. package/composables/layout-shorthands.ts +233 -2
  10. package/composables/local-layout-variant-files.ts +106 -0
  11. package/composables/local-layout-variants.ts +73 -0
  12. package/composables/slide-layout.ts +3 -0
  13. package/composables/use-theme-config.ts +17 -10
  14. package/composables/validate-deck-layouts.cjs +77 -6
  15. package/env.d.ts +18 -0
  16. package/example.md +135 -5
  17. package/package.json +6 -3
  18. package/scripts/browser-smoke.mjs +168 -5
  19. package/scripts/check-local-layout-variant-build.mjs +189 -0
  20. package/scripts/check-package.mjs +18 -2
  21. package/scripts/requirements-illustrations.txt +1 -0
  22. package/scripts/trace-line-art.py +422 -0
  23. package/scripts/validate-deck.cjs +1 -1
  24. package/setup/vite-plugins.ts +109 -2
  25. package/skills/slidev-practicum/SKILL.md +18 -4
  26. package/skills/slidev-practicum/references/contour-illustrations.md +114 -0
  27. package/skills/slidev-practicum/references/deck-project-structure.md +136 -0
  28. package/skills/slidev-practicum/references/illustration-examples/balance-scales.png +0 -0
  29. package/skills/slidev-practicum/references/illustration-examples/balance-scales.svg +88 -0
  30. package/skills/slidev-practicum/references/illustration-examples/chainsaw.png +0 -0
  31. package/skills/slidev-practicum/references/illustration-examples/chainsaw.svg +4 -0
  32. package/skills/slidev-practicum/references/illustration-examples/graduation-cap.png +0 -0
  33. package/skills/slidev-practicum/references/illustration-examples/graduation-cap.svg +23 -0
  34. package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.png +0 -0
  35. package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.svg +4 -0
  36. package/skills/slidev-practicum/references/photographic-illustrations.md +100 -0
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
  ## Агентный скилл
@@ -46,6 +46,34 @@ npm install -D slidev-theme-practicum
46
46
  theme: practicum
47
47
  ```
48
48
 
49
+ ## Структура проекта колоды
50
+
51
+ Для презентации, которая использует установленную тему, каноническая точка входа — `slides.md`, а локальные медиа лежат в `public/` и подключаются абсолютным путём от корня сайта:
52
+
53
+ ```text
54
+ <deck>/
55
+ ├── slides.md
56
+ ├── decors.yaml # необязательный внешний каталог декора
57
+ ├── package.json
58
+ ├── package-lock.json
59
+ ├── pages/ # разделы большой колоды
60
+ ├── components/ # только компоненты этой колоды
61
+ │ └── layout-variants/ # локальные варианты тематических layout
62
+ ├── layouts/ # новые нативные layout самой презентации
63
+ ├── snippets/ # импортируемые примеры кода
64
+ ├── styles/index.css # локальные глобальные переопределения
65
+ ├── public/
66
+ │ ├── decor/ # графический декор
67
+ │ ├── photos/ # фотографии
68
+ │ ├── illustrations/ # контурные предметы, парные PNG/SVG
69
+ │ └── figures/ # схемы, графики и снимки интерфейса
70
+ └── reference/ # исходники и источники, не входящие в сборку
71
+ ```
72
+
73
+ Создавайте необязательные каталоги только при появлении содержимого. Не заводите параллельные `assets/`, `images/` или `img/`. Фотография остаётся в `photos`, даже если служит фоном; контурный предмет остаётся в `illustrations`, даже если используется как декор. Для файла колоды пишите `/photos/team-workshop.webp`, а не `public/photos/...` и не `/theme/photos/...`. Префикс `/theme` принадлежит только встроенным файлам темы.
74
+
75
+ Полный контракт структуры, классификации, именования и проверки закреплён в агентном справочнике [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).
76
+
49
77
  ## Как выбрать слайд
50
78
 
51
79
  Выбирайте не по теме презентации, а по задаче кадра.
@@ -58,14 +86,19 @@ theme: practicum
58
86
  | Объяснить понятие, ситуацию или решение | Поясняющий слайд (`explainer`) | Заголовок и связное пояснение |
59
87
  | Дать большой текстовый блок с разметкой | Текстовый блок (`explainer`, `title-body`) | Абзац, затем короткий список или критерий |
60
88
  | Показать план, правила или шаги | Коллекцию (`collection`) | Однотипные пункты в одном формате |
89
+ | Сопоставить два состояния или подхода | Сравнение (`collection`, `comparison`) | Две стороны с самостоятельными заголовками |
90
+ | Показать последовательность действий | Шаги (`collection`, `steps`) | От 3 до 6 коротких действий |
61
91
  | Показать события во времени | Таймлайн (`collection`, `timeline`) | Годы или месяцы и короткие подписи |
62
92
  | Показать несколько чисел | Метрики (`collection`, `metrics`) | Главное число и подписи, которые объясняют масштаб |
93
+ | Дать четыре пронумерованные опоры | Факты (`collection`, `facts`) | Четыре значения с вложенными подписями |
63
94
  | Собрать нестандартную композицию | Ручную сетку (`none`) | Только когда обычные типы не выражают задачу |
64
95
 
65
96
  ## Что писать
66
97
 
67
98
  Заголовок должен отвечать на вопрос «что зритель должен понять сейчас?». Не называйте слайд технически вроде «Метрики» или «Композиция», если можно сразу написать вывод.
68
99
 
100
+ В первом headmatter верхнеуровневое поле `title` разрешено: это служебное название всей колоды для HTML-документа и метаданных Slidev, а не содержимое первого слайда. Видимый заголовок всё равно пишите в теле: как `# …` для сокращённой записи или как `<Text as="h1">…</Text>` для явной композиции, включая `cover`. Со второго слайда верхнеуровневое поле `title` запрещено, чтобы метаданные не принимали за отображаемый текст. Вложенные поля `items[].title`, `comparison.from/to.title` и `person.title` остаются частью видимых моделей компонентов и не относятся к этому запрету.
101
+
69
102
  Поясняющий текст нужен для контекста, ограничения или критерия выбора. Если текст превращается в два разных вывода, разнесите его по двум слайдам.
70
103
 
71
104
  Список работает, когда пункты однотипны: шаги, правила, критерии, темы. Пишите пункты в одинаковой грамматической форме, чтобы их можно было быстро просканировать.
@@ -74,15 +107,19 @@ theme: practicum
74
107
 
75
108
  Цитата должна быть короткой. Авторство добавляет источник, но не должно конкурировать с самой фразой.
76
109
 
77
- ## Семантика декоративных фотографий
110
+ ## Семантические роли изображений
78
111
 
79
- Фотографии в шаблонах Практикума всегда декоративные. Они не являются источником фактов, доказательством, инструкцией или объектом, который зритель должен рассматривать ради ключевой информации.
112
+ Роль изображения определяется не форматом файла и не компонентом, а тем, что зритель должен из него понять.
80
113
 
81
- Смысл слайда должен полностью читаться из текста, чисел, подписей, порядка элементов и выбранной композиции. Если фотографию убрать или заменить другой фотографией, вывод слайда не должен измениться.
114
+ | Роль | Назначение | Требование к смыслу |
115
+ |---|---|---|
116
+ | Декоративная фотография | Настроение, ритм, плотность, брендовый характер или пауза | Взаимозаменяема: если её убрать или заменить, вывод слайда не изменится |
117
+ | Сюжетная фотографическая иллюстрация | Конкретная метафора, действие или сквозные персонажи | Не взаимозаменяема внутри сюжета, но не является единственным носителем факта, инструкции или вывода |
118
+ | Информационная фигура | Схема, график, интерфейс, сравнение или доказательство, которое нужно рассмотреть | Существенные данные и вывод продублированы доступной подписью или текстом слайда |
82
119
 
83
- Фото может задавать настроение, визуальный ритм, плотность, брендовый характер или паузу между текстовыми блоками. Оно не должно объяснять метрику, доказывать тезис, заменять подпись или содержать единственный важный контекст.
120
+ Встроенные фотографии темы и выбор через `decor` всегда декоративные. Колода может добавлять собственные сюжетные фотографии в `public/photos/` и размещать их через `Image` или `Slot.background`. В таком случае конкретный сюжетный смысл должен быть назван заголовком, текстом, подписью или доступным описанием: фотография поддерживает рассказ, но не заменяет его.
84
121
 
85
- Не пишите в примерах и документации: «на фото видно», «фото доказывает», «снимки дали контекст», «фотография объясняет число». Такие формулировки ошибочно превращают декоративный слой в информационный.
122
+ Не пишите о декоративной фотографии: «фото доказывает» или «фотография объясняет число». Если зритель действительно должен рассмотреть данные, интерфейс или причинно-следственную схему, это информационная фигура из `public/figures/`, а не декоративный слой.
86
123
 
87
124
  ## Живая галерея
88
125
 
@@ -104,10 +141,176 @@ layout: cover | message | explainer | collection | none
104
141
 
105
142
  Для `layout: message`, `explainer` и `collection` часть `variant` принимает markdown в default slot слайда (`#` заголовок, списки, blockquote, frontmatter) — тема разворачивает его в `Slot` / `Text`. Реестр: `composables/layout-shorthands.ts`.
106
143
 
144
+ Для `message:centered` тема выбирает самый крупный помещающийся размер заголовка в диапазоне крупных токенов `7-12`.
145
+
107
146
  Для `message:closing` обязателен один заголовок `# …`; под ним можно добавить один необязательный абзац. Дополнительные абзацы, списки и изображения не поддерживаются.
108
147
 
109
148
  `layout: cover` markdown shorthand **не** имеет: для всех `cover:*` нужны явные `<Slot role="...">` и `<Text>` (см. обложки в [example.md](example.md)).
110
149
 
150
+ Сокращённая Markdown-запись и явные `<Slot role="...">` — два взаимоисключающих режима авторинга одного встроенного варианта. Если в основном слоте есть явный ролевой `Slot`, тема использует ролевые компоненты как готовое содержимое и не разворачивает `items`, `comparison` и другие поля сокращённой записи. Исключение — `collection:agenda`, где разрешён один явный `Slot role="media"` как переопределение иллюстрации. Чтобы `items[].title` или `comparison.from/to.title` попали в визуальный результат, оставьте во входе только поля front matter и канонический Markdown-заголовок; не дублируйте те же данные в `<Text>`.
151
+
152
+ ### Локальные варианты презентации
153
+
154
+ Slidev уже автоматически подключает Vue-компоненты из `components/` конечной презентации. Их можно использовать тегами прямо в `slides.md`. Локальный вариант нужен для другого случая: повторяющаяся композиция, или архетип, выбирается привычной парой `layout` + `variant`, а содержимое слайда остаётся обычным Markdown без Vue-тегов.
155
+
156
+ Файл лежит внутри каталога тематического `layout`. Например, `components/layout-variants/explainer/lesson-summary.vue` соответствует `layout: explainer` и `variant: lesson-summary`:
157
+
158
+ ```text
159
+ <deck>/
160
+ ├── slides.md
161
+ ├── components/
162
+ │ ├── CourseBadge.vue
163
+ │ └── layout-variants/
164
+ │ └── explainer/
165
+ │ └── lesson-summary.vue
166
+ └── layouts/
167
+ └── workshop.vue
168
+ ```
169
+
170
+ ```md
171
+ ---
172
+ layout: explainer
173
+ variant: lesson-summary
174
+ badge:
175
+ text: Практика
176
+ ---
177
+
178
+ # Что запомнить
179
+
180
+ - Компоненты принадлежат презентации
181
+ - Локальный вариант выбирается через `layout` и `variant`
182
+ ```
183
+
184
+ Vue-файл получает отдельную копию `frontmatter`, защищённую от записи на верхнем уровне, значения `layout` и `variant`, а также разобранный Markdown через основной слот:
185
+
186
+ ```vue
187
+ <script setup lang="ts">
188
+ import type {
189
+ DeckLayoutVariantProps,
190
+ DeckLayoutVariantSlots,
191
+ } from 'slidev-theme-practicum/composables/local-layout-variants'
192
+
193
+ type LessonSummaryFrontmatter = {
194
+ badge?: {
195
+ text?: string
196
+ }
197
+ }
198
+
199
+ defineProps<DeckLayoutVariantProps<LessonSummaryFrontmatter>>()
200
+ defineSlots<DeckLayoutVariantSlots>()
201
+ </script>
202
+
203
+ <template>
204
+ <Slot area="1 / 1 / 9 / 9" surface="light" margin="4" gap="3">
205
+ <slot />
206
+ </Slot>
207
+
208
+ <Slot area="9 / 9 / -1 / -1" surface="color" margin="3">
209
+ <CourseBadge :text="frontmatter.badge?.text ?? variant" />
210
+ </Slot>
211
+ </template>
212
+ ```
213
+
214
+ Компоненты темы (`Slot`, `Text`, `Image`, `Person` и другие) и остальные компоненты презентации внутри такого файла доступны по обычным правилам автоматического подключения Slidev. Корневые `Slot` локального варианта используют ручные `area`, `col` или `row`: поле `role` принадлежит только встроенным рецептам темы.
215
+
216
+ Правила выбора намеренно строгие:
217
+
218
+ - первый каталог — один из тематических `cover`, `message`, `explainer` или `collection`, а имя Vue-файла и значение `variant` пишутся в `kebab-case`;
219
+ - встроенный вариант темы нельзя затереть локальным файлом: для локального архетипа выбирайте новое имя `variant`;
220
+ - `arrangement` нельзя добавлять к локальному варианту — дополнительные параметры получают собственные поля front matter;
221
+ - неизвестный `variant` останавливает просмотр, сборку и экспорт с подсказкой до ожидаемого файла;
222
+ - для принципиально нового типа слайда используйте нативный механизм Slidev: `layouts/workshop.vue` и `layout: workshop`. Такой layout не проходит через варианты темы.
223
+
224
+ `slidev-practicum-validate` проверяет имя, наличие локального варианта и допустимое сочетание полей front matter. Типы и шаблон конкретного Vue-файла дополнительно проверяются обычной типизацией и сборкой презентации.
225
+
226
+ ### Варианты collection P0
227
+
228
+ | `variant` | `arrangement` | Контракт |
229
+ | ------------- | ------------------ | -------- |
230
+ | `comparison` | `before-after` | Markdown heading; `comparison.from` и `comparison.to` с обязательным `title`; необязательные `kicker`, `body` и `relation.label` |
231
+ | `comparison` | `stable-variable` | Тот же контракт; вертикальное направление задаёт arrangement |
232
+ | `steps` | `linear` | Markdown heading; `items` из 3–6 объектов с обязательным `title`, необязательными `body`, `label`; не больше одного `active: true` |
233
+ | `steps` | `staggered` | Тот же контракт, но ровно 5 элементов |
234
+ | `metrics` | `dashboard` | Markdown heading; ровно 5 `metrics` с обязательными `value` и одним из `body`, `label`, `title`; без `media` и произвольных spans |
235
+ | `facts` | `numbered-quartet` | Markdown heading; ровно 4 пункта вида «значение → вложенная подпись»; номера 1–4 добавляет тема |
236
+
237
+ Канонический alias для пронумерованных фактов — `variant: facts-numbered-quartet`. Для остальных вариантов также можно использовать aliases `comparison-before-after`, `comparison-stable-variable`, `steps-linear`, `steps-staggered` и `metrics-dashboard` без `arrangement`.
238
+
239
+ Минимальное сравнение:
240
+
241
+ ```md
242
+ ---
243
+ layout: collection
244
+ variant: comparison
245
+ arrangement: before-after
246
+ comparison:
247
+ from:
248
+ title: Ручная проверка
249
+ to:
250
+ title: Единый контур
251
+ relation:
252
+ label: переход
253
+ ---
254
+
255
+ # Как меняется процесс
256
+ ```
257
+
258
+ Минимальные шаги:
259
+
260
+ ```md
261
+ ---
262
+ layout: collection
263
+ variant: steps
264
+ arrangement: linear
265
+ items:
266
+ - title: Собрать входы
267
+ - title: Проверить риски
268
+ active: true
269
+ - title: Зафиксировать результат
270
+ ---
271
+
272
+ # Три шага
273
+ ```
274
+
275
+ Минимальный dashboard:
276
+
277
+ ```md
278
+ ---
279
+ layout: collection
280
+ variant: metrics
281
+ arrangement: dashboard
282
+ metrics:
283
+ - { value: 42%, label: прошли сценарий }
284
+ - { value: 18, label: команд }
285
+ - { value: 9, label: проверок }
286
+ - { value: 2×, label: быстрее обратная связь }
287
+ - { value: 0, label: ручных spans }
288
+ ---
289
+
290
+ # Панель результата
291
+ ```
292
+
293
+ Минимальный `numbered-quartet`:
294
+
295
+ ```md
296
+ ---
297
+ layout: collection
298
+ variant: facts
299
+ arrangement: numbered-quartet
300
+ ---
301
+
302
+ # Четыре опоры
303
+
304
+ - Входы
305
+ - собраны до старта
306
+ - Риски
307
+ - проверены явно
308
+ - Результат
309
+ - сохранён в репозитории
310
+ - Стоп
311
+ - задан до запуска
312
+ ```
313
+
111
314
  | Слой | Для чего |
112
315
  | ------------- | ------------------------------------------------ |
113
316
  | `layout` | семантика слайда на уровне Slidev |
@@ -115,12 +318,14 @@ layout: cover | message | explainer | collection | none
115
318
  | `arrangement` | компактная перестановка одной модели данных |
116
319
  | `Slot` | область сетки, поверхность, отступы и медиа-слой |
117
320
  | `Text` | типографика и согласованный подбор размера |
118
- | `Image` | декоративная картинка внутри ручной композиции |
321
+ | `Image` | изображение внутри ручной композиции; роль задаёт автор |
119
322
  | `decor` | семантический выбор декоративного изображения |
120
- | `background` | низкоуровневое размещение декоративной картинки |
323
+ | `background` | низкоуровневое размещение изображения |
121
324
 
122
325
  ## Метаданные слайда
123
326
 
327
+ Первый headmatter может содержать верхнеуровневый `title` как служебное название всей колоды. На остальных слайдах валидатор отклоняет этот ключ; основной текст каждого кадра должен находиться в теле слайда.
328
+
124
329
  | Поле | Значения | По умолчанию | Смысл |
125
330
  | ------------- | ----------------------------------------------------- | --------------- | ---------------------------------------- |
126
331
  | `layout` | `cover`, `message`, `explainer`, `collection`, `none` | значение Slidev | роль слайда или ручная сетка |
@@ -202,7 +407,8 @@ Slidev парсит markdown внутри компонента только ес
202
407
 
203
408
  | Свойство или поле | Тип | По умолчанию | Смысл |
204
409
  | ------------------ | ------------------------------------------------ | ------------ | --------------------------------- |
205
- | `src` | путь | обязательно | исходное декоративное изображение |
410
+ | `src` | путь | обязательно | исходное изображение |
411
+ | `alt` | строка | пусто | доступное описание недекоративного изображения |
206
412
  | `fit` | `cover`, `contain`, `fill`, `none`, `scale-down` | `cover` | режим заполнения |
207
413
  | `position` | позиция CSS | `center` | позиция изображения |
208
414
  | `zoom` | число | `1` | масштаб |
@@ -230,7 +436,7 @@ Slidev парсит markdown внутри компонента только ес
230
436
 
231
437
  Встроенные файлы темы доступны по префиксу `/theme`: например, `/theme/photos/photo-6.webp` и `/theme/decor/decor-10.svg`. Файлы из `public` самой колоды остаются пользовательскими и задаются от корня, например `/decor/custom-data.png`; префикс `/theme` к ним добавлять не нужно.
232
438
 
233
- Даже когда используется `Image` или `Slot.background`, фотография остаётся декоративной. Ключевые факты, различия, инструкции и выводы должны быть записаны текстом или числом.
439
+ `Image` и `Slot.background` не определяют семантическую роль. Встроенные `/theme/photos/...` и выбор через `decor` остаются декоративными; собственная фотография колоды из `/photos/...` может быть сюжетной. Для сюжетного `Image` заполняйте `alt`. Фон `Slot.background` скрыт от вспомогательных технологий, поэтому его смысл обязательно дублируется видимым заголовком, текстом или подписью. Ключевые факты, различия, инструкции и выводы в любом случае должны быть записаны текстом, числом или доступной подписью.
234
440
 
235
441
  ### Decor
236
442
 
@@ -267,11 +473,18 @@ themeConfig:
267
473
  tone: blue
268
474
  ```
269
475
 
476
+ ```yaml
477
+ themeConfig:
478
+ decors: ./decors.yaml
479
+ ```
480
+
270
481
  ```yaml
271
482
  themeConfig:
272
483
  deckTitle: 'Название колоды'
273
484
  debugGrid: false
274
485
  decors:
486
+ - ./decors/clocks.yaml
487
+ - ./decors/photos.yaml
275
488
  - id: decor-custom-data
276
489
  src: /decor/custom-data.png
277
490
  meaning: data
@@ -281,14 +494,21 @@ themeConfig:
281
494
  ratio: [1.2, 2.7]
282
495
  ```
283
496
 
497
+ `decors` принимает список записей, путь к файлу или смешанный список путей и записей. Файл может быть YAML, JSON или ESM (`decors.yaml`, `.yml`, `.json`, `.mjs`) и содержать массив записей, объект `{ decors: [...] }` или одну запись. Если `decors` не задан, тема подхватывает `decors.yaml` / `.yml` / `.json` / `.mjs` в корне колоды, если такой файл есть. Позже идущие записи с тем же `id` переопределяют более ранние.
498
+
499
+ Файл каталога по умолчанию **заменяет** встроенный каталог темы: слоты видят только записи колоды. Короткие inline-записи по умолчанию по-прежнему добавляются к встроенному каталогу. Явный `replaceDecors` перекрывает оба случая.
500
+
284
501
  В записи каталога `cols` и `rows` описывают допустимый размер слота в сетке 12x12, а `ratio` ограничивает соотношение `cols / rows` для ориентации. Вместо `ratio` можно использовать более явный алиас `aspectRatio`.
285
502
 
286
- | Поле | Тип | По умолчанию | Смысл |
287
- | ----------------- | --------------- | ------------------ | -------------------------------------------------------- |
288
- | `deckTitle` | строка | заголовок слайда | заголовок в шапке |
289
- | `debugGrid` | boolean | `false` | отладочная сетка 12x12 |
290
- | `decors` | записи каталога | встроенный каталог | добавляет или переопределяет записи декора по `decor.id` |
291
- | `decorSaveOrigin` | origin URL | origin dev-сервера | разрешённый origin для сохранения настроек декора |
503
+ | Поле | Тип | По умолчанию | Смысл |
504
+ | ----------------- | --------------------------------- | ------------------ | -------------------------------------------------------- |
505
+ | `deckTitle` | строка | пустая строка | служебное название колоды в шапке |
506
+ | `debugGrid` | boolean | `false` | отладочная сетка 12x12 |
507
+ | `decors` | записи, путь или список путей | встроенный каталог | файл заменяет встроенный каталог; inline-записи добавляют или переопределяют по `decor.id` |
508
+ | `replaceDecors` | boolean | `true` для файла каталога | не подмешивать встроенные картинки темы |
509
+ | `decorSaveOrigin` | origin URL | origin dev-сервера | разрешённый origin для сохранения настроек декора |
510
+
511
+ `themeConfig.deckTitle` задаётся явно и отвечает только за повторяющееся служебное название в шапке. Он не заменяет видимый заголовок конкретного слайда и не берётся из верхнеуровневого `title`.
292
512
 
293
513
  Если dev-сервер работает за reverse proxy с завершением TLS, задайте внешний origin явно:
294
514
 
@@ -327,6 +547,6 @@ npm test
327
547
 
328
548
  - lint и типы;
329
549
  - модульные, контрактные и архитектурные тесты;
330
- - продукционную сборку и состав её артефактов;
550
+ - продукционную сборку, локальный вариант внешней презентации и состав артефактов;
331
551
  - репрезентативные слайды в Chromium, переполнение холста и загрузку медиа по `/theme/...`;
332
552
  - состав и размер npm-пакета.
@@ -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
200
  :inverted="isContrast" />
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>
@@ -366,6 +373,22 @@ const slideClass = computed(() => ({
366
373
  justify-content: flex-start;
367
374
  }
368
375
 
376
+ :deep(.Slide-ComparisonRelation) {
377
+ display: flex;
378
+ min-width: 0;
379
+ min-height: 100%;
380
+ flex-direction: column;
381
+ align-items: center;
382
+ justify-content: center;
383
+ gap: calc(var(--theme-grid-module) * 2);
384
+ text-align: center;
385
+ }
386
+
387
+ :deep(.Slide-ComparisonDirection) {
388
+ font-size: var(--theme-text-size-5);
389
+ line-height: var(--theme-text-line-5);
390
+ }
391
+
369
392
  :deep(.Slide-Quote) {
370
393
  max-width: var(--theme-grid-span-11-width);
371
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
  }))
@@ -0,0 +1,112 @@
1
+ <script setup lang="ts">
2
+ import { getCurrentInstance } from 'vue'
3
+ import Text from './Text.vue'
4
+ import TextFitGroup from './TextFitGroup.vue'
5
+
6
+ type StepItem = {
7
+ title: string
8
+ body: string
9
+ label: string
10
+ active: boolean
11
+ }
12
+
13
+ const stepsFitGroup = `steps-${getCurrentInstance()?.uid ?? 'default'}`
14
+
15
+ const props = defineProps<{
16
+ items: StepItem[]
17
+ arrangement: 'linear' | 'staggered'
18
+ }>()
19
+ </script>
20
+
21
+ <template>
22
+ <div class="Slide-StepsGrid"
23
+ :class="`Slide-StepsGrid_${props.arrangement}`"
24
+ :data-count="props.items.length"
25
+ :style="{ '--slide-steps-count': props.items.length }">
26
+ <article v-for="(item, index) in props.items"
27
+ :key="index"
28
+ class="Slide-Step"
29
+ :class="{ 'Slide-Step_active': item.active }"
30
+ :data-index="index + 1">
31
+ <Text size="4">{{ item.label }}</Text>
32
+ <TextFitGroup class="Slide-Step-FitGroup"
33
+ :fit-group="`${stepsFitGroup}-titles`">
34
+ <Text size="3-5">{{ item.title }}</Text>
35
+ </TextFitGroup>
36
+ <TextFitGroup v-if="item.body"
37
+ class="Slide-Step-FitGroup"
38
+ :fit-group="`${stepsFitGroup}-bodies`">
39
+ <Text size="2-3" priority="2" muted>{{ item.body }}</Text>
40
+ </TextFitGroup>
41
+ </article>
42
+ </div>
43
+ </template>
44
+
45
+ <style scoped>
46
+ .Slide-StepsGrid {
47
+ display: grid;
48
+ width: 100%;
49
+ max-width: 100%;
50
+ height: 100%;
51
+ min-width: 0;
52
+ min-height: 0;
53
+ grid-template-columns: repeat(var(--slide-steps-count), minmax(0, 1fr));
54
+ gap: var(--theme-grid-gap);
55
+ }
56
+
57
+ .Slide-Step {
58
+ display: flex;
59
+ min-width: 0;
60
+ min-height: 0;
61
+ overflow: hidden;
62
+ flex-direction: column;
63
+ gap: calc(var(--theme-grid-module) * 2);
64
+ padding: var(--theme-slot-margin-3);
65
+ border-radius: var(--theme-panel-radius);
66
+ background: var(--theme-surface-light);
67
+ }
68
+
69
+ .Slide-Step-FitGroup {
70
+ width: 100%;
71
+ height: auto;
72
+ min-width: 0;
73
+ }
74
+
75
+ .Slide-Step :deep(.TextFitGroup) {
76
+ width: 100%;
77
+ height: auto;
78
+ min-width: 0;
79
+ }
80
+
81
+ .Slide-StepsGrid_staggered {
82
+ grid-template-columns: repeat(6, minmax(0, 1fr));
83
+ grid-template-rows: repeat(2, minmax(0, 1fr));
84
+ }
85
+
86
+ .Slide-StepsGrid_staggered .Slide-Step:nth-child(1) {
87
+ grid-area: 1 / 1 / 2 / 3;
88
+ }
89
+
90
+ .Slide-StepsGrid_staggered .Slide-Step:nth-child(2) {
91
+ grid-area: 1 / 3 / 2 / 5;
92
+ }
93
+
94
+ .Slide-StepsGrid_staggered .Slide-Step:nth-child(3) {
95
+ grid-area: 1 / 5 / 2 / 7;
96
+ }
97
+
98
+ .Slide-StepsGrid_staggered .Slide-Step:nth-child(4) {
99
+ grid-area: 2 / 2 / 3 / 4;
100
+ }
101
+
102
+ .Slide-StepsGrid_staggered .Slide-Step:nth-child(5) {
103
+ grid-area: 2 / 4 / 3 / 6;
104
+ }
105
+
106
+ .Slide-Step_active {
107
+ background: var(--theme-current-color);
108
+ color: var(--theme-text-on-dark);
109
+ --theme-text: var(--theme-text-on-dark);
110
+ --theme-text-muted: var(--theme-text-muted-on-contrast);
111
+ }
112
+ </style>