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
|
@@ -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
|
}
|
|
@@ -117,10 +118,64 @@ function readHeadingText(children: VNode[], label: string) {
|
|
|
117
118
|
failMarkdownContract(label, 'ожидает один markdown heading внутри default slot.')
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function readRichHeadingChildren(node: VNode | null | undefined): Array<string | VNode> | null {
|
|
122
|
+
if (!node || !isHeadingNode(node))
|
|
123
|
+
return null
|
|
124
|
+
|
|
125
|
+
if (typeof node.children === 'string')
|
|
126
|
+
return [node.children]
|
|
127
|
+
if (!Array.isArray(node.children))
|
|
128
|
+
return null
|
|
129
|
+
|
|
130
|
+
return node.children.filter(child => isVNode(child) || typeof child === 'string') as Array<string | VNode>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readRichNodeChildren(node: VNode | null | undefined): Array<string | VNode> | null {
|
|
134
|
+
if (!node || typeof node.children === 'string')
|
|
135
|
+
return null
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(node.children))
|
|
138
|
+
return null
|
|
139
|
+
|
|
140
|
+
return node.children.filter(child => isVNode(child) || typeof child === 'string') as Array<string | VNode>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readHeadingSlotContent(node: VNode | null | undefined, textFallback: string) {
|
|
144
|
+
const richChildren = readRichHeadingChildren(node) ?? readRichNodeChildren(node)
|
|
145
|
+
return richChildren ?? readRichTextWithLineBreaks(textFallback)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readParagraphSlotContent(node: VNode | null | undefined, textFallback: string) {
|
|
149
|
+
const richChildren = readRichNodeChildren(node)
|
|
150
|
+
return richChildren ?? readRichTextWithLineBreaks(textFallback)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readRichTextWithLineBreaks(text: string): string | Array<string | VNode> {
|
|
154
|
+
const matches = text.split(/(<br\s*\/?>)/i)
|
|
155
|
+
if (matches.length === 1)
|
|
156
|
+
return text
|
|
157
|
+
|
|
158
|
+
return matches.flatMap<string | VNode>((value): Array<string | VNode> => {
|
|
159
|
+
if (!value.trim())
|
|
160
|
+
return []
|
|
161
|
+
if (/^<br\s*\/?>$/i.test(value))
|
|
162
|
+
return [h('br')]
|
|
163
|
+
return [value]
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hasRichSlotContent(content: Array<string | VNode>) {
|
|
168
|
+
return content.some(item => typeof item === 'string' ? item.trim() : isVNode(item))
|
|
169
|
+
}
|
|
170
|
+
|
|
120
171
|
function readParagraphText(children: VNode[]) {
|
|
121
172
|
const paragraph = children.find(isParagraphNode)
|
|
122
173
|
if (paragraph)
|
|
123
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
node: paragraph,
|
|
176
|
+
text: readInlineText(paragraph),
|
|
177
|
+
content: readParagraphSlotContent(paragraph, readInlineText(paragraph)),
|
|
178
|
+
}
|
|
124
179
|
|
|
125
180
|
return null
|
|
126
181
|
}
|
|
@@ -129,7 +184,7 @@ function hText(
|
|
|
129
184
|
context: LayoutShorthandContext,
|
|
130
185
|
as: string,
|
|
131
186
|
size: ThemeTextSizeInput,
|
|
132
|
-
content: string
|
|
187
|
+
content: string | VNode | Array<string | VNode>,
|
|
133
188
|
options: {
|
|
134
189
|
muted?: boolean
|
|
135
190
|
maxSize?: boolean
|
|
@@ -171,10 +226,12 @@ function hText(
|
|
|
171
226
|
if (options.typography)
|
|
172
227
|
textProps['data-typography'] = options.typography
|
|
173
228
|
|
|
229
|
+
const slotChildren = Array.isArray(content) ? content : [content]
|
|
230
|
+
|
|
174
231
|
return h(context.components.Text, {
|
|
175
232
|
...textProps,
|
|
176
233
|
}, {
|
|
177
|
-
default: withCtx(() =>
|
|
234
|
+
default: withCtx(() => slotChildren),
|
|
178
235
|
})
|
|
179
236
|
}
|
|
180
237
|
|
|
@@ -250,15 +307,26 @@ function readListItemText(li: VNode) {
|
|
|
250
307
|
return normalizeInlineText(parts.join(' '))
|
|
251
308
|
}
|
|
252
309
|
|
|
253
|
-
function
|
|
254
|
-
if (
|
|
255
|
-
|
|
310
|
+
function readListItemSlotContent(li: VNode): Array<string | VNode> {
|
|
311
|
+
if (typeof li.children === 'string') {
|
|
312
|
+
const text = li.children
|
|
313
|
+
return text.trim() ? [text] : []
|
|
314
|
+
}
|
|
256
315
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
316
|
+
if (!Array.isArray(li.children))
|
|
317
|
+
return []
|
|
318
|
+
|
|
319
|
+
return li.children.flatMap<string | VNode>((child): Array<string | VNode> => {
|
|
320
|
+
if (!isVNode(child)) {
|
|
321
|
+
const text = String(child)
|
|
322
|
+
return text.trim() ? [text] : []
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (child.type === 'ul' || child.type === 'ol')
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
return [cloneVNode(child)]
|
|
329
|
+
})
|
|
262
330
|
}
|
|
263
331
|
|
|
264
332
|
function readListItemNodes(list: VNode, label: string) {
|
|
@@ -295,7 +363,7 @@ function readListItemContent(li: VNode): VNode[] {
|
|
|
295
363
|
if (!Array.isArray(li.children))
|
|
296
364
|
return []
|
|
297
365
|
|
|
298
|
-
return li.children.flatMap((child) => {
|
|
366
|
+
return li.children.flatMap<VNode>((child): VNode[] => {
|
|
299
367
|
if (!isVNode(child)) {
|
|
300
368
|
const text = String(child).trim()
|
|
301
369
|
return text ? [h('p', text)] : []
|
|
@@ -305,7 +373,10 @@ function readListItemContent(li: VNode): VNode[] {
|
|
|
305
373
|
})
|
|
306
374
|
}
|
|
307
375
|
|
|
308
|
-
type NestedListItem = {
|
|
376
|
+
type NestedListItem = {
|
|
377
|
+
value: Array<string | VNode>
|
|
378
|
+
label: Array<string | VNode>
|
|
379
|
+
}
|
|
309
380
|
|
|
310
381
|
function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
311
382
|
if (!Array.isArray(list.children))
|
|
@@ -316,46 +387,38 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
|
316
387
|
.filter(node => node.type === 'li')
|
|
317
388
|
.map((li) => {
|
|
318
389
|
if (!Array.isArray(li.children)) {
|
|
319
|
-
const text = typeof li.children === 'string' ?
|
|
320
|
-
|
|
390
|
+
const text = typeof li.children === 'string' ? li.children : ''
|
|
391
|
+
const value = text.trim() ? [text] : []
|
|
392
|
+
return { value, label: [] }
|
|
321
393
|
}
|
|
322
394
|
|
|
323
|
-
const valueParts: string[] = []
|
|
324
395
|
let nestedList: VNode | undefined
|
|
325
|
-
|
|
326
|
-
for (const child of li.children) {
|
|
396
|
+
const value = li.children.flatMap<string | VNode>((child): Array<string | VNode> => {
|
|
327
397
|
if (!isVNode(child)) {
|
|
328
|
-
const text = String(child
|
|
329
|
-
|
|
330
|
-
valueParts.push(text)
|
|
331
|
-
continue
|
|
398
|
+
const text = String(child)
|
|
399
|
+
return text.trim() ? [text] : []
|
|
332
400
|
}
|
|
333
401
|
|
|
334
402
|
if (child.type === 'ul' || child.type === 'ol') {
|
|
335
403
|
nestedList = child
|
|
336
|
-
|
|
404
|
+
return []
|
|
337
405
|
}
|
|
338
406
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
valueParts.push(text)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const value = normalizeInlineText(valueParts.join(' '))
|
|
407
|
+
return [cloneVNode(child)]
|
|
408
|
+
})
|
|
345
409
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const firstNestedLi = nestedList.children
|
|
410
|
+
const nestedLabel = nestedList && Array.isArray(nestedList.children)
|
|
411
|
+
? nestedList.children
|
|
349
412
|
.filter(isVNode)
|
|
350
413
|
.find(node => node.type === 'li')
|
|
414
|
+
: null
|
|
351
415
|
|
|
352
|
-
|
|
353
|
-
|
|
416
|
+
return {
|
|
417
|
+
value,
|
|
418
|
+
label: nestedLabel ? readListItemSlotContent(nestedLabel) : [],
|
|
354
419
|
}
|
|
355
|
-
|
|
356
|
-
return { value, label }
|
|
357
420
|
})
|
|
358
|
-
.filter(item => item.value)
|
|
421
|
+
.filter(item => hasRichSlotContent(item.value))
|
|
359
422
|
}
|
|
360
423
|
|
|
361
424
|
const FACTS_VARIANT_COUNTS: Record<string, number> = {
|
|
@@ -539,6 +602,7 @@ function buildQuoteChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
539
602
|
|
|
540
603
|
function buildCenteredChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
541
604
|
const title = readHeadingText(children, 'message:centered')
|
|
605
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
542
606
|
|
|
543
607
|
ensureOnlySupportedChildren('message:centered', children, node =>
|
|
544
608
|
node === title.node
|
|
@@ -547,7 +611,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
547
611
|
|
|
548
612
|
return [
|
|
549
613
|
hSlot(context, 'primary', 'slide-message-centered-primary', { centered: true }, [
|
|
550
|
-
hText(context, 'h1', '7',
|
|
614
|
+
hText(context, 'h1', '7-12', titleContent, { align: 'middle' }),
|
|
551
615
|
]),
|
|
552
616
|
]
|
|
553
617
|
}
|
|
@@ -555,6 +619,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
555
619
|
function buildClosingChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
556
620
|
const title = readHeadingText(children, 'message:closing')
|
|
557
621
|
const paragraph = readParagraphText(children)
|
|
622
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
558
623
|
|
|
559
624
|
ensureOnlySupportedChildren('message:closing', children, node =>
|
|
560
625
|
node === title.node
|
|
@@ -564,9 +629,9 @@ function buildClosingChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
564
629
|
|
|
565
630
|
return [
|
|
566
631
|
hSlot(context, 'primary', 'slide-message-closing-primary', { centered: true }, [
|
|
567
|
-
hText(context, 'h1', '7',
|
|
632
|
+
hText(context, 'h1', '7', titleContent, { align: 'middle' }),
|
|
568
633
|
...(paragraph
|
|
569
|
-
? [hText(context, 'p', '3', paragraph.
|
|
634
|
+
? [hText(context, 'p', '3', paragraph.content, { muted: true })]
|
|
570
635
|
: []),
|
|
571
636
|
]),
|
|
572
637
|
]
|
|
@@ -574,8 +639,9 @@ function buildClosingChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
574
639
|
|
|
575
640
|
function buildDefinitionChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
576
641
|
const title = readHeadingText(children, 'explainer:definition')
|
|
642
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
577
643
|
const paragraph = readParagraphText(children)
|
|
578
|
-
const body = context.readFrontmatterText('body') || paragraph?.
|
|
644
|
+
const body = context.readFrontmatterText('body') || paragraph?.content || ''
|
|
579
645
|
const label = context.readFrontmatterText('label')
|
|
580
646
|
|
|
581
647
|
if (!body)
|
|
@@ -590,7 +656,7 @@ function buildDefinitionChildren(children: VNode[], context: LayoutShorthandCont
|
|
|
590
656
|
return [
|
|
591
657
|
hSlot(context, 'primary', 'slide-explainer-definition-primary', {}, [
|
|
592
658
|
...(label ? [hText(context, 'div', '3', label, { muted: true, className: 'Slide-DefinitionLabel' })] : []),
|
|
593
|
-
hText(context, 'h1', '6',
|
|
659
|
+
hText(context, 'h1', '6', titleContent, { priority: 1 }),
|
|
594
660
|
hText(context, 'div', '6', body, { muted: true, priority: 1 }),
|
|
595
661
|
]),
|
|
596
662
|
]
|
|
@@ -607,6 +673,7 @@ function buildTitleRichSideChildren(
|
|
|
607
673
|
options: { muted?: boolean } = {},
|
|
608
674
|
) {
|
|
609
675
|
const title = readHeadingText(children, contract)
|
|
676
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
610
677
|
const bodyNodes = children.filter(node =>
|
|
611
678
|
node !== title.node
|
|
612
679
|
&& !isBlankTextNode(node),
|
|
@@ -626,7 +693,7 @@ function buildTitleRichSideChildren(
|
|
|
626
693
|
|
|
627
694
|
return [
|
|
628
695
|
hSlot(context, 'primary', `slide-explainer-${slotPrefix}-primary`, {}, [
|
|
629
|
-
hText(context, 'h1', '6',
|
|
696
|
+
hText(context, 'h1', '6', titleContent),
|
|
630
697
|
]),
|
|
631
698
|
hSlot(context, 'secondary', `slide-explainer-${slotPrefix}-secondary`, {}, [
|
|
632
699
|
h('div', { class: bodyClass }, bodyNodes.map(node => cloneVNode(node))),
|
|
@@ -645,6 +712,7 @@ function buildTitleSupportsChildren(
|
|
|
645
712
|
return buildTitleRichSideChildren(children, context, 'explainer:title-supports', { muted: true })
|
|
646
713
|
|
|
647
714
|
const title = readHeadingText(children, 'explainer:title-supports')
|
|
715
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
648
716
|
const list = children.find(isListNode)
|
|
649
717
|
const muted = options.muted ?? true
|
|
650
718
|
const supportSize = options.supportSize ?? '4'
|
|
@@ -677,7 +745,7 @@ function buildTitleSupportsChildren(
|
|
|
677
745
|
|
|
678
746
|
return [
|
|
679
747
|
hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
|
|
680
|
-
hText(context, 'h1', '6',
|
|
748
|
+
hText(context, 'h1', '6', titleContent),
|
|
681
749
|
]),
|
|
682
750
|
...items.map((item, index) =>
|
|
683
751
|
hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
|
|
@@ -701,12 +769,21 @@ function buildTitleBodyChildren(children: VNode[], context: LayoutShorthandConte
|
|
|
701
769
|
|
|
702
770
|
function buildPointsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
703
771
|
const title = readHeadingText(children, 'collection:points')
|
|
772
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
704
773
|
const list = children.find(isListNode)
|
|
705
774
|
|
|
706
775
|
if (!list)
|
|
707
776
|
failMarkdownContract('collection:points', 'ожидает markdown list с тремя пунктами.')
|
|
708
777
|
|
|
709
|
-
const
|
|
778
|
+
const listItems = readListItemNodes(list, 'collection:points')
|
|
779
|
+
.map(node => ({
|
|
780
|
+
text: readListItemText(node),
|
|
781
|
+
content: readListItemSlotContent(node),
|
|
782
|
+
}))
|
|
783
|
+
const items = listItems
|
|
784
|
+
.filter(item => item.text)
|
|
785
|
+
.map(item => item.content)
|
|
786
|
+
|
|
710
787
|
if (items.length !== 3)
|
|
711
788
|
failMarkdownContract('collection:points', 'ожидает ровно три пункта для arrangement: trio.')
|
|
712
789
|
|
|
@@ -720,7 +797,7 @@ function buildPointsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
720
797
|
hSlot(context, 'primary', 'slide-collection-points-primary', {
|
|
721
798
|
...(context.frontmatterDecor ? { decor: context.frontmatterDecor } : {}),
|
|
722
799
|
}, [
|
|
723
|
-
hText(context, 'h1', '7',
|
|
800
|
+
hText(context, 'h1', '7', titleContent, { className: 'Slide-PointsTitle' }),
|
|
724
801
|
]),
|
|
725
802
|
...items.map((item, index) =>
|
|
726
803
|
hSlot(context, 'support', `slide-collection-points-support-${index + 1}`, {}, [
|
|
@@ -777,6 +854,7 @@ function buildComparisonSideChildren(
|
|
|
777
854
|
|
|
778
855
|
function buildComparisonChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
779
856
|
const title = readHeadingText(children, 'collection:comparison')
|
|
857
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
780
858
|
const comparison = context.readFrontmatterObject('comparison')
|
|
781
859
|
|
|
782
860
|
if (!comparison)
|
|
@@ -795,7 +873,7 @@ function buildComparisonChildren(children: VNode[], context: LayoutShorthandCont
|
|
|
795
873
|
|
|
796
874
|
return [
|
|
797
875
|
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
798
|
-
hText(context, 'h1', '6',
|
|
876
|
+
hText(context, 'h1', '6', titleContent),
|
|
799
877
|
]),
|
|
800
878
|
buildComparisonSideChildren(context, from, 0),
|
|
801
879
|
buildComparisonSideChildren(context, to, 1),
|
|
@@ -825,12 +903,13 @@ type StepItem = {
|
|
|
825
903
|
function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
|
|
826
904
|
const items = context.readFrontmatterArray('items')
|
|
827
905
|
const isStaggered = context.variant === 'steps-staggered'
|
|
906
|
+
const minItems = isStaggered ? 2 : 3
|
|
828
907
|
|
|
829
|
-
if (items.length <
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
908
|
+
if (items.length < minItems || items.length > 6) {
|
|
909
|
+
const range = isStaggered ? 'от 2 до 6' : 'от 3 до 6'
|
|
910
|
+
const prefix = isStaggered ? 'arrangement: staggered ' : ''
|
|
911
|
+
failMarkdownContract('collection:steps', `${prefix}ожидает ${range} элементов во frontmatter items.`)
|
|
912
|
+
}
|
|
834
913
|
|
|
835
914
|
const normalized = items.map((item, index) => {
|
|
836
915
|
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
@@ -858,6 +937,7 @@ function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
|
|
|
858
937
|
|
|
859
938
|
function buildStepsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
860
939
|
const title = readHeadingText(children, 'collection:steps')
|
|
940
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
861
941
|
const items = normalizeStepItems(context)
|
|
862
942
|
const arrangement = context.variant === 'steps-staggered' ? 'staggered' : 'linear'
|
|
863
943
|
|
|
@@ -868,25 +948,13 @@ function buildStepsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
868
948
|
|
|
869
949
|
return [
|
|
870
950
|
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
871
|
-
hText(context, 'h1', '6',
|
|
951
|
+
hText(context, 'h1', '6', titleContent),
|
|
872
952
|
]),
|
|
873
953
|
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
|
-
)),
|
|
954
|
+
h(context.components.StepsGrid, {
|
|
955
|
+
items,
|
|
956
|
+
arrangement,
|
|
957
|
+
}),
|
|
890
958
|
]),
|
|
891
959
|
]
|
|
892
960
|
}
|
|
@@ -918,8 +986,9 @@ function normalizeTimelineItems(context: LayoutShorthandContext) {
|
|
|
918
986
|
|
|
919
987
|
function buildTimelineChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
920
988
|
const title = readHeadingText(children, 'collection:timeline')
|
|
989
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
921
990
|
const paragraph = readParagraphText(children)
|
|
922
|
-
const body = context.readFrontmatterText('body') || paragraph?.
|
|
991
|
+
const body = context.readFrontmatterText('body') || paragraph?.content || ''
|
|
923
992
|
|
|
924
993
|
ensureOnlySupportedChildren('collection:timeline', children, node =>
|
|
925
994
|
node === title.node
|
|
@@ -929,7 +998,7 @@ function buildTimelineChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
929
998
|
|
|
930
999
|
return [
|
|
931
1000
|
hSlot(context, 'primary', 'slide-collection-timeline-primary', {}, [
|
|
932
|
-
hText(context, 'h1', '6',
|
|
1001
|
+
hText(context, 'h1', '6', titleContent),
|
|
933
1002
|
]),
|
|
934
1003
|
...(body
|
|
935
1004
|
? [
|
|
@@ -1002,7 +1071,7 @@ function normalizeMetricItemsFromList(list: VNode) {
|
|
|
1002
1071
|
failMarkdownContract('collection:metrics', 'ожидает минимум три пункта в markdown-списке.')
|
|
1003
1072
|
|
|
1004
1073
|
return entries.map((entry, index) => {
|
|
1005
|
-
if (!entry.label)
|
|
1074
|
+
if (!hasRichSlotContent(entry.label))
|
|
1006
1075
|
failMarkdownContract('collection:metrics', `ожидает вложенный пункт с подписью у metric[${index}].`)
|
|
1007
1076
|
|
|
1008
1077
|
return {
|
|
@@ -1034,6 +1103,7 @@ function normalizeMediaItems(context: LayoutShorthandContext) {
|
|
|
1034
1103
|
function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
1035
1104
|
if (context.variant === 'metrics-dashboard') {
|
|
1036
1105
|
const title = readHeadingText(children, 'collection:metrics')
|
|
1106
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
1037
1107
|
const metrics = normalizeDashboardMetrics(context)
|
|
1038
1108
|
const fitGroup = 'collection-metrics-dashboard'
|
|
1039
1109
|
|
|
@@ -1044,7 +1114,7 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
1044
1114
|
|
|
1045
1115
|
return [
|
|
1046
1116
|
hSlot(context, 'primary', 'slide-collection-metrics-dashboard-primary', {}, [
|
|
1047
|
-
hText(context, 'h1', '6',
|
|
1117
|
+
hText(context, 'h1', '6', titleContent),
|
|
1048
1118
|
]),
|
|
1049
1119
|
...metrics.map((metric, index) =>
|
|
1050
1120
|
hSlot(context, 'support', `slide-collection-metrics-dashboard-support-${index + 1}`, {
|
|
@@ -1167,7 +1237,7 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1167
1237
|
}
|
|
1168
1238
|
|
|
1169
1239
|
facts.forEach((fact, index) => {
|
|
1170
|
-
if (!fact.label) {
|
|
1240
|
+
if (!hasRichSlotContent(fact.label)) {
|
|
1171
1241
|
failMarkdownContract(
|
|
1172
1242
|
'collection:facts',
|
|
1173
1243
|
`ожидает вложенный пункт с подписью у facts[${index}].`,
|
|
@@ -1186,8 +1256,6 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1186
1256
|
const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
|
|
1187
1257
|
const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
|
|
1188
1258
|
const headingText = readInlineText(heading)
|
|
1189
|
-
const descriptionText = description ? readInlineText(description) : ''
|
|
1190
|
-
const footnoteText = footnote ? readInlineText(footnote) : ''
|
|
1191
1259
|
const rowFactSize: ThemeTextSizeInput = isQuartet ? '5-8' : isDuo ? '5-10' : '5-9'
|
|
1192
1260
|
const stackedFactSize: ThemeTextSizeInput = '5-9'
|
|
1193
1261
|
const stackedLabelSize: ThemeTextSizeInput = '3-5'
|
|
@@ -1196,24 +1264,30 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1196
1264
|
const supportFactSize: ThemeTextSizeInput = isFeatured ? '5-7' : isStacked ? stackedFactSize : rowFactSize
|
|
1197
1265
|
const featuredLabelSize: ThemeTextSizeInput = isFeatured ? '4-6' : isStacked ? stackedLabelSize : rowLabelSize
|
|
1198
1266
|
const supportLabelSize: ThemeTextSizeInput = isFeatured ? '2-4' : isStacked ? stackedLabelSize : rowLabelSize
|
|
1267
|
+
const headingContent = readHeadingSlotContent(heading, headingText)
|
|
1268
|
+
const descriptionContent = description ? readParagraphSlotContent(description, readInlineText(description)) : ''
|
|
1269
|
+
const footnoteContent = footnote ? readParagraphSlotContent(footnote, readInlineText(footnote)) : ''
|
|
1270
|
+
const hasFootnote = Array.isArray(footnoteContent)
|
|
1271
|
+
? footnoteContent.length > 0
|
|
1272
|
+
: Boolean(footnoteContent)
|
|
1199
1273
|
|
|
1200
1274
|
const primaryChildren: VNode[] = [
|
|
1201
|
-
hText(context, 'h2', isFeatured ? '5-7' : '6-8',
|
|
1275
|
+
hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingContent, { priority: 1 }),
|
|
1202
1276
|
]
|
|
1203
1277
|
|
|
1204
|
-
if (
|
|
1205
|
-
primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4',
|
|
1278
|
+
if (descriptionContent)
|
|
1279
|
+
primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4', descriptionContent, { priority: 2, muted: true }))
|
|
1206
1280
|
|
|
1207
|
-
if (
|
|
1208
|
-
primaryChildren.push(hText(context, 'div', '2-3',
|
|
1281
|
+
if (hasFootnote && factFitGroup)
|
|
1282
|
+
primaryChildren.push(hText(context, 'div', '2-3', footnoteContent, { priority: 3, muted: true }))
|
|
1209
1283
|
|
|
1210
1284
|
const featuredChildren: VNode[] = [
|
|
1211
1285
|
hText(context, 'div', featuredFactSize, featured.value, { priority: 1 }),
|
|
1212
1286
|
hText(context, 'div', featuredLabelSize, featured.label, { priority: 2, muted: true }),
|
|
1213
1287
|
]
|
|
1214
1288
|
|
|
1215
|
-
if (
|
|
1216
|
-
featuredChildren.push(hText(context, 'div', '2-3',
|
|
1289
|
+
if (hasFootnote && !factFitGroup)
|
|
1290
|
+
featuredChildren.push(hText(context, 'div', '2-3', footnoteContent, { priority: 3, muted: true }))
|
|
1217
1291
|
|
|
1218
1292
|
if (isNumberedQuartet) {
|
|
1219
1293
|
return [
|
|
@@ -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
|
+
}
|