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.
- package/README.md +225 -22
- package/components/Slide.vue +12 -58
- package/components/Slot.vue +4 -3
- package/components/StepsGrid.vue +117 -0
- package/components/Text.vue +5 -0
- package/composables/deck-decors.ts +74 -0
- package/composables/deck-slot-markup.cjs +19 -5
- package/composables/decor-sources.ts +43 -0
- package/composables/layout-authoring.ts +34 -9
- package/composables/layout-recipes.ts +16 -2
- package/composables/layout-shorthands.ts +157 -83
- package/composables/local-layout-variant-files.ts +106 -0
- package/composables/local-layout-variants.ts +73 -0
- package/composables/slide-layout.ts +3 -0
- package/composables/text-fit-runtime.ts +7 -3
- package/composables/theme-foundation.ts +7 -3
- package/composables/typography-guard.cjs +59 -0
- package/composables/use-theme-config.ts +17 -10
- package/composables/validate-deck-layouts.cjs +95 -7
- package/composables/validate-deck-typography.cjs +101 -0
- package/env.d.ts +18 -0
- package/example.md +1 -1
- package/package.json +18 -6
- package/scripts/browser-smoke.mjs +85 -23
- package/scripts/check-accessibility.mjs +364 -0
- package/scripts/check-consumer.mjs +159 -0
- package/scripts/check-local-layout-variant-build.mjs +189 -0
- package/scripts/check-package.mjs +18 -2
- package/scripts/check-pixels.mjs +251 -0
- package/scripts/check-typography.mjs +108 -0
- package/scripts/requirements-illustrations.txt +1 -0
- package/scripts/test-typography.mjs +89 -0
- package/scripts/trace-line-art.py +422 -0
- package/scripts/typography-browser.mjs +134 -0
- package/scripts/validate-deck.cjs +23 -3
- package/setup/vite-plugins.ts +109 -2
- package/skills/slidev-practicum/SKILL.md +19 -4
- package/skills/slidev-practicum/references/contour-illustrations.md +114 -0
- package/skills/slidev-practicum/references/deck-project-structure.md +136 -0
- package/skills/slidev-practicum/references/illustration-examples/balance-scales.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/balance-scales.svg +88 -0
- package/skills/slidev-practicum/references/illustration-examples/chainsaw.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/chainsaw.svg +4 -0
- package/skills/slidev-practicum/references/illustration-examples/graduation-cap.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/graduation-cap.svg +23 -0
- package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.png +0 -0
- package/skills/slidev-practicum/references/illustration-examples/woodcutter-axe.svg +4 -0
- package/skills/slidev-practicum/references/photographic-illustrations.md +100 -0
- package/styles/index.css +20 -27
- 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
|
|
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} — контракты
|
|
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
|
|
package/setup/vite-plugins.ts
CHANGED
|
@@ -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
|
|
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:
|
|
3
|
+
description: Используй для Slidev-презентаций, фотографических и предметных контурных иллюстраций в стиле slidev-theme-practicum или Яндекс Практикума.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Slidev Практикума
|
|
7
7
|
|
|
8
|
-
Используй этот скилл как тонкий маршрутизатор для
|
|
8
|
+
Используй этот скилл как тонкий маршрутизатор для колод, фотографических и контурных иллюстраций темы.
|
|
9
9
|
|
|
10
|
-
|
|
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` и сборка колоды завершаются без ошибок.
|