slidev-theme-practicum 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -18
- package/components/Slide.vue +11 -57
- 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 +16 -2
- package/composables/layout-shorthands.ts +6 -17
- 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/package.json +6 -3
- package/scripts/browser-smoke.mjs +65 -0
- 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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type LayoutShorthandContext,
|
|
9
9
|
} from './layout-shorthands'
|
|
10
10
|
import {
|
|
11
|
+
hasLayoutVariant,
|
|
11
12
|
resolveLayoutSpec,
|
|
12
13
|
resolveLayoutVariant,
|
|
13
14
|
resolveLayoutRecipe,
|
|
@@ -21,6 +22,11 @@ import {
|
|
|
21
22
|
type ThemeLayoutSlotSpec,
|
|
22
23
|
} from './layout-recipes'
|
|
23
24
|
import { isBlankTextVNode, isCommentVNode } from './layout-vnode'
|
|
25
|
+
import {
|
|
26
|
+
buildDeckLayoutVariantVNode,
|
|
27
|
+
resolveDeckLayoutVariant,
|
|
28
|
+
type DeckLayoutVariantCatalog,
|
|
29
|
+
} from './local-layout-variants'
|
|
24
30
|
import { createSlotRules, type SlotRules } from './slot-rules'
|
|
25
31
|
import {
|
|
26
32
|
isContrastSlideMode,
|
|
@@ -48,6 +54,7 @@ export type LayoutAuthoringInput = {
|
|
|
48
54
|
defaultTone: ThemeTone
|
|
49
55
|
debugGrid?: boolean
|
|
50
56
|
slotPlanState?: LayoutSlotPlanState
|
|
57
|
+
layoutVariants?: DeckLayoutVariantCatalog
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
export type LayoutSlotPlanState = Map<string, { role: ThemeLayoutRole, index: number }>
|
|
@@ -303,17 +310,27 @@ function resolveHeader(input: {
|
|
|
303
310
|
export function compileLayoutAuthoring(input: LayoutAuthoringInput): CompiledLayoutAuthoring {
|
|
304
311
|
const props = input.props
|
|
305
312
|
const frontmatter = input.frontmatter
|
|
306
|
-
const
|
|
313
|
+
const frontmatterLayout = readFrontmatterString(frontmatter, 'layout')
|
|
314
|
+
const explicitLayout = props.layout || frontmatterLayout
|
|
315
|
+
const rawVariant = props.variant || readFrontmatterString(frontmatter, 'variant')
|
|
316
|
+
const rawArrangement = props.arrangement || readFrontmatterString(frontmatter, 'arrangement')
|
|
307
317
|
const layout = isThemeLayout(explicitLayout) ? explicitLayout : ''
|
|
308
318
|
const active = Boolean(layout)
|
|
309
319
|
const slideMode = resolveSlideMode(props.mode ?? readFrontmatterString(frontmatter, 'mode'), 'Slide.mode', 'light')
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
320
|
+
const builtInVariant = active && hasLayoutVariant(layout as ThemeLayout, rawVariant)
|
|
321
|
+
const deckVariant = active && rawVariant && !builtInVariant
|
|
322
|
+
? resolveDeckLayoutVariant({
|
|
323
|
+
layout: layout as ThemeLayout,
|
|
324
|
+
variant: rawVariant,
|
|
325
|
+
arrangement: rawArrangement,
|
|
326
|
+
variants: input.layoutVariants ?? {},
|
|
327
|
+
})
|
|
328
|
+
: undefined
|
|
329
|
+
const variant = deckVariant
|
|
330
|
+
? rawVariant
|
|
331
|
+
: resolveLayoutVariant(layout, rawVariant, rawArrangement)
|
|
332
|
+
const layoutSpec = active && !deckVariant ? resolveLayoutSpec(layout as ThemeLayout, variant) : null
|
|
333
|
+
const recipe = active && !deckVariant ? resolveLayoutRecipe(layout as ThemeLayout, variant) : null
|
|
317
334
|
const contrast = isContrastSlideMode(slideMode)
|
|
318
335
|
const frontmatterDecor = readFrontmatterObject(frontmatter, 'decor')
|
|
319
336
|
const agendaDecor = resolveAgendaDecor({
|
|
@@ -337,7 +354,15 @@ export function compileLayoutAuthoring(input: LayoutAuthoringInput): CompiledLay
|
|
|
337
354
|
const rawChildren = input.children?.filter(isVNode) ?? []
|
|
338
355
|
const shouldCompileChildren = input.children !== undefined
|
|
339
356
|
const slotRules = createSlotRules({ Slot: input.components.Slot })
|
|
340
|
-
const children =
|
|
357
|
+
const children = deckVariant && shouldCompileChildren
|
|
358
|
+
? [buildDeckLayoutVariantVNode({
|
|
359
|
+
layout: layout as ThemeLayout,
|
|
360
|
+
variant,
|
|
361
|
+
component: deckVariant,
|
|
362
|
+
frontmatter,
|
|
363
|
+
children: flattenVNodes(rawChildren, true),
|
|
364
|
+
})]
|
|
365
|
+
: active && shouldCompileChildren
|
|
341
366
|
? buildSourceChildren({
|
|
342
367
|
rawChildren,
|
|
343
368
|
layout: layout as ThemeLayout,
|
|
@@ -417,16 +417,30 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
417
417
|
]),
|
|
418
418
|
]
|
|
419
419
|
|
|
420
|
-
|
|
420
|
+
function findLayoutRecipe(layout: ThemeLayout, variant: string, arrangement = '') {
|
|
421
421
|
const resolvedVariant = variant.trim()
|
|
422
422
|
const resolvedArrangement = arrangement.trim()
|
|
423
|
-
|
|
423
|
+
return LAYOUT_RECIPES.find(candidate =>
|
|
424
424
|
candidate.layout === layout
|
|
425
425
|
&& candidate.aliases.some(alias =>
|
|
426
426
|
alias.variant === resolvedVariant
|
|
427
427
|
&& (alias.arrangement ?? '') === resolvedArrangement,
|
|
428
428
|
),
|
|
429
429
|
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function hasLayoutVariant(layout: ThemeLayout, variant: string) {
|
|
433
|
+
const resolvedVariant = variant.trim()
|
|
434
|
+
return LAYOUT_RECIPES.some(candidate =>
|
|
435
|
+
candidate.layout === layout
|
|
436
|
+
&& candidate.aliases.some(alias => alias.variant === resolvedVariant),
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function resolveLayoutRecipe(layout: ThemeLayout, variant: string, arrangement = '') {
|
|
441
|
+
const resolvedVariant = variant.trim()
|
|
442
|
+
const resolvedArrangement = arrangement.trim()
|
|
443
|
+
const recipe = findLayoutRecipe(layout, resolvedVariant, resolvedArrangement)
|
|
430
444
|
|
|
431
445
|
if (!recipe) {
|
|
432
446
|
const suffix = resolvedArrangement ? `:${resolvedArrangement}` : ''
|
|
@@ -8,6 +8,7 @@ export type LayoutShorthandComponents = {
|
|
|
8
8
|
Image: Component
|
|
9
9
|
Person: Component
|
|
10
10
|
Slot: Component
|
|
11
|
+
StepsGrid: Component
|
|
11
12
|
Text: Component
|
|
12
13
|
Timeline: Component
|
|
13
14
|
}
|
|
@@ -547,7 +548,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
547
548
|
|
|
548
549
|
return [
|
|
549
550
|
hSlot(context, 'primary', 'slide-message-centered-primary', { centered: true }, [
|
|
550
|
-
hText(context, 'h1', '7', title.text, { align: 'middle' }),
|
|
551
|
+
hText(context, 'h1', '7-12', title.text, { align: 'middle' }),
|
|
551
552
|
]),
|
|
552
553
|
]
|
|
553
554
|
}
|
|
@@ -871,22 +872,10 @@ function buildStepsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
871
872
|
hText(context, 'h1', '6', title.text),
|
|
872
873
|
]),
|
|
873
874
|
hSlot(context, 'support', `slide-collection-${context.variant}-support`, {}, [
|
|
874
|
-
h(
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}, items.map((item, index) =>
|
|
879
|
-
h('article', {
|
|
880
|
-
'class': ['Slide-Step', item.active ? 'Slide-Step_active' : ''],
|
|
881
|
-
'data-index': String(index + 1),
|
|
882
|
-
}, [
|
|
883
|
-
hText(context, 'div', '2', item.label, { muted: !item.active }),
|
|
884
|
-
hText(context, 'div', '4-5', item.title, { priority: 1 }),
|
|
885
|
-
...(item.body
|
|
886
|
-
? [hText(context, 'div', '2-3', item.body, { priority: 2, muted: true })]
|
|
887
|
-
: []),
|
|
888
|
-
]),
|
|
889
|
-
)),
|
|
875
|
+
h(context.components.StepsGrid, {
|
|
876
|
+
items,
|
|
877
|
+
arrangement,
|
|
878
|
+
}),
|
|
890
879
|
]),
|
|
891
880
|
]
|
|
892
881
|
}
|
|
@@ -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/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",
|