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
@@ -0,0 +1,134 @@
1
+ import { inspectTypography } from '../composables/typography-guard.cjs'
2
+
3
+ export async function settleTypography(slide) {
4
+ await slide.evaluate(async (root) => {
5
+ const timeout = new Promise((_, reject) => setTimeout(
6
+ () => reject(new Error('Шрифты или изображения не загрузились за 5 секунд.')), 5_000,
7
+ ))
8
+ await Promise.race([
9
+ Promise.all([
10
+ document.fonts.ready,
11
+ ...[...root.querySelectorAll('img')].map(image => image.decode().catch(() => undefined)),
12
+ ]),
13
+ timeout,
14
+ ])
15
+ await new Promise((resolveStable, rejectStable) => {
16
+ const deadline = performance.now() + 5_000
17
+ let previous = ''
18
+ let stableFrames = 0
19
+ function observe() {
20
+ const signature = [...root.querySelectorAll('.Slot, .Slot-Content, .Text, .Text *')]
21
+ .map((element) => {
22
+ const style = getComputedStyle(element)
23
+ const rect = element.getBoundingClientRect()
24
+ return [element.className, element.textContent, style.fontSize, style.color,
25
+ rect.x, rect.y, rect.width, rect.height, element.scrollWidth, element.scrollHeight].join(':')
26
+ }).join('|')
27
+ stableFrames = signature === previous ? stableFrames + 1 : 0
28
+ previous = signature
29
+ if (stableFrames >= 8)
30
+ resolveStable()
31
+ else if (performance.now() >= deadline)
32
+ rejectStable(new Error('Типографика слайда не стабилизировалась за 5 секунд.'))
33
+ else
34
+ requestAnimationFrame(observe)
35
+ }
36
+ requestAnimationFrame(observe)
37
+ })
38
+ })
39
+ }
40
+
41
+ // Функция передаётся в браузер целиком, поэтому не использует внешние замыкания.
42
+ export function collectRenderedTypography(root) {
43
+ const content = root.querySelector('.Slide-Grid') ?? root.querySelector('.slidev-layout') ?? root
44
+ const excluded = '.Slide-Header, .Header, .Logo, .Slot-Background, .ImageRenderer, svg, img, canvas, video, audio, script, style'
45
+ const slots = [...content.querySelectorAll('.Slot')]
46
+ const texts = []
47
+ const issues = []
48
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT)
49
+ const canvas = document.createElement('canvas')
50
+ canvas.width = canvas.height = 1
51
+ const colorContext = canvas.getContext('2d', { willReadFrequently: true })
52
+ if (!colorContext)
53
+ throw new Error('Не удалось нормализовать вычисленные цвета в sRGB.')
54
+ const colors = new Map()
55
+
56
+ function normalizeColor(value) {
57
+ if (!colors.has(value)) {
58
+ colorContext.clearRect(0, 0, 1, 1)
59
+ colorContext.fillStyle = value
60
+ colorContext.fillRect(0, 0, 1, 1)
61
+ const [red, green, blue, alpha] = colorContext.getImageData(0, 0, 1, 1).data
62
+ colors.set(value, `rgba(${red}, ${green}, ${blue}, ${alpha / 255})`)
63
+ }
64
+ return colors.get(value)
65
+ }
66
+
67
+ function visible(element) {
68
+ for (let current = element; current; current = current.parentElement) {
69
+ const style = getComputedStyle(current)
70
+ if (style.display === 'none' || style.visibility !== 'visible' || Number(style.opacity) === 0)
71
+ return false
72
+ if (current === root)
73
+ break
74
+ }
75
+ return true
76
+ }
77
+
78
+ while (walker.nextNode()) {
79
+ const node = walker.currentNode
80
+ const element = node.parentElement
81
+ const text = node.textContent.replace(/\s+/gu, ' ').trim().slice(0, 100)
82
+ if (!text || !element || element.closest(excluded) || !visible(element))
83
+ continue
84
+ const range = document.createRange()
85
+ range.selectNodeContents(node)
86
+ const rects = [...range.getClientRects()].filter(rect => rect.width > 0 && rect.height > 0)
87
+ if (!rects.length)
88
+ continue
89
+ const slotElement = element.closest('.Slot')
90
+ const slot = slotElement ? `Slot ${slots.indexOf(slotElement) + 1}` : 'Слайд вне Slot'
91
+ const style = getComputedStyle(element)
92
+ const textElement = element.closest('.Text')
93
+ texts.push({
94
+ text,
95
+ slot,
96
+ size: `${Math.round(Number.parseFloat(style.fontSize) * 100) / 100}px`,
97
+ color: normalizeColor(style.color),
98
+ keyNumber: textElement?.tagName === 'DATA',
99
+ muted: Boolean(element.closest('.Text_muted, [data-text-color^="text-muted"]')),
100
+ })
101
+
102
+ // Проверяем также внешний Slot: вложенная композиция не должна вытекать из него.
103
+ for (let current = slotElement; current; current = current.parentElement?.closest('.Slot')) {
104
+ const boundsElement = current.querySelector(':scope > .Slot-Content') ?? current
105
+ const bounds = boundsElement.getBoundingClientRect()
106
+ const scaleY = current.getBoundingClientRect().height / current.offsetHeight || 1
107
+ const lineHeight = Number.parseFloat(style.lineHeight) * scaleY
108
+ const overflow = Math.max(0, ...rects.flatMap((rect) => {
109
+ // Range включает метрики шрифта за пределами CSS-строки даже у
110
+ // корректного заголовка. По вертикали проверяем именно строку.
111
+ const leading = Number.isFinite(lineHeight) ? Math.max(0, rect.height - lineHeight) / 2 : 0
112
+ return [
113
+ bounds.left - rect.left, rect.right - bounds.right,
114
+ bounds.top - rect.top - leading, rect.bottom - leading - bounds.bottom,
115
+ ]
116
+ }))
117
+ if (overflow > 1) {
118
+ issues.push({
119
+ code: 'text-overflow',
120
+ message: `Slot ${slots.indexOf(current) + 1}: «${text}» выходит за границы текста слота на ${overflow.toFixed(1)} px.`,
121
+ hint: 'Сократите текст или измените композицию, сохранив число размеров.',
122
+ })
123
+ break
124
+ }
125
+ }
126
+ }
127
+ return { texts, issues }
128
+ }
129
+
130
+ export async function inspectRenderedTypography(slide) {
131
+ await settleTypography(slide)
132
+ const { texts, issues } = await slide.evaluate(collectRenderedTypography)
133
+ return { textCount: texts.length, issues: [...issues, ...inspectTypography(texts)] }
134
+ }
@@ -1,12 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  const { resolve } = require('node:path')
3
+ const { parseArgs } = require('node:util')
3
4
  const { formatDeckLayoutIssues, validateDeckLayouts } = require('../composables/validate-deck-layouts.cjs')
4
5
 
5
- const deckPath = resolve(process.argv[2] ?? 'example.md')
6
+ const { values, positionals } = parseArgs({
7
+ allowPositionals: true,
8
+ options: {
9
+ 'typography-guard': { type: 'boolean', default: false },
10
+ 'help': { type: 'boolean', short: 'h' },
11
+ },
12
+ })
13
+ if (values.help) {
14
+ console.log('Использование: slidev-practicum-validate [slides.md] [--typography-guard]\n'
15
+ + '--typography-guard проверяет объявленные политики Text. Фактический результат проверяйте командой slidev-practicum-check-typography.')
16
+ process.exit(0)
17
+ }
18
+ if (positionals.length > 1) {
19
+ console.error('Укажите один файл колоды.')
20
+ process.exit(1)
21
+ }
22
+ const deckPath = resolve(positionals[0] ?? 'example.md')
23
+ const typographyGuard = values['typography-guard']
6
24
 
7
- validateDeckLayouts(deckPath).then((issues) => {
25
+ validateDeckLayouts(deckPath, { typographyGuard }).then((issues) => {
8
26
  if (!issues.length) {
9
- console.log(`OK: ${deckPath} — контракты layout/markdown соблюдены.`)
27
+ console.log(`OK: ${deckPath} — контракты макетов, Markdown и локальных вариантов соблюдены.`)
28
+ if (typographyGuard)
29
+ console.log('Объявленные политики Text соблюдены. Для размеров после подбора, Markdown и компонентов выполните slidev-practicum-check-typography.')
10
30
  process.exit(0)
11
31
  }
12
32
 
@@ -1,19 +1,124 @@
1
- import { resolve } from 'node:path'
1
+ import { relative, resolve, sep } from 'node:path'
2
2
  import type { ResolvedSlidevOptions } from '@slidev/types'
3
3
  import type { Plugin } from 'vite'
4
+ import {
5
+ collectDeckDecorFiles,
6
+ DECK_DECORS_VIRTUAL_ID,
7
+ resolveDeckFileDecors,
8
+ RESOLVED_DECK_DECORS_VIRTUAL_ID,
9
+ } from '../composables/deck-decors'
4
10
  import { createFileDecorStore } from '../composables/decor-file-store'
11
+ import { isRecord } from '../composables/decor-sources'
5
12
  import { createDecorSaveMiddleware } from './decor-save-middleware'
13
+ import {
14
+ createDeckLayoutVariantModuleSource,
15
+ deckLayoutVariantsDirectory,
16
+ DECK_LAYOUT_VARIANTS_VIRTUAL_ID,
17
+ discoverDeckLayoutVariantFiles,
18
+ RESOLVED_DECK_LAYOUT_VARIANTS_VIRTUAL_ID,
19
+ } from '../composables/local-layout-variant-files'
6
20
 
7
21
  const OUTPUT_PATH = resolve(process.cwd(), 'composables/decor-tuning-overrides.mjs')
8
22
 
9
23
  type DecorLibraryVitePluginContext = Pick<ResolvedSlidevOptions, 'data'>
24
+ & Partial<Pick<ResolvedSlidevOptions, 'userRoot'>>
25
+
26
+ function readThemeConfig(options?: DecorLibraryVitePluginContext) {
27
+ const value = options?.data?.config?.themeConfig
28
+ return isRecord(value) ? value : {}
29
+ }
10
30
 
11
31
  function readExpectedOrigin(options?: DecorLibraryVitePluginContext) {
12
- const value = options?.data.config.themeConfig.decorSaveOrigin
32
+ const value = readThemeConfig(options).decorSaveOrigin
13
33
 
14
34
  return typeof value === 'string' && value.trim() ? value : undefined
15
35
  }
16
36
 
37
+ function createDeckDecorsPlugin(options?: DecorLibraryVitePluginContext): Plugin {
38
+ const root = process.cwd()
39
+
40
+ function spec() {
41
+ return {
42
+ decors: readThemeConfig(options).decors,
43
+ root,
44
+ }
45
+ }
46
+
47
+ return {
48
+ name: 'practicum:deck-decors',
49
+ async config() {
50
+ return {
51
+ define: {
52
+ __PRATICUM_DECK_DECORS__: JSON.stringify(await resolveDeckFileDecors(spec())),
53
+ },
54
+ optimizeDeps: {
55
+ exclude: ['slidev-theme-practicum'],
56
+ },
57
+ }
58
+ },
59
+ resolveId(id) {
60
+ if (id === DECK_DECORS_VIRTUAL_ID)
61
+ return RESOLVED_DECK_DECORS_VIRTUAL_ID
62
+ },
63
+ async load(id) {
64
+ if (id !== RESOLVED_DECK_DECORS_VIRTUAL_ID)
65
+ return
66
+
67
+ const current = spec()
68
+ for (const path of collectDeckDecorFiles(current))
69
+ this.addWatchFile(path)
70
+
71
+ return `export const DECK_FILE_DECORS = ${JSON.stringify(await resolveDeckFileDecors(current))}\n`
72
+ },
73
+ configureServer(server) {
74
+ const watcher = server.watcher
75
+ if (!watcher?.add)
76
+ return
77
+
78
+ for (const path of collectDeckDecorFiles(spec()))
79
+ watcher.add(path)
80
+ },
81
+ }
82
+ }
83
+
84
+ function createDeckLayoutVariantsPlugin(options?: DecorLibraryVitePluginContext): Plugin {
85
+ const root = options?.userRoot ?? process.cwd()
86
+
87
+ return {
88
+ name: 'practicum:deck-layout-variants',
89
+ resolveId(id) {
90
+ if (id === DECK_LAYOUT_VARIANTS_VIRTUAL_ID)
91
+ return RESOLVED_DECK_LAYOUT_VARIANTS_VIRTUAL_ID
92
+ },
93
+ load(id) {
94
+ if (id !== RESOLVED_DECK_LAYOUT_VARIANTS_VIRTUAL_ID)
95
+ return
96
+
97
+ const files = discoverDeckLayoutVariantFiles(root)
98
+ for (const file of files)
99
+ this.addWatchFile(file.path)
100
+
101
+ return createDeckLayoutVariantModuleSource(files)
102
+ },
103
+ configureServer(server) {
104
+ server.watcher.add(deckLayoutVariantsDirectory(root))
105
+ },
106
+ handleHotUpdate(context) {
107
+ const relativeFile = relative(deckLayoutVariantsDirectory(root), context.file)
108
+ const segments = relativeFile.split(sep)
109
+ if (segments.length !== 2 || relativeFile.startsWith(`..${sep}`) || !context.file.endsWith('.vue'))
110
+ return
111
+
112
+ const virtualModule = context.server.moduleGraph.getModuleById(RESOLVED_DECK_LAYOUT_VARIANTS_VIRTUAL_ID)
113
+ if (virtualModule)
114
+ context.server.moduleGraph.invalidateModule(virtualModule)
115
+
116
+ context.server.ws.send({ type: 'full-reload' })
117
+ return []
118
+ },
119
+ }
120
+ }
121
+
17
122
  export default function decorLibraryVitePlugins(
18
123
  options?: DecorLibraryVitePluginContext,
19
124
  ): Plugin[] {
@@ -32,5 +137,7 @@ export default function decorLibraryVitePlugins(
32
137
  }))
33
138
  },
34
139
  },
140
+ createDeckDecorsPlugin(options),
141
+ createDeckLayoutVariantsPlugin(options),
35
142
  ]
36
143
  }
@@ -1,15 +1,17 @@
1
1
  ---
2
2
  name: slidev-practicum
3
- description: Используй, когда нужно создать, отредактировать, проверить или экспортировать Slidev-презентацию для slidev-theme-practicum, Яндекс Практикума, стиля Практикума или запроса вроде «слайды в стиле Практикума».
3
+ description: Используй для Slidev-презентаций, фотографических и предметных контурных иллюстраций в стиле slidev-theme-practicum или Яндекс Практикума.
4
4
  ---
5
5
 
6
6
  # Slidev Практикума
7
7
 
8
- Используй этот скилл как тонкий маршрутизатор для коротких запросов вроде «сделай слайды в стиле Практикума».
8
+ Используй этот скилл как тонкий маршрутизатор для колод, фотографических и контурных иллюстраций темы.
9
9
 
10
- **ОБЯЗАТЕЛЬНЫЙ ПОДСКИЛЛ:** используй `slidev` для платформенной механики: Markdown-синтаксиса, frontmatter, блоков кода, заметок, анимаций, dev-сервера, сборки и экспорта.
10
+ ## Колода
11
11
 
12
- Для авторинга в стиле Практикума не дублируй правила из документации темы. Читай локальный источник истины:
12
+ **ОБЯЗАТЕЛЬНЫЙ ПОДСКИЛЛ:** используй `slidev` для Markdown-синтаксиса, frontmatter, кода, заметок, анимаций, dev-сервера, сборки и экспорта.
13
+
14
+ Не дублируй правила темы. Читай локальный источник истины:
13
15
 
14
16
  - `README.md` — публичный авторский контракт, лейауты, компоненты, `themeConfig`, декор и правила контента.
15
17
  - `example.md` — канонические паттерны слайдов, которые нужно копировать и адаптировать.
@@ -20,6 +22,19 @@ description: Используй, когда нужно создать, отре
20
22
  - Внутри этого репозитория используй `theme: ./`.
21
23
  - Во внешней колоде с установленным пакетом используй `theme: practicum`.
22
24
  - Пиши по-русски, если пользователь не попросил другой язык.
25
+ - Если для колоды выбрана строгая типографика, прочитай раздел README темы «Строгая проверка типографики», включая пример слайда и порядок работы агента. Во внешней колоде источник — `node_modules/slidev-theme-practicum/README.md`. Проверяй объявления через `npm exec -- slidev-practicum-validate slides.md --typography-guard`, затем фактический результат свежей сборки через `npm exec -- slidev-practicum-check-typography slides.md --dist dist`. Отмечай ключевые числа явно через `Text as="data"`, не назначай это исключение датам и номерам шагов автоматически. Не считай статическую проверку достаточной для Markdown, динамических атрибутов и автоматического подбора размеров. После исправлений пересобери колоду и повтори проверки по порядку из README.
23
26
  - Выбирай тип слайда по задаче кадра, следуя `README.md`.
27
+ - Используй верхнеуровневый `title` только в первом headmatter как служебное название всей колоды. Не считай его видимым содержимым: пиши основной заголовок каждого кадра в теле слайда как Markdown `# …`, а для явной композиции — как видимый `<Text as="h1">…</Text>`. Вложенные `title` моделей компонентов сохраняй по контракту варианта.
28
+ - Для повторяющейся композиции конкретной колоды без Vue-тегов в `slides.md` используй `components/layout-variants/<layout>/<variant>.vue` и существующие поля front matter `layout` + `variant`; точный контракт бери из раздела README «Локальные варианты презентации».
29
+
30
+ Перед созданием структуры новой колоды, добавлением локального медиа или реорганизацией существующей колоды полностью прочитай `references/deck-project-structure.md`. Он владеет каноническим деревом проекта, классификацией `public/decor`, `public/photos`, `public/illustrations`, `public/figures`, правилами путей и именования. Не создавай альтернативные общие каталоги `assets` или `images`.
24
31
 
25
32
  Не используй `slidev-theme-architect-skill.md` для обычных колод; он нужен для создания или редизайна theme package.
33
+
34
+ ## Контурная иллюстрация
35
+
36
+ Перед созданием, перерисовкой или векторизацией предметной контурной иллюстрации полностью прочитай `references/contour-illustrations.md`. Он владеет референсами, растровой генерацией, трассировкой и приёмкой. Документы колоды загружай только если задача одновременно меняет слайд.
37
+
38
+ ## Фотографическая иллюстрация
39
+
40
+ Перед созданием, редактированием или приёмкой сюжетной фотографической иллюстрации полностью прочитай `references/photographic-illustrations.md`. Он владеет семантическими ролями изображений, распределением ролей между референсами, карточкой сложной сцены, масштабом, физическими контактами, перспективой жёстких объектов и проверкой увеличений. Документы конкретной колоды загружай только для её персонажей, предметов, окружения и сюжета.
@@ -0,0 +1,114 @@
1
+ # Контурные иллюстрации
2
+
3
+ Используй этот протокол для одного самостоятельного предмета в контурном стиле темы: создать, перерисовать или перевести в SVG. Многообъектной сцене нужен отдельный композиционный замысел.
4
+
5
+ Пути ниже относительны корню `slidev-theme-practicum`.
6
+
7
+ ## Результат
8
+
9
+ Одна и та же принятая иллюстрация сохраняется в двух форматах:
10
+
11
+ - PNG — чёрный растр с настоящим альфа-каналом;
12
+ - SVG — его векторная трассировка с `currentColor`, русским `<title>` и без встроенного растра.
13
+
14
+ Для иллюстрации конкретной колоды используй её `public/illustrations/<slug>.{png,svg}` по контракту `deck-project-structure.md`. Не перемещай контурный предмет в `public/decor` только потому, что он используется как декор. В `public/decor` темы добавляй только переиспользуемый графический декор; вместе с ним обнови каталог декора и прогони проверки темы.
15
+
16
+ ## Визуальный контракт
17
+
18
+ - Один узнаваемый предмет, целиком и крупно; безопасный отступ 5–8%.
19
+ - Чистая чёрная линия ровного оптического веса: строгая, гладкая, слегка органичная.
20
+ - Правдоподобные пропорции, перспектива и соединения деталей.
21
+ - Трёхчетвертной или диагональный ракурс, если он лучше раскрывает конструкцию; осевая фронтальность — когда важна симметрия.
22
+ - Прозрачный фон, одна краска, без теней, заливок, градиентов, фактур и подложки.
23
+ - Текст и символические атрибуты — только дословно из задания.
24
+ - Детализация сохраняет силуэт и обязательные соединения при размере 320 пикселей.
25
+
26
+ По умолчанию используй квадратный холст не меньше 1024×1024. Для явно широкого или высокого предмета выбери естественное соотношение сторон. Несколько размеров или вариантов одного предмета создавай только по отдельному запросу.
27
+
28
+ ## Референсы
29
+
30
+ Для характера линии и меры детализации выбери 2–4 подходящих SVG из:
31
+
32
+ ```text
33
+ public/decor/decor-{1,3,4,8,9,10,13,14,15,16,19}.svg
34
+ ```
35
+
36
+ Для цельности предмета и ожидаемого комплекта PNG/SVG используй:
37
+
38
+ ```text
39
+ skills/slidev-practicum/references/illustration-examples/{woodcutter-axe,chainsaw,graduation-cap,balance-scales}.{png,svg}
40
+ ```
41
+
42
+ PNG прикладывай генератору как стилевые референсы; SVG используй для проверки формата. Референс задаёт язык линии, а не предметное содержание.
43
+
44
+ ## Процесс
45
+
46
+ ### 1. Зафиксировать конструкцию
47
+
48
+ Одним абзацем перечисли узнаваемые части, их физические соединения, подходящий ракурс и нежелательные ассоциации. Шаг завершён, когда результат можно проверить не только «на похожесть», но и на связность конструкции.
49
+
50
+ ### 2. Получить растровый оригинал
51
+
52
+ Приложи выбранные PNG и передай генератору заполненный промпт:
53
+
54
+ ```text
55
+ Референсы задают только визуальный язык: строгую гладкую контурную линию, слегка органичный характер и меру детализации. Не копируй их предметы.
56
+
57
+ Нарисуй один отдельный объект: <ПРЕДМЕТ>. Обязательные детали и связи: <ДЕТАЛИ>. Ракурс: <РАКУРС>. Покажи объект целиком и крупно, с отступом 5–8%.
58
+
59
+ Чистая одноцветная чёрная линия ровного оптического веса; правдоподобные соединения и согласованная перспектива. Настоящий прозрачный фон, а если он недоступен — ровный белый. Без теней, заливок, градиентов, фактур, рамки, окружения, текста, логотипов и случайных символов. Нужен растровый оригинал высокого разрешения для последующей трассировки, не иконка, логотип, чертёж или SVG.
60
+
61
+ Исключи: <ИСКЛЮЧЕНИЯ>.
62
+ ```
63
+
64
+ Прими растр только после визуальной проверки: предмет узнаваем; пропорции, соединения и перспектива согласованы; силуэт читается; лишних объектов и подложки нет. Крупный дефект исправляй одной целевой перегенерацией, а не ручной правкой SVG.
65
+
66
+ ### 3. Очистить и трассировать
67
+
68
+ Канонический инструмент — `scripts/trace-line-art.py`; его стандартные параметры и справка являются источником истины. Зафиксированное окружение — `scripts/requirements-illustrations.txt`.
69
+
70
+ ```bash
71
+ python3 -m pip install -r scripts/requirements-illustrations.txt
72
+ python3 scripts/trace-line-art.py <исходник> <каталог>/<slug> --title "<русское название>"
73
+ ```
74
+
75
+ Трассировщик сам выбирает альфа-канал либо яркость, сохраняет очищенный PNG и строит SVG. Промежуточные маски вручную не нужны. Любое переопределение параметров укажи в отчёте.
76
+
77
+ ### 4. Принять комплект
78
+
79
+ Работа завершена, когда выполнены все условия:
80
+
81
+ - предмет узнаваем без подписи, конструктивно связен и читается при 320 пикселях;
82
+ - PNG прозрачен; SVG валиден и рендерится;
83
+ - SVG содержит `viewBox`, русский `<title>`, `currentColor` и векторные пути;
84
+ - внутри SVG нет `<image>`, `data:image` и внешнего `href` на растр;
85
+ - после рендера SVG в размере PNG каждая сторона ограничивающей рамки отличается не более чем на 3 пикселя;
86
+ - результат проверен чёрным на `#ffffff`, `#f0f0f0`, `#98d2fe` и белым на `#1e1e1e`;
87
+ - в отчёте есть пути к PNG/SVG, окончательный промпт, генератор и отклонения от стандартной трассировки.
88
+
89
+ ## Диагностика
90
+
91
+ | Симптом | Действие |
92
+ |---|---|
93
+ | Слабая форма, перспектива или соединения | Перегенерировать растр с одной поправкой |
94
+ | Белый фон или нарисованная шахматка | Подать исходник трассировщику; проверить очищенный PNG на цветной подложке |
95
+ | «Лесенка», гранёные окружности, шипы | Умеренно изменить допуск трассировки и зафиксировать параметры |
96
+ | Параллельные линии слиплись | Вернуться к растру; механическое утолщение часто ухудшает результат |
97
+ | Рисунок стал пиктограммой | Усилить предметность и конструкцию в промпте |
98
+
99
+ ## Короткое задание другому ИИ
100
+
101
+ Передай этот файл целиком и заполни только параметры:
102
+
103
+ ```xml
104
+ <задание_контурной_иллюстрации>
105
+ <предмет>{{ОДИН ПРЕДМЕТ}}</предмет>
106
+ <обязательные_детали>{{ДЕТАЛИ И СОЕДИНЕНИЯ}}</обязательные_детали>
107
+ <ракурс>{{РАКУРС ИЛИ «ВЫБЕРИ САМ»}}</ракурс>
108
+ <исключения>{{ЧЕГО НЕ ДОЛЖНО БЫТЬ}}</исключения>
109
+ <каталог>{{КАТАЛОГ РЕЗУЛЬТАТА}}</каталог>
110
+ <slug>{{ИМЯ БЕЗ РАСШИРЕНИЯ}}</slug>
111
+ </задание_контурной_иллюстрации>
112
+ ```
113
+
114
+ ИИ самостоятельно проходит весь процесс до проверенных PNG и SVG. Вопрос нужен только при смысловой неоднозначности, которую нельзя разрешить по заданию и референсам.
@@ -0,0 +1,136 @@
1
+ # Структура проекта колоды
2
+
3
+ Используй этот контракт при создании новой Slidev-колоды на теме Практикума, при добавлении в неё медиа и при целевой реорганизации существующей колоды.
4
+
5
+ ## Каноническое дерево
6
+
7
+ ```text
8
+ <deck>/
9
+ ├── slides.md
10
+ ├── decors.yaml
11
+ ├── package.json
12
+ ├── package-lock.json
13
+ ├── README.md
14
+ ├── .gitignore
15
+ ├── pages/
16
+ ├── components/
17
+ │ └── layout-variants/
18
+ ├── layouts/
19
+ ├── snippets/
20
+ ├── styles/
21
+ │ └── index.css
22
+ ├── public/
23
+ │ ├── decor/
24
+ │ ├── photos/
25
+ │ ├── illustrations/
26
+ │ └── figures/
27
+ └── reference/
28
+ └── media-sources.md
29
+ ```
30
+
31
+ Обязательный минимум — `slides.md`, `package.json`, файл блокировки зависимостей и `public/` с реально используемыми подкаталогами. Остальные каталоги создавай только тогда, когда в них появляется содержимое; не добавляй пустую структуру «на будущее».
32
+
33
+ | Путь | Ответственность |
34
+ |---|---|
35
+ | `slides.md` | Единственная точка входа: общие метаданные и сама колода либо импорты разделов. Для установленного пакета указывай `theme: practicum`; внутри репозитория темы — `theme: ./`. |
36
+ | `decors.yaml` | Необязательный внешний каталог декора колоды. Подключается как `themeConfig.decors: ./decors.yaml` или подхватывается сам, если `decors` не задан. Большой каталог не держи в headmatter `slides.md`. |
37
+ | `package.json` | Зависимости и команды `dev`, `build`, `export`, `validate:slides` для одной точки входа `slides.md`. |
38
+ | `package-lock.json` | Зафиксированное дерево зависимостей. При выбранном другом диспетчере пакетов используй только его один файл блокировки. |
39
+ | `README.md` | Короткие команды запуска, проверки и экспорта, требования к окружению и правила обновления колоды. |
40
+ | `.gitignore` | Результаты сборки, экспорта, кэш Slidev и локальные служебные файлы. |
41
+ | `pages/` | Разделы большой колоды, подключаемые через `src: ./pages/<slug>.md`. Небольшую колоду не дроби без необходимости. |
42
+ | `components/` | Только компоненты, специфичные для этой колоды. Не копируй сюда компоненты и макеты темы. Обычные компоненты Slidev подключает автоматически. |
43
+ | `components/layout-variants/` | Необязательные Vue-композиции для новых вариантов тематических макетов. Файл `components/layout-variants/explainer/lesson-summary.vue` соответствует `layout: explainer` и `variant: lesson-summary`; используй `kebab-case`, ручные координаты `Slot`, не добавляй `arrangement` и не затеняй встроенный вариант темы. |
44
+ | `layouts/` | Нативные макеты Slidev для принципиально новых типов слайда. Например, `layouts/workshop.vue` выбирается через `layout: workshop` и не проходит через механизм вариантов темы. |
45
+ | `snippets/` | Исходный код для вставок вида `<<< @/snippets/<file>`. |
46
+ | `styles/index.css` | Минимальные глобальные переопределения колоды. Сначала используй API и токены темы. |
47
+ | `public/` | Только файлы, которые должны быть доступны в собранной презентации. |
48
+ | `reference/` | Референсы, исходники, лицензии, ссылки и рабочие материалы, которые не должны попадать в сборку и не подключаются из слайдов. |
49
+
50
+ Не вводи параллельные каталоги `assets/`, `images/`, `img/` или `static/`: они размывают классификацию. В существующей колоде не начинай массовый перенос старых файлов без явного запроса; новые файлы клади по этому контракту, а затронутые старые переноси только вместе с обновлением всех ссылок.
51
+
52
+ ## Классификация медиа
53
+
54
+ Каталог выбирается по природе и роли файла, а не по конкретному месту на одном слайде.
55
+
56
+ | Каталог | Что хранить | Обычные форматы | Пример URL |
57
+ |---|---|---|---|
58
+ | `public/decor/` | Графический декор: пятна, паттерны, абстрактные и брендовые композиции без самостоятельного предметного смысла | SVG, PNG, WebP | `/decor/data-flow.svg` |
59
+ | `public/photos/` | Декоративные фотографии и сюжетные фотографические иллюстрации; ни те ни другие не несут единственный факт или инструкцию | WebP, при необходимости PNG/JPEG | `/photos/team-workshop.webp` |
60
+ | `public/illustrations/` | Предметные контурные иллюстрации в визуальном языке темы | парные PNG и SVG | `/illustrations/balance-scales.svg` |
61
+ | `public/figures/` | Информационные изображения: схемы, графики, диаграммы и снимки интерфейса, которые зритель должен рассмотреть | SVG, PNG, WebP | `/figures/research-funnel.svg` |
62
+
63
+ Правила разрешения пограничных случаев:
64
+
65
+ - фотография остаётся в `photos`, даже если используется как декоративный фон;
66
+ - сюжетная фотография тоже остаётся в `photos`; её конкретный смысл дублируется заголовком, текстом, подписью или доступным описанием;
67
+ - контурный предмет остаётся в `illustrations`, даже если включён в каталог `themeConfig.decors`;
68
+ - в `decor` не складывай все «картинки для красоты»: это каталог именно графического декора;
69
+ - если изображение содержит необходимый факт, интерфейс или причинно-следственную схему, это `figures`, а смысл должен быть продублирован доступной подписью или текстом слайда;
70
+ - промпты генерации, оригиналы высокого разрешения, мудборды и сведения об источниках храни в `reference/`, а не в `public/`.
71
+
72
+ Для одной принятой контурной иллюстрации сохраняй комплект с одинаковым именем:
73
+
74
+ ```text
75
+ public/illustrations/balance-scales.png
76
+ public/illustrations/balance-scales.svg
77
+ ```
78
+
79
+ На слайде по умолчанию используй SVG. Полный протокол генерации, трассировки и приёмки находится в `contour-illustrations.md`.
80
+
81
+ Для сюжетной фотографической иллюстрации до генерации зафиксируй роли референсов, масштаб, граф контактов, предметную конструкцию, камеру и обязательные увеличения. Полный производственный контур находится в `photographic-illustrations.md`.
82
+
83
+ ## Пути в слайдах
84
+
85
+ Файлы колоды из `public/` доступны от корня сайта. Сегмент `public` в URL не пишется:
86
+
87
+ ```md
88
+ <Image src="/photos/team-workshop.webp" fit="cover" />
89
+ <Image src="/illustrations/balance-scales.svg" fit="contain" />
90
+ ```
91
+
92
+ Встроенные медиа самой темы имеют отдельное пространство имён `/theme`:
93
+
94
+ ```md
95
+ <Image src="/theme/photos/photo-5.webp" fit="cover" />
96
+ <Image src="/theme/decor/decor-10.svg" fit="contain" />
97
+ ```
98
+
99
+ Не используй `/theme` для файлов колоды и не пиши `src="public/..."` или `src="./public/..."`.
100
+
101
+ Если графический декор колоды должен участвовать в семантическом подборе, зарегистрируй его из корневого headmatter или вынеси во внешний файл. Файл каталога заменяет встроенные картинки темы; для колоды только со своими изображениями закрепляй слоты через `decor.id`.
102
+
103
+ ```yaml
104
+ themeConfig:
105
+ replaceDecors: true
106
+ decors: ./decors.yaml
107
+ ```
108
+
109
+ ```yaml
110
+ # decors.yaml
111
+ - id: deck-data-flow
112
+ src: /decor/data-flow.svg
113
+ meaning: data
114
+ tone: blue
115
+ ```
116
+
117
+ Короткий список можно оставить inline. Несколько файлов и точечные записи можно смешивать в одном `decors`. Прямое использование через `Image` или `Slot.background` регистрации не требует. Фотографию или контурную иллюстрацию при регистрации не перемещай в `decor`: `src` может ссылаться на `/photos/...` или `/illustrations/...`.
118
+
119
+ ## Имена и подготовка файлов
120
+
121
+ - Используй строчные семантические имена в `kebab-case`: `team-workshop.webp`, а не `IMG_4821-final-2.webp`.
122
+ - Не нумеруй файлы, если номер не выражает реальную серию. Для вариантов добавляй осмысленный суффикс: `data-flow-wide.svg`, `data-flow-square.svg`.
123
+ - У парных PNG/SVG контурной иллюстрации должен совпадать `<slug>`.
124
+ - Перед добавлением проверь лицензию и зафиксируй источник в `reference/media-sources.md`; не оставляй удалённый URL единственным источником медиа для сборки.
125
+ - Оптимизируй публикуемый файл, но не храни рядом с ним исходники редактора, промежуточные маски и дубликаты форматов, которые не нужны контракту.
126
+
127
+ ## Проверка перед завершением
128
+
129
+ - Каждый новый файл лежит ровно в одной смысловой категории.
130
+ - Все ссылки начинаются с `/`, а `/theme` используется только для встроенных файлов темы.
131
+ - В `public/` нет референсов, промптов, исходников редактора и неиспользуемых дублей.
132
+ - Фотографии не являются единственным носителем факта или инструкции.
133
+ - Для каждой нетривиальной фотографии понятна роль: декоративная или сюжетная.
134
+ - Контурные иллюстрации сохранены парой PNG/SVG и прошли протокол `contour-illustrations.md`.
135
+ - Каталоги и имена файлов в `components/layout-variants/<layout>/` совпадают со значениями `layout` и `variant` и записаны в `kebab-case`.
136
+ - `npm exec -- slidev-practicum-validate slides.md` и сборка колоды завершаются без ошибок.