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,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { getCurrentInstance } from 'vue'
|
|
3
|
+
import Text from './Text.vue'
|
|
4
|
+
import TextFitGroup from './TextFitGroup.vue'
|
|
5
|
+
|
|
6
|
+
type StepItem = {
|
|
7
|
+
title: string
|
|
8
|
+
body: string
|
|
9
|
+
label: string
|
|
10
|
+
active: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const stepsFitGroup = `steps-${getCurrentInstance()?.uid ?? 'default'}`
|
|
14
|
+
|
|
15
|
+
const props = defineProps<{
|
|
16
|
+
items: StepItem[]
|
|
17
|
+
arrangement: 'linear' | 'staggered'
|
|
18
|
+
}>()
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="Slide-StepsGrid"
|
|
23
|
+
:class="`Slide-StepsGrid_${props.arrangement}`"
|
|
24
|
+
:data-count="props.items.length"
|
|
25
|
+
:style="{ '--slide-steps-count': props.items.length }">
|
|
26
|
+
<article v-for="(item, index) in props.items"
|
|
27
|
+
:key="index"
|
|
28
|
+
class="Slide-Step"
|
|
29
|
+
:class="{ 'Slide-Step_active': item.active }"
|
|
30
|
+
:data-index="index + 1">
|
|
31
|
+
<Text size="4">{{ item.label }}</Text>
|
|
32
|
+
<TextFitGroup class="Slide-Step-FitGroup Slide-Step-TitleFitGroup"
|
|
33
|
+
:fit-group="`${stepsFitGroup}-titles`">
|
|
34
|
+
<Text size="3-5">{{ item.title }}</Text>
|
|
35
|
+
</TextFitGroup>
|
|
36
|
+
<TextFitGroup v-if="item.body"
|
|
37
|
+
class="Slide-Step-FitGroup"
|
|
38
|
+
:fit-group="`${stepsFitGroup}-bodies`">
|
|
39
|
+
<Text size="2-3" priority="2" muted>{{ item.body }}</Text>
|
|
40
|
+
</TextFitGroup>
|
|
41
|
+
</article>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<style scoped>
|
|
46
|
+
.Slide-StepsGrid {
|
|
47
|
+
display: grid;
|
|
48
|
+
width: 100%;
|
|
49
|
+
max-width: 100%;
|
|
50
|
+
height: 100%;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
min-height: 0;
|
|
53
|
+
grid-template-columns: repeat(var(--slide-steps-count), minmax(0, 1fr));
|
|
54
|
+
gap: var(--theme-grid-gap);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.Slide-Step {
|
|
58
|
+
display: flex;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
min-height: 0;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: calc(var(--theme-grid-module) * 2);
|
|
64
|
+
padding: var(--theme-slot-margin-3);
|
|
65
|
+
border-radius: var(--theme-panel-radius);
|
|
66
|
+
background: var(--theme-surface-light);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.Slide-Step-FitGroup {
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: auto;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.Slide-Step :deep(.TextFitGroup) {
|
|
76
|
+
width: 100%;
|
|
77
|
+
height: auto;
|
|
78
|
+
min-width: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.Slide-StepsGrid_staggered .Slide-Step-TitleFitGroup {
|
|
82
|
+
margin-top: auto;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.Slide-StepsGrid_staggered {
|
|
86
|
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
87
|
+
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.Slide-StepsGrid_staggered .Slide-Step {
|
|
91
|
+
grid-column: span 2;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.Slide-StepsGrid_staggered[data-count='2'] .Slide-Step {
|
|
95
|
+
grid-column: 2 / 6;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.Slide-StepsGrid_staggered[data-count='3'] .Slide-Step,
|
|
99
|
+
.Slide-StepsGrid_staggered[data-count='4'] .Slide-Step {
|
|
100
|
+
grid-column: span 3;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.Slide-StepsGrid_staggered[data-count='3'] .Slide-Step:nth-child(3) {
|
|
104
|
+
grid-column: 2 / 6;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.Slide-StepsGrid_staggered[data-count='5'] .Slide-Step:nth-child(4) {
|
|
108
|
+
grid-column: 2 / 4;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.Slide-Step_active {
|
|
112
|
+
background: var(--theme-current-color);
|
|
113
|
+
color: var(--theme-text-on-color);
|
|
114
|
+
--theme-text: var(--theme-text-on-color);
|
|
115
|
+
--theme-text-muted: var(--theme-text-muted-on-color);
|
|
116
|
+
}
|
|
117
|
+
</style>
|
package/components/Text.vue
CHANGED
|
@@ -16,7 +16,9 @@ type TextColorToken =
|
|
|
16
16
|
| 'text'
|
|
17
17
|
| 'text-muted'
|
|
18
18
|
| 'text-on-dark'
|
|
19
|
+
| 'text-on-color'
|
|
19
20
|
| 'text-muted-on-contrast'
|
|
21
|
+
| 'text-muted-on-color'
|
|
20
22
|
| 'link'
|
|
21
23
|
| 'light-0'
|
|
22
24
|
| 'light-1'
|
|
@@ -37,7 +39,9 @@ const TEXT_COLOR_MAP: Record<TextColorToken, string> = {
|
|
|
37
39
|
'text': 'var(--theme-text)',
|
|
38
40
|
'text-muted': 'var(--theme-text-muted)',
|
|
39
41
|
'text-on-dark': 'var(--theme-text-on-dark)',
|
|
42
|
+
'text-on-color': 'var(--theme-text-on-color)',
|
|
40
43
|
'text-muted-on-contrast': 'var(--theme-text-muted-on-contrast)',
|
|
44
|
+
'text-muted-on-color': 'var(--theme-text-muted-on-color)',
|
|
41
45
|
'link': 'var(--theme-link)',
|
|
42
46
|
'light-0': 'var(--theme-color-light-0)',
|
|
43
47
|
'light-1': 'var(--theme-color-light-1)',
|
|
@@ -119,6 +123,7 @@ const textStyle = computed(() => ({
|
|
|
119
123
|
<component :is="as"
|
|
120
124
|
ref="textElement"
|
|
121
125
|
class="Text"
|
|
126
|
+
:data-text-color="resolvedColor"
|
|
122
127
|
:class="[
|
|
123
128
|
`Text_size_${normalizedSize}`,
|
|
124
129
|
{
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { extname, isAbsolute, relative, resolve } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { parse as parseYaml } from 'yaml'
|
|
6
|
+
import { collectDecorSources, unwrapDecorFile } from './decor-sources'
|
|
7
|
+
|
|
8
|
+
export const DECK_DECORS_VIRTUAL_ID = 'virtual:practicum-deck-decors'
|
|
9
|
+
export const RESOLVED_DECK_DECORS_VIRTUAL_ID = `\0${DECK_DECORS_VIRTUAL_ID}`
|
|
10
|
+
export const CONVENTION_DECOR_FILES = Object.freeze([
|
|
11
|
+
'decors.yaml',
|
|
12
|
+
'decors.yml',
|
|
13
|
+
'decors.json',
|
|
14
|
+
'decors.mjs',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
const SUPPORTED_EXTENSIONS = new Set(['.yaml', '.yml', '.json', '.mjs', '.js'])
|
|
18
|
+
|
|
19
|
+
export function resolveDecorFilePath(root: string, spec: string) {
|
|
20
|
+
const base = resolve(root)
|
|
21
|
+
const resolved = resolve(base, spec)
|
|
22
|
+
const rel = relative(base, resolved)
|
|
23
|
+
|
|
24
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel))
|
|
25
|
+
throw new Error(`Decor catalog path escapes the deck root: ${spec}`)
|
|
26
|
+
|
|
27
|
+
return resolved
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function findConventionDecorFile(root: string) {
|
|
31
|
+
return CONVENTION_DECOR_FILES
|
|
32
|
+
.map(name => resolve(root, name))
|
|
33
|
+
.find(path => existsSync(path))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function collectDeckDecorFiles(input: { decors: unknown, root: string }) {
|
|
37
|
+
const { files } = collectDecorSources(input.decors)
|
|
38
|
+
|
|
39
|
+
if (files.length)
|
|
40
|
+
return files.map(file => resolveDecorFilePath(input.root, file))
|
|
41
|
+
|
|
42
|
+
if (input.decors != null)
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
const convention = findConventionDecorFile(input.root)
|
|
46
|
+
return convention ? [convention] : []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function loadDecorFile(path: string) {
|
|
50
|
+
const ext = extname(path).toLowerCase()
|
|
51
|
+
if (!SUPPORTED_EXTENSIONS.has(ext))
|
|
52
|
+
throw new Error(`Unsupported decor catalog file: ${path}`)
|
|
53
|
+
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
throw new Error(`Decor catalog file not found: ${path}`)
|
|
56
|
+
|
|
57
|
+
if (ext === '.mjs' || ext === '.js') {
|
|
58
|
+
const loadModule = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<Record<string, unknown>>
|
|
59
|
+
const mod = await loadModule(`${pathToFileURL(path).href}?t=${Date.now()}`)
|
|
60
|
+
return unwrapDecorFile(mod.decors ?? mod.default)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const source = await readFile(path, 'utf8')
|
|
64
|
+
return unwrapDecorFile(ext === '.json' ? JSON.parse(source) : parseYaml(source))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolveDeckFileDecors(input: { decors: unknown, root: string }) {
|
|
68
|
+
const records: Record<string, unknown>[] = []
|
|
69
|
+
|
|
70
|
+
for (const path of collectDeckDecorFiles(input))
|
|
71
|
+
records.push(...await loadDecorFile(path))
|
|
72
|
+
|
|
73
|
+
return records
|
|
74
|
+
}
|
|
@@ -7,7 +7,7 @@ const { parse } = require('vue/compiler-sfc')
|
|
|
7
7
|
* @typedef {(
|
|
8
8
|
* | { kind: 'comment' }
|
|
9
9
|
* | { kind: 'text', content: string }
|
|
10
|
-
* | { kind: 'element', tag: string, props: Record<string, unknown>, children: DeckLiveNode[] }
|
|
10
|
+
* | { kind: 'element', tag: string, props: Record<string, unknown>, dynamicProps?: string[], children: DeckLiveNode[] }
|
|
11
11
|
* )} DeckLiveNode
|
|
12
12
|
*/
|
|
13
13
|
/** @typedef {{ children: DeckLiveNode[], intents: ExplicitSlotIntent[] }} DeckLiveStructure */
|
|
@@ -26,6 +26,11 @@ function formatCompilerError(error) {
|
|
|
26
26
|
* @returns {unknown}
|
|
27
27
|
*/
|
|
28
28
|
function readLiteralBinding(prop) {
|
|
29
|
+
const source = prop.exp?.loc.source.trim()
|
|
30
|
+
if (source === 'true' || source === 'false')
|
|
31
|
+
return source === 'true'
|
|
32
|
+
if (source === 'null')
|
|
33
|
+
return null
|
|
29
34
|
const ast = prop.exp?.ast
|
|
30
35
|
if (!ast)
|
|
31
36
|
return undefined
|
|
@@ -75,11 +80,12 @@ function containsSlot(node) {
|
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
82
|
* @param {import('@vue/compiler-core').ElementNode} node
|
|
78
|
-
* @returns {Record<string, unknown
|
|
83
|
+
* @returns {{ props: Record<string, unknown>, dynamicProps?: string[] }}
|
|
79
84
|
*/
|
|
80
85
|
function elementProps(node) {
|
|
81
86
|
/** @type {Record<string, unknown>} */
|
|
82
87
|
const props = {}
|
|
88
|
+
const dynamicProps = []
|
|
83
89
|
|
|
84
90
|
for (const prop of node.props) {
|
|
85
91
|
if (prop.type === 6) {
|
|
@@ -87,8 +93,11 @@ function elementProps(node) {
|
|
|
87
93
|
continue
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
if (prop.name !== 'bind')
|
|
96
|
+
if (prop.name !== 'bind') {
|
|
97
|
+
if (SLOT_CONTROL_FLOW_DIRECTIVES.has(prop.name))
|
|
98
|
+
dynamicProps.push(`v-${prop.name}`)
|
|
91
99
|
continue
|
|
100
|
+
}
|
|
92
101
|
|
|
93
102
|
const key = prop.arg?.type === 4 && prop.arg.isStatic
|
|
94
103
|
? prop.arg.content
|
|
@@ -96,6 +105,11 @@ function elementProps(node) {
|
|
|
96
105
|
|
|
97
106
|
if (node.tag !== 'Slot'
|
|
98
107
|
|| (!SLOT_CONTRACT_PROPS.has(key) && key !== 'label' && key)) {
|
|
108
|
+
const value = readLiteralBinding(prop)
|
|
109
|
+
if (key && value !== undefined)
|
|
110
|
+
props[key] = value
|
|
111
|
+
else
|
|
112
|
+
dynamicProps.push(key ? `v-bind:${key}` : 'v-bind')
|
|
99
113
|
continue
|
|
100
114
|
}
|
|
101
115
|
|
|
@@ -118,7 +132,7 @@ function elementProps(node) {
|
|
|
118
132
|
props[key] = value
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
return props
|
|
135
|
+
return dynamicProps.length ? { props, dynamicProps } : { props }
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
/**
|
|
@@ -134,7 +148,7 @@ function liveNode(node) {
|
|
|
134
148
|
return {
|
|
135
149
|
kind: 'element',
|
|
136
150
|
tag: node.tag,
|
|
137
|
-
|
|
151
|
+
...elementProps(node),
|
|
138
152
|
children: node.children
|
|
139
153
|
.map(liveNode)
|
|
140
154
|
.filter(isLiveNode),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isDecorFileRef(value: unknown): value is string {
|
|
6
|
+
return typeof value === 'string' && Boolean(value.trim())
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function collectDecorSources(value: unknown): {
|
|
10
|
+
files: string[]
|
|
11
|
+
inline: Record<string, unknown>[]
|
|
12
|
+
} {
|
|
13
|
+
if (isDecorFileRef(value))
|
|
14
|
+
return { files: [value.trim()], inline: [] }
|
|
15
|
+
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return { files: [], inline: [] }
|
|
18
|
+
|
|
19
|
+
const files: string[] = []
|
|
20
|
+
const inline: Record<string, unknown>[] = []
|
|
21
|
+
|
|
22
|
+
for (const item of value) {
|
|
23
|
+
if (isDecorFileRef(item))
|
|
24
|
+
files.push(item.trim())
|
|
25
|
+
else if (isRecord(item))
|
|
26
|
+
inline.push(item)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { files, inline }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function unwrapDecorFile(value: unknown): Record<string, unknown>[] {
|
|
33
|
+
if (Array.isArray(value))
|
|
34
|
+
return value.filter(isRecord)
|
|
35
|
+
|
|
36
|
+
if (isRecord(value) && Array.isArray(value.decors))
|
|
37
|
+
return value.decors.filter(isRecord)
|
|
38
|
+
|
|
39
|
+
if (isRecord(value) && typeof value.id === 'string')
|
|
40
|
+
return [value]
|
|
41
|
+
|
|
42
|
+
return []
|
|
43
|
+
}
|
|
@@ -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}` : ''
|