slidev-theme-practicum 0.2.0 → 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 (35) hide show
  1. package/README.md +147 -18
  2. package/components/Slide.vue +11 -57
  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 +16 -2
  9. package/composables/layout-shorthands.ts +6 -17
  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/package.json +6 -3
  17. package/scripts/browser-smoke.mjs +65 -0
  18. package/scripts/check-local-layout-variant-build.mjs +189 -0
  19. package/scripts/check-package.mjs +18 -2
  20. package/scripts/requirements-illustrations.txt +1 -0
  21. package/scripts/trace-line-art.py +422 -0
  22. package/scripts/validate-deck.cjs +1 -1
  23. package/setup/vite-plugins.ts +109 -2
  24. package/skills/slidev-practicum/SKILL.md +18 -4
  25. package/skills/slidev-practicum/references/contour-illustrations.md +114 -0
  26. package/skills/slidev-practicum/references/deck-project-structure.md +136 -0
  27. package/skills/slidev-practicum/references/illustration-examples/balance-scales.png +0 -0
  28. package/skills/slidev-practicum/references/illustration-examples/balance-scales.svg +88 -0
  29. package/skills/slidev-practicum/references/illustration-examples/chainsaw.png +0 -0
  30. package/skills/slidev-practicum/references/illustration-examples/chainsaw.svg +4 -0
  31. package/skills/slidev-practicum/references/illustration-examples/graduation-cap.png +0 -0
  32. package/skills/slidev-practicum/references/illustration-examples/graduation-cap.svg +23 -0
  33. package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.png +0 -0
  34. package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.svg +4 -0
  35. package/skills/slidev-practicum/references/photographic-illustrations.md +100 -0
@@ -10,9 +10,28 @@ import {
10
10
 
11
11
  const PROJECT_ROOT = resolve(import.meta.dirname, '..')
12
12
  const DIST_DIR = resolve(PROJECT_ROOT, 'dist')
13
+ const EXPECTED_DOCUMENT_TITLE = 'Яндекс Практикум — справочная галерея - Slidev'
13
14
  const REPRESENTATIVE_SLIDES = [1, 9, 15, 20, 38, 46, 47]
14
15
  const VISUAL_ONLY_SLIDES = [17, 18]
15
16
  const P0_REGRESSION_SLIDES = [23, 24, 25, 26, 27, 28]
17
+ const P0_FRONTMATTER_TEXT = {
18
+ 23: {
19
+ selector: '.Slide_variant_comparison-before-after',
20
+ values: ['«Я просто запомнил формулу»', '«Я выбрал формулу по условию»'],
21
+ },
22
+ 24: {
23
+ selector: '.Slide_variant_comparison-stable-variable',
24
+ values: ['Научиться проверять гипотезу', 'Данные и уровень подсказок'],
25
+ },
26
+ 25: {
27
+ selector: '.Slide-StepsGrid_linear',
28
+ values: ['Назвать цель', 'Дать контекст', 'Показать пример', 'Оставить практику', 'Проверить критерий', 'Закрепить вывод'],
29
+ },
30
+ 26: {
31
+ selector: '.Slide-StepsGrid_staggered',
32
+ values: ['Заметить сбой', 'Повторить шаг', 'Найти правило', 'Изменить подход', 'Записать вывод'],
33
+ },
34
+ }
16
35
  const LIFECYCLE_EVENT = 'practicum:slide-lifecycle'
17
36
  const LIFECYCLE_STATE_KEY = '__practicumBrowserLifecycle'
18
37
  const THEME_MEDIA = [
@@ -40,6 +59,10 @@ function assertNoDiagnostics(label, diagnostics) {
40
59
  )
41
60
  }
42
61
 
62
+ function normalizeVisibleText(value) {
63
+ return value.replace(/\s+/g, ' ').trim()
64
+ }
65
+
43
66
  async function settleSlide(page) {
44
67
  await page.evaluate(async () => {
45
68
  await Promise.race([
@@ -251,6 +274,40 @@ async function assertNumberedQuartet(slide, slideNumber) {
251
274
  )
252
275
  }
253
276
 
277
+ async function assertP0FrontmatterText(slide, slideNumber) {
278
+ const contract = P0_FRONTMATTER_TEXT[slideNumber]
279
+ if (!contract)
280
+ return
281
+
282
+ assert.equal(
283
+ await slide.locator(contract.selector).count(),
284
+ 1,
285
+ `слайд ${slideNumber}: не найден ожидаемый variant/arrangement ${contract.selector}`,
286
+ )
287
+
288
+ const visibleText = await slide.locator('.Text').evaluateAll(elements => elements
289
+ .filter((element) => {
290
+ const rect = element.getBoundingClientRect()
291
+ const style = getComputedStyle(element)
292
+
293
+ return rect.width > 0
294
+ && rect.height > 0
295
+ && style.display !== 'none'
296
+ && style.visibility !== 'hidden'
297
+ && style.opacity !== '0'
298
+ })
299
+ .map(element => element.textContent ?? ''))
300
+ const normalizedVisibleText = visibleText.map(normalizeVisibleText)
301
+
302
+ for (const expected of contract.values) {
303
+ assert.ok(
304
+ normalizedVisibleText.includes(normalizeVisibleText(expected)),
305
+ `слайд ${slideNumber}: вложенный текст front matter «${expected}» не виден; `
306
+ + `видимые Text=${JSON.stringify(normalizedVisibleText)}`,
307
+ )
308
+ }
309
+ }
310
+
254
311
  async function inspectSlide(
255
312
  page,
256
313
  origin,
@@ -315,6 +372,7 @@ async function inspectSlide(
315
372
  await assertStaggeredStepsLayout(slide, slideNumber)
316
373
  if (slideNumber === 28)
317
374
  await assertNumberedQuartet(slide, slideNumber)
375
+ await assertP0FrontmatterText(slide, slideNumber)
318
376
  assertLifecycleTransition(
319
377
  beforeLifecycle,
320
378
  afterLifecycle,
@@ -474,6 +532,13 @@ export async function runBrowserSmoke() {
474
532
  REPRESENTATIVE_SLIDES[index - 1],
475
533
  () => inFlightTracker.waitForIdle(),
476
534
  )
535
+ if (index === 0) {
536
+ assert.equal(
537
+ await page.title(),
538
+ EXPECTED_DOCUMENT_TITLE,
539
+ 'первый headmatter title должен задавать служебный HTML title колоды',
540
+ )
541
+ }
477
542
  }
478
543
 
479
544
  for (const slideNumber of VISUAL_ONLY_SLIDES) {
@@ -0,0 +1,189 @@
1
+ import assert from 'node:assert/strict'
2
+ import { spawnSync } from 'node:child_process'
3
+ import {
4
+ cpSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ rmSync,
10
+ } from 'node:fs'
11
+ import { join, resolve } from 'node:path'
12
+ import process from 'node:process'
13
+ import { chromium } from 'playwright-chromium'
14
+ import { createStaticDistServer } from './browser-smoke-runtime.mjs'
15
+
16
+ const PROJECT_ROOT = resolve(import.meta.dirname, '..')
17
+ const FIXTURE_ROOT = join(PROJECT_ROOT, 'tests/fixtures/local-layout-variant-deck')
18
+ const SLIDEV_CLI = join(PROJECT_ROOT, 'node_modules/@slidev/cli/bin/slidev.mjs')
19
+ const consumerParent = join(PROJECT_ROOT, '.slidev')
20
+ mkdirSync(consumerParent, { recursive: true })
21
+ const consumerRoot = mkdtempSync(join(consumerParent, 'layout-variant-consumer-'))
22
+ const deckRoot = join(consumerRoot, 'deck')
23
+ const outputRoot = join(deckRoot, 'dist')
24
+
25
+ const FRONTMATTER_SLIDES = [
26
+ {
27
+ number: 2,
28
+ selector: '.Slide-StepsGrid_linear',
29
+ values: ['Найти нужную версию', 'Восстановить контекст', 'Сгенерировать черновик', 'Проверить', 'Исправить', 'Перенести результат'],
30
+ },
31
+ {
32
+ number: 3,
33
+ selector: '.Slide-StepsGrid_staggered',
34
+ values: ['Заметить сбой', 'Повторить шаг', 'Найти правило', 'Изменить подход', 'Записать вывод'],
35
+ },
36
+ {
37
+ number: 4,
38
+ selector: '.Slide_variant_comparison-before-after',
39
+ values: ['Решение — за человеком', 'Стадия работает сама'],
40
+ },
41
+ {
42
+ number: 5,
43
+ selector: '.Slide_variant_comparison-stable-variable',
44
+ values: ['Научиться проверять гипотезу', 'Данные и уровень подсказок'],
45
+ },
46
+ ]
47
+
48
+ function normalizeVisibleText(value) {
49
+ return value.replace(/\s+/g, ' ').trim()
50
+ }
51
+
52
+ function readArtifact(directory) {
53
+ return readdirSync(directory, { withFileTypes: true })
54
+ .flatMap((entry) => {
55
+ const path = join(directory, entry.name)
56
+ return entry.isDirectory() ? readArtifact(path) : [readFileSync(path)]
57
+ })
58
+ }
59
+
60
+ async function inspectRenderedVariant() {
61
+ const diagnostics = []
62
+ const server = await createStaticDistServer(outputRoot)
63
+ const browser = await chromium.launch({ headless: true })
64
+
65
+ try {
66
+ const context = await browser.newContext({ serviceWorkers: 'block' })
67
+ await context.grantPermissions(['screen-wake-lock'], { origin: server.origin })
68
+ const page = await context.newPage()
69
+ page.on('console', (message) => {
70
+ if (message.type() === 'error')
71
+ diagnostics.push(message.text())
72
+ })
73
+ page.on('pageerror', error => diagnostics.push(error.message))
74
+
75
+ await page.goto(`${server.origin}/1`, { waitUntil: 'networkidle' })
76
+
77
+ const firstSlide = page.locator('.slidev-page[data-slidev-no="1"]')
78
+ const badge = firstSlide.locator('.LocalLayoutVariantBadge')
79
+ await firstSlide.waitFor({ state: 'visible' })
80
+ await badge.waitFor({ state: 'visible' })
81
+
82
+ assert.equal((await badge.textContent())?.trim(), 'Практика')
83
+ assert.equal((await firstSlide.locator('h1').textContent())?.trim(), 'Что запомнить')
84
+
85
+ for (const contract of FRONTMATTER_SLIDES) {
86
+ await page.goto(`${server.origin}/${contract.number}`, { waitUntil: 'networkidle' })
87
+ const slide = page.locator(`.slidev-page[data-slidev-no="${contract.number}"]`)
88
+ await slide.waitFor({ state: 'visible' })
89
+
90
+ assert.equal(
91
+ await slide.locator(contract.selector).count(),
92
+ 1,
93
+ `слайд ${contract.number}: не найден ${contract.selector}`,
94
+ )
95
+
96
+ const rendered = await slide.locator('.Text').evaluateAll(elements => elements
97
+ .filter((element) => {
98
+ const rect = element.getBoundingClientRect()
99
+ const style = getComputedStyle(element)
100
+
101
+ return rect.width > 0
102
+ && rect.height > 0
103
+ && style.display !== 'none'
104
+ && style.visibility !== 'hidden'
105
+ && style.opacity !== '0'
106
+ })
107
+ .map(element => element.textContent ?? ''))
108
+ const normalized = rendered.map(normalizeVisibleText)
109
+
110
+ for (const expected of contract.values) {
111
+ assert.ok(
112
+ normalized.includes(normalizeVisibleText(expected)),
113
+ `слайд ${contract.number}: вложенный текст front matter «${expected}» не виден; `
114
+ + `видимые Text=${JSON.stringify(normalized)}`,
115
+ )
116
+ }
117
+
118
+ const overflow = await slide.evaluate(element => ({
119
+ x: element.scrollWidth - element.clientWidth,
120
+ y: element.scrollHeight - element.clientHeight,
121
+ }))
122
+ assert.ok(overflow.x <= 1, `слайд ${contract.number}: горизонтальное переполнение ${overflow.x}px`)
123
+ assert.ok(overflow.y <= 1, `слайд ${contract.number}: вертикальное переполнение ${overflow.y}px`)
124
+ }
125
+
126
+ assert.deepEqual(diagnostics, [], diagnostics.join('\n'))
127
+ }
128
+ finally {
129
+ await browser.close()
130
+ await server.close()
131
+ }
132
+ }
133
+
134
+ try {
135
+ cpSync(FIXTURE_ROOT, deckRoot, { recursive: true })
136
+ const themeInstallRoot = join(deckRoot, 'node_modules', 'slidev-theme-practicum')
137
+ const manifest = JSON.parse(readFileSync(join(PROJECT_ROOT, 'package.json'), 'utf8'))
138
+
139
+ mkdirSync(themeInstallRoot, { recursive: true })
140
+ cpSync(join(PROJECT_ROOT, 'package.json'), join(themeInstallRoot, 'package.json'))
141
+ for (const entry of manifest.files)
142
+ cpSync(join(PROJECT_ROOT, entry), join(themeInstallRoot, entry), { recursive: true })
143
+
144
+ const deckPath = join(deckRoot, 'slides.md')
145
+ const validation = spawnSync(process.execPath, [
146
+ join(themeInstallRoot, 'scripts', 'validate-deck.cjs'),
147
+ deckPath,
148
+ ], {
149
+ cwd: deckRoot,
150
+ encoding: 'utf8',
151
+ })
152
+
153
+ assert.equal(
154
+ validation.status,
155
+ 0,
156
+ validation.stderr || validation.stdout || validation.error?.message,
157
+ )
158
+
159
+ const result = spawnSync(process.execPath, [
160
+ SLIDEV_CLI,
161
+ 'build',
162
+ deckPath,
163
+ '--out',
164
+ outputRoot,
165
+ ], {
166
+ cwd: deckRoot,
167
+ encoding: 'utf8',
168
+ })
169
+
170
+ if (result.error || result.status !== 0) {
171
+ process.stderr.write(result.stderr || result.stdout || result.error?.message || 'Сборка локального варианта завершилась без диагностики.\n')
172
+ process.exitCode = result.status || 1
173
+ }
174
+ else {
175
+ const artifact = Buffer.concat(readArtifact(outputRoot)).toString()
176
+
177
+ if (!artifact.includes('LocalLayoutVariantBadge')) {
178
+ console.error('Сборка не содержит локальный компонент, вызванный из варианта презентации.')
179
+ process.exitCode = 1
180
+ }
181
+ else {
182
+ await inspectRenderedVariant()
183
+ console.log('Локальный вариант и вложенные поля front matter отрисовались в итоговой сборке установленной темы.')
184
+ }
185
+ }
186
+ }
187
+ finally {
188
+ rmSync(consumerRoot, { recursive: true, force: true })
189
+ }
@@ -83,12 +83,28 @@ try {
83
83
  if (!files.has('LICENSE'))
84
84
  errors.push('package должен содержать LICENSE')
85
85
 
86
- if (!files.has('skills/slidev-practicum/SKILL.md'))
87
- errors.push('package должен содержать skills/slidev-practicum/SKILL.md')
86
+ for (const skillFile of [
87
+ 'skills/slidev-practicum/SKILL.md',
88
+ 'skills/slidev-practicum/references/deck-project-structure.md',
89
+ 'skills/slidev-practicum/references/contour-illustrations.md',
90
+ 'skills/slidev-practicum/references/photographic-illustrations.md',
91
+ ]) {
92
+ if (!files.has(skillFile))
93
+ errors.push(`package должен содержать ${skillFile}`)
94
+ }
88
95
 
89
96
  if (!files.has('example.md'))
90
97
  errors.push('package должен содержать example.md как каноническую колоду для авторов')
91
98
 
99
+ for (const runtimeFile of [
100
+ 'composables/local-layout-variant-files.ts',
101
+ 'composables/local-layout-variants.ts',
102
+ 'env.d.ts',
103
+ ]) {
104
+ if (!files.has(runtimeFile))
105
+ errors.push(`package должен содержать контракт локальных вариантов: ${runtimeFile}`)
106
+ }
107
+
92
108
  const defaultDeckTargets = new Set(
93
109
  Object.values(manifest.scripts ?? {})
94
110
  .flatMap(script => String(script).match(/\b[\w-]+\.md\b/g) ?? []),
@@ -0,0 +1 @@
1
+ Pillow==12.2.0