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,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
|
+
}
|
|
@@ -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,
|
|
@@ -239,6 +239,46 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
239
239
|
],
|
|
240
240
|
},
|
|
241
241
|
},
|
|
242
|
+
{
|
|
243
|
+
content: 'comparison',
|
|
244
|
+
aliases: [{ variant: 'comparison-before-after' }, { variant: 'comparison', arrangement: 'before-after' }],
|
|
245
|
+
regions: {
|
|
246
|
+
primary: '1 / 1 / 4 / -1',
|
|
247
|
+
secondary: [
|
|
248
|
+
{ area: '4 / 1 / -1 / 6', surface: 'light', margin: 4 },
|
|
249
|
+
{ area: '4 / 8 / -1 / -1', surface: 'light', margin: 4 },
|
|
250
|
+
],
|
|
251
|
+
support: { area: '4 / 6 / -1 / 8', centered: true },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
content: 'comparison',
|
|
256
|
+
aliases: [{ variant: 'comparison-stable-variable' }, { variant: 'comparison', arrangement: 'stable-variable' }],
|
|
257
|
+
regions: {
|
|
258
|
+
primary: '1 / 1 / 4 / -1',
|
|
259
|
+
secondary: [
|
|
260
|
+
{ area: '4 / 1 / 8 / -1', surface: 'light', margin: 4 },
|
|
261
|
+
{ area: '9 / 1 / -1 / -1', surface: 'light', margin: 4 },
|
|
262
|
+
],
|
|
263
|
+
support: { area: '8 / 1 / 9 / -1', centered: true },
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
content: 'steps',
|
|
268
|
+
aliases: [{ variant: 'steps-linear' }, { variant: 'steps', arrangement: 'linear' }],
|
|
269
|
+
regions: {
|
|
270
|
+
primary: '1 / 1 / 4 / -1',
|
|
271
|
+
support: '4 / 1 / -1 / -1',
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
content: 'steps',
|
|
276
|
+
aliases: [{ variant: 'steps-staggered' }, { variant: 'steps', arrangement: 'staggered' }],
|
|
277
|
+
regions: {
|
|
278
|
+
primary: '1 / 1 / 4 / -1',
|
|
279
|
+
support: '4 / 1 / -1 / -1',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
242
282
|
{
|
|
243
283
|
content: 'metrics',
|
|
244
284
|
aliases: [{ variant: 'metrics-featured-media' }, { variant: 'metrics', arrangement: 'featured-media' }],
|
|
@@ -251,6 +291,20 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
251
291
|
],
|
|
252
292
|
},
|
|
253
293
|
},
|
|
294
|
+
{
|
|
295
|
+
content: 'metrics',
|
|
296
|
+
aliases: [{ variant: 'metrics-dashboard' }, { variant: 'metrics', arrangement: 'dashboard' }],
|
|
297
|
+
regions: {
|
|
298
|
+
primary: '1 / 1 / 4 / -1',
|
|
299
|
+
support: [
|
|
300
|
+
{ area: '4 / 1 / 8 / 5', surface: 'light', margin: 3 },
|
|
301
|
+
{ area: '4 / 5 / 8 / 9', surface: 'light', margin: 3 },
|
|
302
|
+
{ area: '4 / 9 / 8 / -1', surface: 'light', margin: 3 },
|
|
303
|
+
{ area: '8 / 1 / -1 / 7', surface: 'light', margin: 3 },
|
|
304
|
+
{ area: '8 / 7 / -1 / -1', surface: 'light', margin: 3 },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
254
308
|
{
|
|
255
309
|
content: 'metrics',
|
|
256
310
|
aliases: [{ variant: 'metrics-featured-copy' }, { variant: 'metrics', arrangement: 'featured-copy' }],
|
|
@@ -335,6 +389,19 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
335
389
|
],
|
|
336
390
|
},
|
|
337
391
|
},
|
|
392
|
+
{
|
|
393
|
+
content: 'facts',
|
|
394
|
+
aliases: [{ variant: 'facts-numbered-quartet' }, { variant: 'facts', arrangement: 'numbered-quartet' }],
|
|
395
|
+
regions: {
|
|
396
|
+
primary: '1 / 1 / 4 / -1',
|
|
397
|
+
support: [
|
|
398
|
+
{ area: '4 / 1 / 8 / 7', surface: 'light', margin: 3 },
|
|
399
|
+
{ area: '4 / 7 / 8 / -1', surface: 'light', margin: 3 },
|
|
400
|
+
{ area: '8 / 1 / -1 / 7', surface: 'light', margin: 3 },
|
|
401
|
+
{ area: '8 / 7 / -1 / -1', surface: 'light', margin: 3 },
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
338
405
|
{
|
|
339
406
|
content: 'facts',
|
|
340
407
|
aliases: [{ variant: 'facts-featured' }, { variant: 'facts', arrangement: 'featured' }],
|
|
@@ -350,16 +417,30 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
350
417
|
]),
|
|
351
418
|
]
|
|
352
419
|
|
|
353
|
-
|
|
420
|
+
function findLayoutRecipe(layout: ThemeLayout, variant: string, arrangement = '') {
|
|
354
421
|
const resolvedVariant = variant.trim()
|
|
355
422
|
const resolvedArrangement = arrangement.trim()
|
|
356
|
-
|
|
423
|
+
return LAYOUT_RECIPES.find(candidate =>
|
|
357
424
|
candidate.layout === layout
|
|
358
425
|
&& candidate.aliases.some(alias =>
|
|
359
426
|
alias.variant === resolvedVariant
|
|
360
427
|
&& (alias.arrangement ?? '') === resolvedArrangement,
|
|
361
428
|
),
|
|
362
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)
|
|
363
444
|
|
|
364
445
|
if (!recipe) {
|
|
365
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
|
}
|
|
@@ -136,6 +137,7 @@ function hText(
|
|
|
136
137
|
priority?: 1 | 2 | 3
|
|
137
138
|
align?: 'start' | 'middle' | 'end'
|
|
138
139
|
className?: string
|
|
140
|
+
color?: 'light-0'
|
|
139
141
|
typography?: 'compact'
|
|
140
142
|
} = {},
|
|
141
143
|
) {
|
|
@@ -147,6 +149,7 @@ function hText(
|
|
|
147
149
|
priority?: 1 | 2 | 3
|
|
148
150
|
align?: 'start' | 'middle' | 'end'
|
|
149
151
|
class?: string
|
|
152
|
+
color?: 'light-0'
|
|
150
153
|
} & {
|
|
151
154
|
'data-typography'?: 'compact'
|
|
152
155
|
} = {
|
|
@@ -164,6 +167,8 @@ function hText(
|
|
|
164
167
|
textProps.align = options.align
|
|
165
168
|
if (options.className)
|
|
166
169
|
textProps.class = options.className
|
|
170
|
+
if (options.color)
|
|
171
|
+
textProps.color = options.color
|
|
167
172
|
if (options.typography)
|
|
168
173
|
textProps['data-typography'] = options.typography
|
|
169
174
|
|
|
@@ -358,6 +363,7 @@ const FACTS_VARIANT_COUNTS: Record<string, number> = {
|
|
|
358
363
|
'facts-duo': 2,
|
|
359
364
|
'facts-trio': 3,
|
|
360
365
|
'facts-quartet': 4,
|
|
366
|
+
'facts-numbered-quartet': 4,
|
|
361
367
|
'facts-stacked': 3,
|
|
362
368
|
'facts-featured': 3,
|
|
363
369
|
}
|
|
@@ -542,7 +548,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
542
548
|
|
|
543
549
|
return [
|
|
544
550
|
hSlot(context, 'primary', 'slide-message-centered-primary', { centered: true }, [
|
|
545
|
-
hText(context, 'h1', '7', title.text, { align: 'middle' }),
|
|
551
|
+
hText(context, 'h1', '7-12', title.text, { align: 'middle' }),
|
|
546
552
|
]),
|
|
547
553
|
]
|
|
548
554
|
}
|
|
@@ -726,6 +732,154 @@ function buildPointsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
726
732
|
]
|
|
727
733
|
}
|
|
728
734
|
|
|
735
|
+
type ComparisonSide = {
|
|
736
|
+
title: string
|
|
737
|
+
kicker: string
|
|
738
|
+
body: string
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function normalizeComparisonSide(
|
|
742
|
+
value: unknown,
|
|
743
|
+
side: 'from' | 'to',
|
|
744
|
+
): ComparisonSide {
|
|
745
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
746
|
+
failMarkdownContract('collection:comparison', `ожидает object в comparison.${side}.`)
|
|
747
|
+
|
|
748
|
+
const record = value as Record<string, unknown>
|
|
749
|
+
const title = readRecordText(record, ['title'])
|
|
750
|
+
|
|
751
|
+
if (!title)
|
|
752
|
+
failMarkdownContract('collection:comparison', `ожидает обязательный title в comparison.${side}.`)
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
title,
|
|
756
|
+
kicker: readRecordText(record, ['kicker']),
|
|
757
|
+
body: readRecordText(record, ['body']),
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function buildComparisonSideChildren(
|
|
762
|
+
context: LayoutShorthandContext,
|
|
763
|
+
side: ComparisonSide,
|
|
764
|
+
index: number,
|
|
765
|
+
) {
|
|
766
|
+
return hSlot(context, 'secondary', `slide-collection-${context.variant}-side-${index + 1}`, {
|
|
767
|
+
gap: '3',
|
|
768
|
+
}, [
|
|
769
|
+
...(side.kicker
|
|
770
|
+
? [hText(context, 'div', '2', side.kicker, { priority: 3, muted: true })]
|
|
771
|
+
: []),
|
|
772
|
+
hText(context, 'div', '5-7', side.title, { priority: 1 }),
|
|
773
|
+
...(side.body
|
|
774
|
+
? [hText(context, 'div', '3-4', side.body, { priority: 2, muted: true })]
|
|
775
|
+
: []),
|
|
776
|
+
])
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function buildComparisonChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
780
|
+
const title = readHeadingText(children, 'collection:comparison')
|
|
781
|
+
const comparison = context.readFrontmatterObject('comparison')
|
|
782
|
+
|
|
783
|
+
if (!comparison)
|
|
784
|
+
failMarkdownContract('collection:comparison', 'ожидает frontmatter comparison с полями from и to.')
|
|
785
|
+
|
|
786
|
+
const relation = context.readFrontmatterObject('relation')
|
|
787
|
+
const from = normalizeComparisonSide(comparison.from, 'from')
|
|
788
|
+
const to = normalizeComparisonSide(comparison.to, 'to')
|
|
789
|
+
const relationLabel = relation ? readRecordText(relation, ['label']) : ''
|
|
790
|
+
const isBeforeAfter = context.variant === 'comparison-before-after'
|
|
791
|
+
|
|
792
|
+
ensureOnlySupportedChildren('collection:comparison', children, node =>
|
|
793
|
+
node === title.node
|
|
794
|
+
|| isBlankTextNode(node),
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
return [
|
|
798
|
+
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
799
|
+
hText(context, 'h1', '6', title.text),
|
|
800
|
+
]),
|
|
801
|
+
buildComparisonSideChildren(context, from, 0),
|
|
802
|
+
buildComparisonSideChildren(context, to, 1),
|
|
803
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-relation`, {}, [
|
|
804
|
+
h('div', {
|
|
805
|
+
class: [
|
|
806
|
+
'Slide-ComparisonRelation',
|
|
807
|
+
`Slide-ComparisonRelation_${isBeforeAfter ? 'horizontal' : 'vertical'}`,
|
|
808
|
+
],
|
|
809
|
+
}, [
|
|
810
|
+
h('span', { 'class': 'Slide-ComparisonDirection', 'aria-hidden': 'true' }, isBeforeAfter ? '→' : '↓'),
|
|
811
|
+
...(relationLabel
|
|
812
|
+
? [hText(context, 'div', '2-3', relationLabel, { align: 'middle', muted: true })]
|
|
813
|
+
: []),
|
|
814
|
+
]),
|
|
815
|
+
]),
|
|
816
|
+
]
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
type StepItem = {
|
|
820
|
+
title: string
|
|
821
|
+
body: string
|
|
822
|
+
label: string
|
|
823
|
+
active: boolean
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
|
|
827
|
+
const items = context.readFrontmatterArray('items')
|
|
828
|
+
const isStaggered = context.variant === 'steps-staggered'
|
|
829
|
+
|
|
830
|
+
if (items.length < 3 || items.length > 6)
|
|
831
|
+
failMarkdownContract('collection:steps', 'ожидает от 3 до 6 элементов во frontmatter items.')
|
|
832
|
+
|
|
833
|
+
if (isStaggered && items.length !== 5)
|
|
834
|
+
failMarkdownContract('collection:steps', 'arrangement: staggered ожидает ровно 5 элементов.')
|
|
835
|
+
|
|
836
|
+
const normalized = items.map((item, index) => {
|
|
837
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
838
|
+
failMarkdownContract('collection:steps', `ожидает object item в items[${index}].`)
|
|
839
|
+
|
|
840
|
+
const record = item as Record<string, unknown>
|
|
841
|
+
const title = readRecordText(record, ['title'])
|
|
842
|
+
|
|
843
|
+
if (!title)
|
|
844
|
+
failMarkdownContract('collection:steps', `ожидает обязательный title в items[${index}].`)
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
title,
|
|
848
|
+
body: readRecordText(record, ['body']),
|
|
849
|
+
label: readRecordText(record, ['label']) || String(index + 1),
|
|
850
|
+
active: record.active === true,
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
if (normalized.filter(item => item.active).length > 1)
|
|
855
|
+
failMarkdownContract('collection:steps', 'поддерживает не больше одного элемента с active: true.')
|
|
856
|
+
|
|
857
|
+
return normalized
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function buildStepsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
861
|
+
const title = readHeadingText(children, 'collection:steps')
|
|
862
|
+
const items = normalizeStepItems(context)
|
|
863
|
+
const arrangement = context.variant === 'steps-staggered' ? 'staggered' : 'linear'
|
|
864
|
+
|
|
865
|
+
ensureOnlySupportedChildren('collection:steps', children, node =>
|
|
866
|
+
node === title.node
|
|
867
|
+
|| isBlankTextNode(node),
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
return [
|
|
871
|
+
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
872
|
+
hText(context, 'h1', '6', title.text),
|
|
873
|
+
]),
|
|
874
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-support`, {}, [
|
|
875
|
+
h(context.components.StepsGrid, {
|
|
876
|
+
items,
|
|
877
|
+
arrangement,
|
|
878
|
+
}),
|
|
879
|
+
]),
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
|
|
729
883
|
function normalizeTimelineItems(context: LayoutShorthandContext) {
|
|
730
884
|
const items = context.readFrontmatterArray('items')
|
|
731
885
|
|
|
@@ -804,6 +958,32 @@ function normalizeMetricItems(context: LayoutShorthandContext) {
|
|
|
804
958
|
})
|
|
805
959
|
}
|
|
806
960
|
|
|
961
|
+
function normalizeDashboardMetrics(context: LayoutShorthandContext) {
|
|
962
|
+
const metrics = context.readFrontmatterArray('metrics')
|
|
963
|
+
|
|
964
|
+
if (metrics.length !== 5)
|
|
965
|
+
failMarkdownContract('collection:metrics', 'arrangement: dashboard ожидает ровно 5 элементов во frontmatter metrics.')
|
|
966
|
+
|
|
967
|
+
return metrics.map((item, index) => {
|
|
968
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
969
|
+
failMarkdownContract('collection:metrics', `ожидает object item в metrics[${index}].`)
|
|
970
|
+
|
|
971
|
+
const record = item as Record<string, unknown>
|
|
972
|
+
const value = readRecordText(record, ['value'])
|
|
973
|
+
const label = readRecordText(record, ['body', 'label', 'title'])
|
|
974
|
+
const hasArbitrarySpan = ['area', 'col', 'row', 'span', 'colSpan', 'rowSpan']
|
|
975
|
+
.some(key => record[key] !== undefined)
|
|
976
|
+
|
|
977
|
+
if (!value || !label)
|
|
978
|
+
failMarkdownContract('collection:metrics', `ожидает value и body/label/title в metrics[${index}].`)
|
|
979
|
+
|
|
980
|
+
if (hasArbitrarySpan)
|
|
981
|
+
failMarkdownContract('collection:metrics', `не принимает произвольные координаты или spans в metrics[${index}].`)
|
|
982
|
+
|
|
983
|
+
return { value, label }
|
|
984
|
+
})
|
|
985
|
+
}
|
|
986
|
+
|
|
807
987
|
function normalizeMetricItemsFromList(list: VNode) {
|
|
808
988
|
const entries = readNestedListItems(list, 'collection:metrics')
|
|
809
989
|
|
|
@@ -841,6 +1021,32 @@ function normalizeMediaItems(context: LayoutShorthandContext) {
|
|
|
841
1021
|
}
|
|
842
1022
|
|
|
843
1023
|
function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
1024
|
+
if (context.variant === 'metrics-dashboard') {
|
|
1025
|
+
const title = readHeadingText(children, 'collection:metrics')
|
|
1026
|
+
const metrics = normalizeDashboardMetrics(context)
|
|
1027
|
+
const fitGroup = 'collection-metrics-dashboard'
|
|
1028
|
+
|
|
1029
|
+
ensureOnlySupportedChildren('collection:metrics', children, node =>
|
|
1030
|
+
node === title.node
|
|
1031
|
+
|| isBlankTextNode(node),
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
return [
|
|
1035
|
+
hSlot(context, 'primary', 'slide-collection-metrics-dashboard-primary', {}, [
|
|
1036
|
+
hText(context, 'h1', '6', title.text),
|
|
1037
|
+
]),
|
|
1038
|
+
...metrics.map((metric, index) =>
|
|
1039
|
+
hSlot(context, 'support', `slide-collection-metrics-dashboard-support-${index + 1}`, {
|
|
1040
|
+
fitGroup,
|
|
1041
|
+
gap: '2',
|
|
1042
|
+
}, [
|
|
1043
|
+
hText(context, 'div', index < 3 ? '5-7' : '6-8', metric.value, { priority: 1 }),
|
|
1044
|
+
hText(context, 'div', '2-4', metric.label, { priority: 2, muted: true, typography: 'compact' }),
|
|
1045
|
+
]),
|
|
1046
|
+
),
|
|
1047
|
+
]
|
|
1048
|
+
}
|
|
1049
|
+
|
|
844
1050
|
const list = children.find(isListNode)
|
|
845
1051
|
|
|
846
1052
|
ensureOnlySupportedChildren('collection:metrics', children, node =>
|
|
@@ -964,7 +1170,8 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
964
1170
|
const isStacked = context.variant === 'facts-stacked'
|
|
965
1171
|
const isDuo = context.variant === 'facts-duo'
|
|
966
1172
|
const isQuartet = context.variant === 'facts-quartet'
|
|
967
|
-
const
|
|
1173
|
+
const isNumberedQuartet = context.variant === 'facts-numbered-quartet'
|
|
1174
|
+
const shouldCoordinateFactSizes = isStacked || isDuo || context.variant === 'facts-trio' || isQuartet || isNumberedQuartet
|
|
968
1175
|
const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
|
|
969
1176
|
const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
|
|
970
1177
|
const headingText = readInlineText(heading)
|
|
@@ -997,6 +1204,24 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
997
1204
|
if (footnoteText && !factFitGroup)
|
|
998
1205
|
featuredChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
|
|
999
1206
|
|
|
1207
|
+
if (isNumberedQuartet) {
|
|
1208
|
+
return [
|
|
1209
|
+
hSlot(context, 'primary', 'slide-collection-facts-numbered-quartet-primary', { gap: '3' }, primaryChildren),
|
|
1210
|
+
...facts.map((fact, index) =>
|
|
1211
|
+
hSlot(context, 'support', `slide-collection-facts-numbered-quartet-support-${index + 1}`, {
|
|
1212
|
+
gap: '2',
|
|
1213
|
+
fitGroup: factFitGroup,
|
|
1214
|
+
}, [
|
|
1215
|
+
hText(context, 'div', '4', String(index + 1), {
|
|
1216
|
+
priority: 1,
|
|
1217
|
+
}),
|
|
1218
|
+
hText(context, 'div', '4', fact.value, { priority: 1, align: 'end' }),
|
|
1219
|
+
hText(context, 'div', '2-3', fact.label, { priority: 2, muted: true }),
|
|
1220
|
+
]),
|
|
1221
|
+
),
|
|
1222
|
+
]
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1000
1225
|
return [
|
|
1001
1226
|
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, { gap: '3' }, primaryChildren),
|
|
1002
1227
|
hSlot(context, 'support', `slide-collection-${context.variant}-support-1`, {
|
|
@@ -1018,16 +1243,22 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1018
1243
|
|
|
1019
1244
|
const LAYOUT_SHORTHANDS = {
|
|
1020
1245
|
'collection:agenda': buildAgendaChildren,
|
|
1246
|
+
'collection:comparison-before-after': buildComparisonChildren,
|
|
1247
|
+
'collection:comparison-stable-variable': buildComparisonChildren,
|
|
1021
1248
|
'collection:facts-stacked': buildFactsChildren,
|
|
1022
1249
|
'collection:facts-duo': buildFactsChildren,
|
|
1023
1250
|
'collection:facts-trio': buildFactsChildren,
|
|
1024
1251
|
'collection:facts-quartet': buildFactsChildren,
|
|
1252
|
+
'collection:facts-numbered-quartet': buildFactsChildren,
|
|
1025
1253
|
'collection:facts-featured': buildFactsChildren,
|
|
1026
1254
|
'collection:points-trio': buildPointsChildren,
|
|
1255
|
+
'collection:steps-linear': buildStepsChildren,
|
|
1256
|
+
'collection:steps-staggered': buildStepsChildren,
|
|
1027
1257
|
'collection:timeline': buildTimelineChildren,
|
|
1028
1258
|
'collection:metrics-featured-media': buildMetricsChildren,
|
|
1029
1259
|
'collection:metrics-featured-copy': buildMetricsChildren,
|
|
1030
1260
|
'collection:metrics-featured-copy-split-media': buildMetricsChildren,
|
|
1261
|
+
'collection:metrics-dashboard': buildMetricsChildren,
|
|
1031
1262
|
'message:centered': buildCenteredChildren,
|
|
1032
1263
|
'message:quote': buildQuoteChildren,
|
|
1033
1264
|
'message:closing': buildClosingChildren,
|