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.
- package/README.md +238 -18
- package/components/Slide.vue +27 -4
- package/components/Slot.vue +2 -1
- package/components/StepsGrid.vue +112 -0
- package/composables/deck-decors.ts +74 -0
- package/composables/decor-sources.ts +43 -0
- package/composables/layout-authoring.ts +34 -9
- package/composables/layout-recipes.ts +83 -2
- package/composables/layout-shorthands.ts +233 -2
- 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/use-theme-config.ts +17 -10
- package/composables/validate-deck-layouts.cjs +77 -6
- package/env.d.ts +18 -0
- package/example.md +135 -5
- package/package.json +6 -3
- package/scripts/browser-smoke.mjs +168 -5
- package/scripts/check-local-layout-variant-build.mjs +189 -0
- package/scripts/check-package.mjs +18 -2
- package/scripts/requirements-illustrations.txt +1 -0
- package/scripts/trace-line-art.py +422 -0
- package/scripts/validate-deck.cjs +1 -1
- package/setup/vite-plugins.ts +109 -2
- package/skills/slidev-practicum/SKILL.md +18 -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
|
@@ -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
|
|
6
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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"
|
|
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.
|
|
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",
|