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
@@ -0,0 +1,106 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { hasLayoutVariant, type ThemeLayout } from './layout-recipes'
4
+
5
+ export type DeckLayoutVariantFile = {
6
+ key: string
7
+ layout: ThemeLayout
8
+ variant: string
9
+ path: string
10
+ }
11
+
12
+ export const DECK_LAYOUT_VARIANTS_VIRTUAL_ID = 'virtual:practicum-deck-layout-variants'
13
+ export const RESOLVED_DECK_LAYOUT_VARIANTS_VIRTUAL_ID = '\0virtual:practicum-deck-layout-variants'
14
+
15
+ const THEME_LAYOUTS = new Set<ThemeLayout>(['cover', 'message', 'explainer', 'collection'])
16
+ const VARIANT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
17
+
18
+ export function deckLayoutVariantsDirectory(root: string) {
19
+ return join(root, 'components', 'layout-variants')
20
+ }
21
+
22
+ function readDirectory(path: string) {
23
+ try {
24
+ return readdirSync(path, { withFileTypes: true })
25
+ }
26
+ catch (error) {
27
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
28
+ return []
29
+ throw error
30
+ }
31
+ }
32
+
33
+ export function discoverDeckLayoutVariantFiles(root: string): DeckLayoutVariantFile[] {
34
+ const directory = deckLayoutVariantsDirectory(root)
35
+ const layoutEntries = readDirectory(directory)
36
+
37
+ for (const entry of layoutEntries) {
38
+ if (entry.isFile() && entry.name.endsWith('.vue')) {
39
+ throw new Error(
40
+ `[Practicum] Локальный вариант "${entry.name}" должен лежать в components/layout-variants/<layout>/${entry.name}.`,
41
+ )
42
+ }
43
+
44
+ if (entry.isDirectory() && !THEME_LAYOUTS.has(entry.name as ThemeLayout)) {
45
+ throw new Error(
46
+ `[Practicum] "${entry.name}" не является тематическим layout. Для нового layout создайте layouts/${entry.name}.vue.`,
47
+ )
48
+ }
49
+ }
50
+
51
+ return layoutEntries
52
+ .filter(entry => entry.isDirectory() && THEME_LAYOUTS.has(entry.name as ThemeLayout))
53
+ .flatMap((layoutEntry) => {
54
+ const layout = layoutEntry.name as ThemeLayout
55
+ const layoutDirectory = join(directory, layout)
56
+
57
+ return readDirectory(layoutDirectory)
58
+ .filter(entry => entry.isFile() && entry.name.endsWith('.vue'))
59
+ .map((entry) => {
60
+ const variant = entry.name.slice(0, -'.vue'.length)
61
+
62
+ if (!VARIANT_ID_PATTERN.test(variant)) {
63
+ throw new Error(
64
+ `[Practicum] Локальный вариант "${layout}/${entry.name}" должен иметь kebab-case имя, например "lesson-summary.vue".`,
65
+ )
66
+ }
67
+
68
+ if (hasLayoutVariant(layout, variant)) {
69
+ throw new Error(
70
+ `[Practicum] Локальный вариант "${layout}:${variant}" конфликтует со встроенным вариантом темы.`,
71
+ )
72
+ }
73
+
74
+ return {
75
+ key: `${layout}:${variant}`,
76
+ layout,
77
+ variant,
78
+ path: join(layoutDirectory, entry.name),
79
+ }
80
+ })
81
+ })
82
+ .sort((left, right) => left.key.localeCompare(right.key))
83
+ }
84
+
85
+ function toViteFileId(path: string) {
86
+ const normalized = path.replaceAll('\\', '/')
87
+ return `/@fs${normalized.startsWith('/') ? '' : '/'}${normalized}`
88
+ }
89
+
90
+ export function createDeckLayoutVariantModuleSource(files: readonly DeckLayoutVariantFile[]) {
91
+ const imports = files.map((file, index) =>
92
+ `import DeckLayoutVariant${index} from ${JSON.stringify(toViteFileId(file.path))}`,
93
+ )
94
+ const entries = files.map((file, index) =>
95
+ ` ${JSON.stringify(file.key)}: DeckLayoutVariant${index},`,
96
+ )
97
+
98
+ return [
99
+ ...imports,
100
+ '',
101
+ 'export const DECK_LAYOUT_VARIANTS = Object.freeze({',
102
+ ...entries,
103
+ '})',
104
+ '',
105
+ ].join('\n')
106
+ }
@@ -0,0 +1,73 @@
1
+ import { h, type Component, type VNode } from 'vue'
2
+ import type { ThemeLayout } from './layout-recipes'
3
+
4
+ export type DeckLayoutVariantCatalog = Readonly<Record<string, Component>>
5
+
6
+ export type DeckLayoutVariantProps<Frontmatter extends object = Record<string, unknown>> = {
7
+ layout: ThemeLayout
8
+ variant: string
9
+ frontmatter: Readonly<Frontmatter>
10
+ }
11
+
12
+ export type DeckLayoutVariantSlots = {
13
+ default(): VNode[]
14
+ }
15
+
16
+ export type SlideDeckLayoutVariantContractError = Error & {
17
+ name: 'SlideMarkdownContractError'
18
+ hint?: string
19
+ }
20
+
21
+ function failDeckLayoutVariantContract(message: string, hint?: string): never {
22
+ const error = new Error(`[Slide] Локальный вариант ${message}`) as SlideDeckLayoutVariantContractError
23
+ error.name = 'SlideMarkdownContractError'
24
+ if (hint)
25
+ error.hint = hint
26
+ throw error
27
+ }
28
+
29
+ export function deckLayoutVariantKey(layout: ThemeLayout, variant: string) {
30
+ return `${layout}:${variant}`
31
+ }
32
+
33
+ export function resolveDeckLayoutVariant(input: {
34
+ layout: ThemeLayout
35
+ variant: string
36
+ arrangement: string
37
+ variants: DeckLayoutVariantCatalog
38
+ }) {
39
+ const component = input.variants[deckLayoutVariantKey(input.layout, input.variant)]
40
+
41
+ if (component && input.arrangement) {
42
+ failDeckLayoutVariantContract(
43
+ `"${input.layout}:${input.variant}" не поддерживает arrangement: ${input.arrangement}.`,
44
+ 'Удалите `arrangement` либо используйте встроенный вариант темы.',
45
+ )
46
+ }
47
+
48
+ if (component)
49
+ return component
50
+
51
+ failDeckLayoutVariantContract(
52
+ `"${input.layout}:${input.variant}" не найден.`,
53
+ `Создайте components/layout-variants/${input.layout}/${input.variant}.vue в проекте презентации.`,
54
+ )
55
+ }
56
+
57
+ export function buildDeckLayoutVariantVNode(input: {
58
+ layout: ThemeLayout
59
+ variant: string
60
+ component: Component
61
+ frontmatter: Record<string, unknown>
62
+ children: readonly VNode[]
63
+ }) {
64
+ const frontmatter = Object.freeze({ ...input.frontmatter })
65
+
66
+ return h(input.component, {
67
+ layout: input.layout,
68
+ variant: input.variant,
69
+ frontmatter,
70
+ } satisfies DeckLayoutVariantProps, {
71
+ default: () => [...input.children],
72
+ })
73
+ }
@@ -5,6 +5,7 @@ import {
5
5
  type LayoutAuthoringInput,
6
6
  } from './layout-authoring'
7
7
  import type { LayoutShorthandComponents } from './layout-shorthands'
8
+ import type { DeckLayoutVariantCatalog } from './local-layout-variants'
8
9
  import {
9
10
  createSlotPlacementSession,
10
11
  type SlotPlacementSession,
@@ -19,6 +20,7 @@ export type SlideLayout = SlotPlacementSession & {
19
20
 
20
21
  export function createSlideLayout(input: {
21
22
  components: LayoutShorthandComponents
23
+ layoutVariants?: DeckLayoutVariantCatalog
22
24
  }): SlideLayout {
23
25
  const slotPlanState = createLayoutSlotPlanState()
24
26
  let current: CompiledSlideLayout | null = null
@@ -31,6 +33,7 @@ export function createSlideLayout(input: {
31
33
  const compiled = compileLayoutAuthoring({
32
34
  ...authoringInput,
33
35
  components: input.components,
36
+ layoutVariants: input.layoutVariants,
34
37
  slotPlanState,
35
38
  })
36
39
 
@@ -1,9 +1,15 @@
1
1
  import { computed } from 'vue'
2
2
  import { useSlideContext } from '@slidev/client'
3
+ import { collectDecorSources, isRecord } from './decor-sources'
3
4
  import { resolveTone, type ThemeTone } from './theme-foundation'
4
5
 
5
- function isRecord(value: unknown): value is Record<string, unknown> {
6
- return typeof value === 'object' && value !== null && !Array.isArray(value)
6
+ function readFileDecors() {
7
+ try {
8
+ return Array.isArray(__PRATICUM_DECK_DECORS__) ? __PRATICUM_DECK_DECORS__ : []
9
+ }
10
+ catch {
11
+ return []
12
+ }
7
13
  }
8
14
 
9
15
  function readBoolean(value: unknown, fallback: boolean) {
@@ -45,20 +51,20 @@ export function useThemeConfig() {
45
51
  if (typeof explicit === 'string' && explicit.trim())
46
52
  return explicit.trim()
47
53
 
48
- const fallback = configs.value.title
49
- if (typeof fallback === 'string' && fallback.trim())
50
- return fallback.trim()
51
-
52
54
  return ''
53
55
  })
54
56
 
55
57
  const showPageNumber = computed(() => readBoolean(themeConfigs.value.showPageNumber, true))
56
58
  const debugGrid = computed(() => readBoolean(themeConfigs.value.debugGrid, false))
57
59
  const defaultTone = computed(() => readDefaultTone(themeConfigs.value.defaultTone, 'blue'))
58
- const decors = computed(() => {
59
- const explicit = themeConfigs.value.decors
60
- return Array.isArray(explicit) ? explicit : []
61
- })
60
+ const decors = computed(() => [
61
+ ...readFileDecors(),
62
+ ...collectDecorSources(themeConfigs.value.decors).inline,
63
+ ])
64
+ const replaceDecors = computed(() => readBoolean(
65
+ themeConfigs.value.replaceDecors,
66
+ readFileDecors().length > 0 || collectDecorSources(themeConfigs.value.decors).files.length > 0,
67
+ ))
62
68
 
63
69
  return {
64
70
  themeConfigs,
@@ -67,5 +73,6 @@ export function useThemeConfig() {
67
73
  debugGrid,
68
74
  defaultTone,
69
75
  decors,
76
+ replaceDecors,
70
77
  }
71
78
  }
@@ -1,5 +1,5 @@
1
1
  const { readFileSync } = require('node:fs')
2
- const { isAbsolute, join, resolve } = require('node:path')
2
+ const { dirname, isAbsolute, join, resolve } = require('node:path')
3
3
  const { parseSync } = require('@slidev/parser')
4
4
  const { createCommentVNode, createTextVNode, createVNode } = require('vue')
5
5
  const { registerTypeScript } = require('./typescript-require.cjs')
@@ -41,7 +41,10 @@ registerTypeScript()
41
41
 
42
42
  const { compileLayoutAuthoring } = require(themePath('composables/layout-authoring.ts'))
43
43
  const { hasLayoutShorthand } = require(themePath('composables/layout-shorthands.ts'))
44
+ const { discoverDeckLayoutVariantFiles } = require(themePath('composables/local-layout-variant-files.ts'))
45
+ const { resolveDeckLayoutVariant } = require(themePath('composables/local-layout-variants.ts'))
44
46
  const {
47
+ hasLayoutVariant,
45
48
  resolveLayoutSlotSpec,
46
49
  resolveLayoutVariant,
47
50
  } = require(themePath('composables/layout-recipes.ts'))
@@ -57,6 +60,7 @@ function stubComponents() {
57
60
  Slot: named('Slot'),
58
61
  Text: named('Text'),
59
62
  Timeline: named('Timeline'),
63
+ StepsGrid: named('StepsGrid'),
60
64
  }
61
65
  }
62
66
 
@@ -121,7 +125,7 @@ function validateExplicitSlotIntents(intents, input) {
121
125
  function slideRef(slide, index) {
122
126
  const layout = String(slide.frontmatter?.layout ?? '').trim()
123
127
  const variant = String(slide.frontmatter?.variant ?? '').trim()
124
- const title = String(slide.title || slide.content?.match(/^#\s+(.+)/m)?.[1] || '').trim()
128
+ const title = String(slide.content?.match(/^#\s+(.+)/m)?.[1] || '').trim()
125
129
  return {
126
130
  index,
127
131
  page: index + 1,
@@ -141,17 +145,64 @@ async function validateDeckLayouts(deckPath) {
141
145
  const source = readFileSync(absolutePath, 'utf8')
142
146
  const deck = parseSync(source, absolutePath)
143
147
  const components = stubComponents()
148
+ /** @type {{ key: string, path: string }[]} */
149
+ const layoutVariantFiles = discoverDeckLayoutVariantFiles(dirname(absolutePath))
150
+ const layoutVariants = Object.fromEntries(
151
+ layoutVariantFiles
152
+ .map(file => [file.key, { name: `DeckLayoutVariant:${file.key}` }]),
153
+ )
144
154
  /** @type {DeckValidationIssue[]} */
145
155
  const issues = []
146
156
 
147
157
  for (const [index, slide] of deck.slides.entries()) {
148
158
  const layout = String(slide.frontmatter?.layout ?? '').trim()
159
+ const ref = slideRef(slide, index)
160
+
161
+ if (index > 0 && Object.prototype.hasOwnProperty.call(slide.frontmatter ?? {}, 'title')) {
162
+ issues.push({
163
+ ...ref,
164
+ message: 'Верхнеуровневое поле `title` разрешено только в первом headmatter как служебное название колоды и не является видимым содержимым слайда.',
165
+ hint: 'Удалите поле и перенесите основной текст в тело слайда: используйте Markdown-заголовок `# …` или видимый `<Text as="h1">…</Text>` в явной композиции.',
166
+ })
167
+ }
168
+
149
169
  if (!isThemeLayout(layout))
150
170
  continue
151
171
 
152
172
  const rawVariant = String(slide.frontmatter?.variant ?? '').trim()
153
173
  const arrangement = String(slide.frontmatter?.arrangement ?? '').trim()
154
- const ref = slideRef(slide, index)
174
+ const localVariant = layoutVariants[`${layout}:${rawVariant}`]
175
+
176
+ if (localVariant) {
177
+ const content = stripAuthorNotes(slide.content ?? '')
178
+
179
+ try {
180
+ compileLayoutAuthoring({
181
+ props: {
182
+ layout,
183
+ variant: rawVariant,
184
+ arrangement,
185
+ mode: slide.frontmatter?.mode,
186
+ tone: slide.frontmatter?.tone ?? '',
187
+ },
188
+ frontmatter: slide.frontmatter ?? {},
189
+ children: markdownToVnodes(content),
190
+ components,
191
+ layoutVariants,
192
+ defaultTone: 'blue',
193
+ })
194
+ }
195
+ catch (error) {
196
+ const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined
197
+ issues.push({
198
+ ...ref,
199
+ message: error instanceof Error ? error.message : String(error),
200
+ hint: typeof hint === 'string' ? hint : undefined,
201
+ })
202
+ }
203
+ continue
204
+ }
205
+
155
206
  /** @type {string} */
156
207
  let variant
157
208
 
@@ -159,10 +210,26 @@ async function validateDeckLayouts(deckPath) {
159
210
  variant = resolveLayoutVariant(layout, rawVariant, arrangement)
160
211
  }
161
212
  catch (error) {
162
- const hint = error && typeof error === 'object' && 'hint' in error ? error.hint : undefined
213
+ let reportedError = error
214
+ if (rawVariant && !hasLayoutVariant(layout, rawVariant)) {
215
+ try {
216
+ resolveDeckLayoutVariant({
217
+ layout,
218
+ variant: rawVariant,
219
+ arrangement,
220
+ variants: layoutVariants,
221
+ })
222
+ }
223
+ catch (deckVariantError) {
224
+ reportedError = deckVariantError
225
+ }
226
+ }
227
+ const hint = reportedError && typeof reportedError === 'object' && 'hint' in reportedError
228
+ ? reportedError.hint
229
+ : undefined
163
230
  issues.push({
164
231
  ...ref,
165
- message: error instanceof Error ? error.message : String(error),
232
+ message: reportedError instanceof Error ? reportedError.message : String(reportedError),
166
233
  hint: typeof hint === 'string' ? hint : undefined,
167
234
  })
168
235
  continue
@@ -197,6 +264,7 @@ async function validateDeckLayouts(deckPath) {
197
264
  frontmatter: slide.frontmatter ?? {},
198
265
  children: liveStructure.children.map(node => liveNodeToVNode(node, components)),
199
266
  components,
267
+ layoutVariants,
200
268
  defaultTone: 'blue',
201
269
  })
202
270
  validateExplicitSlotIntents(liveStructure.intents, {
@@ -233,6 +301,7 @@ async function validateDeckLayouts(deckPath) {
233
301
  frontmatter: slide.frontmatter ?? {},
234
302
  children: markdownToVnodes(content),
235
303
  components,
304
+ layoutVariants,
236
305
  defaultTone: 'blue',
237
306
  })
238
307
  }
@@ -264,7 +333,9 @@ function formatDeckLayoutIssues(issues, deckPath = 'deck') {
264
333
  ]
265
334
  if (issue.hint)
266
335
  lines.push(` Подсказка: ${issue.hint}`)
267
- lines.push(' См. example.md в slidev-theme-practicum (заметки «Контракт»).')
336
+ lines.push(issue.hint?.includes('components/layout-variants/')
337
+ ? ' См. README.md в slidev-theme-practicum, раздел «Локальные варианты презентации».'
338
+ : ' См. example.md в slidev-theme-practicum (заметки «Контракт»).')
268
339
  return lines.join('\n')
269
340
  }).join(`\n\n${'—'.repeat(40)}\n\n`)
270
341
 
package/env.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+
4
+ const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
5
+ export default component
6
+ }
7
+
8
+ declare const __PRATICUM_DECK_DECORS__: readonly Record<string, unknown>[]
9
+
10
+ declare module 'virtual:practicum-deck-layout-variants' {
11
+ import type { Component } from 'vue'
12
+
13
+ export const DECK_LAYOUT_VARIANTS: Readonly<Record<string, Component>>
14
+ }
15
+
16
+ declare module 'virtual:practicum-deck-decors' {
17
+ export const DECK_FILE_DECORS: readonly Record<string, unknown>[]
18
+ }
package/example.md CHANGED
@@ -306,9 +306,6 @@ variant: title-supports-bottom-plain
306
306
  # Когда нужны два самостоятельных абзаца
307
307
 
308
308
  - Этот вариант снимает приглушение с нижних опор. Каждый абзац читается как самостоятельный текст: его не воспринимают как «второстепенный к заголовку», а ставят рядом по весу.
309
- - авава
310
- - ава
311
- авава
312
309
  - Берите слайд, когда тезис распадается на два независимых направления: например, «что включить» и «чего избегать», «теория» и «практика», «причина» и «следствие». Третий блок не нужен — два аргумента уже задают сильную смысловую пару.
313
310
 
314
311
  ---
@@ -467,10 +464,143 @@ decor:
467
464
 
468
465
  # Оранжевый тон: акцент
469
466
 
470
- 1. Выносите важный поворот или предупреждение
467
+ 1. Подсвечивайте один критический поворот
471
468
  2. Не окрашивайте им весь ряд одинаковых фактов
472
469
  3. Оставляйте рядом достаточно воздуха
473
470
 
471
+ ---
472
+ layout: collection
473
+ variant: comparison
474
+ arrangement: before-after
475
+ comparison:
476
+ from:
477
+ kicker: До разбора
478
+ title: «Я просто запомнил формулу»
479
+ body: Ответ верный, но ученик не может объяснить выбор метода.
480
+ to:
481
+ kicker: После разбора
482
+ title: «Я выбрал формулу по условию»
483
+ body: Ученик связывает данные, метод и проверку результата.
484
+ relation:
485
+ label: объяснить ход мысли
486
+ ---
487
+
488
+ <!-- Контракт: comparison before-after — заголовок в Markdown, две стороны и связь во frontmatter. -->
489
+
490
+ # Как обратная связь превращает ответ в решение
491
+
492
+ ---
493
+ layout: collection
494
+ variant: comparison
495
+ arrangement: stable-variable
496
+ comparison:
497
+ from:
498
+ kicker: Сохраняем
499
+ title: Научиться проверять гипотезу
500
+ body: Результат занятия остаётся общим для любой группы.
501
+ to:
502
+ kicker: Меняем
503
+ title: Данные и уровень подсказок
504
+ body: Контекст и поддержка зависят от опыта студентов.
505
+ relation:
506
+ label: одна цель, разные маршруты
507
+ ---
508
+
509
+ <!-- Контракт: comparison stable-variable — тот же shorthand, но вертикальная связь. -->
510
+
511
+ # Как адаптировать практику, сохраняя учебную цель
512
+
513
+ ---
514
+ layout: collection
515
+ variant: steps
516
+ arrangement: linear
517
+ items:
518
+ - title: Назвать цель
519
+ body: Что студент сделает сам
520
+ - title: Дать контекст
521
+ body: Где возникает задача
522
+ - title: Показать пример
523
+ body: Один разобранный ход
524
+ - title: Оставить практику
525
+ body: Задание без подсказки
526
+ - title: Проверить критерий
527
+ body: Видимый признак качества
528
+ active: true
529
+ - title: Закрепить вывод
530
+ body: Что перенести дальше
531
+ ---
532
+
533
+ <!-- Контракт: steps linear — ровно 6 шагов во frontmatter, один из них active. -->
534
+
535
+ # Шесть шагов учебного объяснения
536
+
537
+ ---
538
+ layout: collection
539
+ variant: steps
540
+ arrangement: staggered
541
+ items:
542
+ - label: Сигнал
543
+ title: Заметить сбой
544
+ body: Зафиксировать наблюдение
545
+ - label: Факт
546
+ title: Повторить шаг
547
+ body: Отделить случайность
548
+ - label: Причина
549
+ title: Найти правило
550
+ body: Объяснить механизм
551
+ active: true
552
+ - label: Решение
553
+ title: Изменить подход
554
+ body: Проверить новый ход
555
+ - label: Перенос
556
+ title: Записать вывод
557
+ body: Применить в другой задаче
558
+ ---
559
+
560
+ <!-- Контракт: steps staggered — ровно 5 шагов во frontmatter для ритма 3+2. -->
561
+
562
+ # Как команда разбирает ошибку
563
+
564
+ ---
565
+ layout: collection
566
+ variant: metrics
567
+ arrangement: dashboard
568
+ metrics:
569
+ - value: '84%'
570
+ label: завершили тренажёр
571
+ - value: '18 мин'
572
+ label: медиана выполнения
573
+ - value: '3'
574
+ label: попытки до зачёта
575
+ - value: '12'
576
+ label: вопросов разобрали
577
+ - value: '+21 п. п.'
578
+ label: к входной диагностике
579
+ ---
580
+
581
+ <!-- Контракт: metrics dashboard — ровно 5 метрик во frontmatter, без media и ручных spans. -->
582
+
583
+ # Что изменилось после практикума
584
+
585
+ ---
586
+ layout: collection
587
+ variant: facts
588
+ arrangement: numbered-quartet
589
+ ---
590
+
591
+ <!-- Контракт: facts numbered-quartet — заголовок и 4 вложенных Markdown-факта; номера добавляет тема. -->
592
+
593
+ # Четыре вопроса перед публикацией задания
594
+
595
+ - Цель
596
+ - Что студент сделает самостоятельно
597
+ - Контекст
598
+ - Почему задача нужна именно сейчас
599
+ - Критерий
600
+ - Как выглядит проверяемый результат
601
+ - Обратная связь
602
+ - Что поможет улучшить следующую попытку
603
+
474
604
  ---
475
605
  layout: collection
476
606
  variant: metrics
@@ -717,7 +847,7 @@ header: default
717
847
  </Slot>
718
848
 
719
849
  <Slot area="6 / 1 / -1 / 7" surface="color" tone="orange" margin="4" gap="2">
720
- <Text size="5">Цветная поверхность</Text>
850
+ <Text size="5">Акцентная поверхность</Text>
721
851
  <Text size="3">Слот целиком красится в текущий акцентный цвет — для смыслового выделения.</Text>
722
852
  </Slot>
723
853
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-theme-practicum",
3
- "version": "0.1.4",
3
+ "version": "0.3.0",
4
4
  "description": "Strict Slidev presentation theme with a 12x12 grid, editorial layouts, and low-level slot composition API",
5
5
  "keywords": [
6
6
  "slidev-theme",
@@ -25,6 +25,7 @@
25
25
  "styles",
26
26
  "types",
27
27
  "skills",
28
+ "env.d.ts",
28
29
  "LICENSE",
29
30
  "README.md",
30
31
  "example.md",
@@ -64,12 +65,13 @@
64
65
  "lint": "eslint .",
65
66
  "lint:fix": "eslint . --fix",
66
67
  "validate-deck": "node scripts/validate-deck.cjs",
67
- "test": "npm run lint && npm run typecheck && npm run test:unit && npm run test:architecture && npm run test:build && npm run test:build-artifact && npm run test:browser && npm run test:package",
68
+ "test": "npm run lint && npm run typecheck && npm run test:unit && npm run test:architecture && npm run test:build && npm run test:layout-variant-build && npm run test:build-artifact && npm run test:browser && npm run test:package",
68
69
  "typecheck": "vue-tsc --noEmit",
69
70
  "test:architecture": "node scripts/check-test-architecture.mjs",
70
71
  "test:unit": "node --test tests/*.test.cjs",
71
72
  "test:validate-deck": "node scripts/validate-deck.cjs example.md",
72
73
  "test:build": "slidev build example.md",
74
+ "test:layout-variant-build": "node scripts/check-local-layout-variant-build.mjs",
73
75
  "test:build-artifact": "node scripts/check-build-artifact.mjs",
74
76
  "test:browser": "node scripts/browser-smoke.mjs",
75
77
  "test:package": "node scripts/check-package.mjs"
@@ -81,7 +83,8 @@
81
83
  "@slidev/types": "^52.14.2",
82
84
  "markdown-it": "^14.1.0",
83
85
  "typescript": "^6.0.3",
84
- "vue": "^3.5.13"
86
+ "vue": "^3.5.13",
87
+ "yaml": "^2.9.0"
85
88
  },
86
89
  "devDependencies": {
87
90
  "@eslint/js": "^10.0.1",