slidev-theme-practicum 0.1.0 → 0.1.2

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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -10
  3. package/components/Slide.vue +81 -26
  4. package/components/Slot.vue +33 -48
  5. package/components/Text.vue +68 -2
  6. package/components/Timeline.vue +2 -23
  7. package/composables/deck-slot-markup.cjs +249 -0
  8. package/composables/decor-catalog.mjs +169 -47
  9. package/composables/layout-authoring.ts +6 -2
  10. package/composables/layout-markdown.mjs +33 -0
  11. package/composables/layout-recipes.ts +51 -5
  12. package/composables/layout-shorthands.ts +251 -81
  13. package/composables/markdown-to-vnodes.cjs +1 -0
  14. package/composables/markdown-to-vnodes.mjs +237 -0
  15. package/composables/slot-placement.ts +1 -1
  16. package/composables/slot-spacing.ts +66 -0
  17. package/composables/text-fit-runtime.ts +458 -76
  18. package/composables/text-fit-scope.ts +13 -0
  19. package/composables/text-slot-markdown.mjs +193 -0
  20. package/composables/text-typography.mjs +2 -0
  21. package/composables/theme-assets.mjs +60 -0
  22. package/composables/timeline-grid.ts +27 -0
  23. package/composables/typescript-require.cjs +29 -0
  24. package/composables/validate-deck-layouts.cjs +277 -0
  25. package/decor-library.md +2 -0
  26. package/example.md +132 -10
  27. package/package.json +25 -11
  28. package/public/photos/photo-1.webp +0 -0
  29. package/public/photos/photo-10.webp +0 -0
  30. package/public/photos/photo-11.webp +0 -0
  31. package/public/photos/photo-12.webp +0 -0
  32. package/public/photos/photo-13.webp +0 -0
  33. package/public/photos/photo-14.webp +0 -0
  34. package/public/photos/photo-15.webp +0 -0
  35. package/public/photos/photo-16.webp +0 -0
  36. package/public/photos/photo-17.webp +0 -0
  37. package/public/photos/photo-18.webp +0 -0
  38. package/public/photos/photo-19.webp +0 -0
  39. package/public/photos/photo-2.webp +0 -0
  40. package/public/photos/photo-20.webp +0 -0
  41. package/public/photos/photo-21.webp +0 -0
  42. package/public/photos/photo-22.webp +0 -0
  43. package/public/photos/photo-23.webp +0 -0
  44. package/public/photos/photo-24.webp +0 -0
  45. package/public/photos/photo-25.webp +0 -0
  46. package/public/photos/photo-26.webp +0 -0
  47. package/public/photos/photo-27.webp +0 -0
  48. package/public/photos/photo-28.webp +0 -0
  49. package/public/photos/photo-29.webp +0 -0
  50. package/public/photos/photo-3.webp +0 -0
  51. package/public/photos/photo-30.webp +0 -0
  52. package/public/photos/photo-4.webp +0 -0
  53. package/public/photos/photo-5.webp +0 -0
  54. package/public/photos/photo-6.webp +0 -0
  55. package/public/photos/photo-7.webp +0 -0
  56. package/public/photos/photo-8.webp +0 -0
  57. package/public/photos/photo-9.webp +0 -0
  58. package/scripts/browser-smoke-runtime.mjs +428 -0
  59. package/scripts/browser-smoke.mjs +424 -0
  60. package/scripts/build-artifact-assets.mjs +55 -0
  61. package/scripts/check-build-artifact.mjs +243 -0
  62. package/scripts/check-package.mjs +132 -0
  63. package/scripts/check-test-architecture.mjs +176 -0
  64. package/scripts/npm-spawn.mjs +31 -0
  65. package/scripts/test-architecture-policy.mjs +245 -0
  66. package/scripts/theme-asset-inventory.mjs +83 -0
  67. package/scripts/validate-deck.cjs +18 -0
  68. package/setup/decor-save-middleware.ts +244 -0
  69. package/setup/vite-plugins.ts +14 -84
  70. package/skills/slidev-practicum/SKILL.md +25 -0
  71. package/skills/slidev-practicum/agents/openai.yaml +4 -0
  72. package/styles/index.css +10 -1
  73. package/composables/layout-registry.ts +0 -47
  74. package/env.d.ts +0 -6
  75. package/public/photos/photo-1.png +0 -0
  76. package/public/photos/photo-10.png +0 -0
  77. package/public/photos/photo-2.png +0 -0
  78. package/public/photos/photo-3.png +0 -0
  79. package/public/photos/photo-4.png +0 -0
  80. package/public/photos/photo-5.png +0 -0
  81. package/public/photos/photo-6.png +0 -0
  82. package/public/photos/photo-7.png +0 -0
  83. package/public/photos/photo-8.png +0 -0
  84. package/public/photos/photo-9.png +0 -0
  85. package/slidev-theme-practicum.png +0 -0
  86. package/slidev-theme-practicum.svg +0 -102
@@ -167,8 +167,7 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
167
167
  content: 'closing',
168
168
  aliases: [{ variant: 'closing' }],
169
169
  regions: {
170
- primary: { area: '4 / 2 / 10 / 12', centered: true },
171
- support: '12 / 1 / -1 / 4',
170
+ primary: { area: '1 / 2 / -1 / 12', centered: true },
172
171
  },
173
172
  },
174
173
  ]),
@@ -177,7 +176,7 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
177
176
  content: 'title-supports',
178
177
  aliases: [{ variant: 'title-supports' }],
179
178
  regions: {
180
- primary: '2 / 1 / -1 / 7',
179
+ primary: { area: '2 / 1 / -1 / 7', marginRight: 4 },
181
180
  secondary: '2 / 7 / -1 / -1',
182
181
  },
183
182
  },
@@ -224,8 +223,8 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
224
223
  content: 'agenda',
225
224
  aliases: [{ variant: 'agenda' }],
226
225
  regions: {
227
- primary: { area: '1 / 1 / -1 / 9', margin: 4 },
228
- media: '1 / 9 / -1 / -1',
226
+ primary: { area: '1 / 1 / -1 / 10', margin: 4 },
227
+ media: '1 / 10 / -1 / -1',
229
228
  },
230
229
  },
231
230
  {
@@ -312,6 +311,30 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
312
311
  ],
313
312
  },
314
313
  },
314
+ {
315
+ content: 'facts',
316
+ aliases: [{ variant: 'facts-duo' }, { variant: 'facts', arrangement: 'duo' }],
317
+ regions: {
318
+ primary: { area: '2 / 1 / 6 / -1' },
319
+ support: [
320
+ { area: '6 / 1 / -1 / 7', surface: 'light', margin: 3 },
321
+ { area: '6 / 7 / -1 / -1', surface: 'light', margin: 3 },
322
+ ],
323
+ },
324
+ },
325
+ {
326
+ content: 'facts',
327
+ aliases: [{ variant: 'facts-quartet' }, { variant: 'facts', arrangement: 'quartet' }],
328
+ regions: {
329
+ primary: { area: '2 / 1 / 6 / -1' },
330
+ support: [
331
+ { area: '6 / 1 / -1 / 4', surface: 'light', margin: 3 },
332
+ { area: '6 / 4 / -1 / 7', surface: 'light', margin: 3 },
333
+ { area: '6 / 7 / -1 / 10', surface: 'light', margin: 3 },
334
+ { area: '6 / 10 / -1 / -1', surface: 'light', margin: 3 },
335
+ ],
336
+ },
337
+ },
315
338
  {
316
339
  content: 'facts',
317
340
  aliases: [{ variant: 'facts-featured' }, { variant: 'facts', arrangement: 'featured' }],
@@ -345,3 +368,26 @@ export function resolveLayoutRecipe(layout: ThemeLayout, variant: string, arrang
345
368
 
346
369
  return recipe
347
370
  }
371
+
372
+ export function resolveLayoutVariant(layout: ThemeLayout | '', variant: string, arrangement = '') {
373
+ if (!layout)
374
+ return variant.trim()
375
+
376
+ return resolveLayoutRecipe(layout, variant, arrangement).aliases[0].variant
377
+ }
378
+
379
+ export function resolveLayoutSpec(layout: ThemeLayout, variant: string) {
380
+ return normalizeRecipeRegions(resolveLayoutRecipe(layout, variant))
381
+ }
382
+
383
+ export function resolveLayoutSlotSpec(
384
+ layout: ThemeLayout,
385
+ variant: string,
386
+ role: ThemeLayoutRole,
387
+ index: number,
388
+ ) {
389
+ const resolved = resolveLayoutSpec(layout, variant).slots[role]?.[index]
390
+ if (!resolved)
391
+ fail('Slide', `Variant "${layout}:${variant}" не поддерживает ${role}[${index}].`)
392
+ return resolved
393
+ }
@@ -1,6 +1,6 @@
1
1
  import { Text as NativeText, cloneVNode, h, isVNode, withCtx, type Component, type VNode } from 'vue'
2
- import { extractMarkdownHeading, extractMarkdownQuote } from './layout-markdown.mjs'
3
- import type { ThemeLayoutRole } from './layout-registry'
2
+ import { extractMarkdownHeading, extractMarkdownQuote, normalizeInlineText } from './layout-markdown.mjs'
3
+ import type { ThemeLayoutRole } from './layout-recipes'
4
4
  import { isBlankTextVNode, isNamedComponentVNode, readVNodeProp, readVNodeTypeName } from './layout-vnode'
5
5
  import type { ThemeTextSizeInput } from './text-fit-runtime'
6
6
 
@@ -25,8 +25,17 @@ export type LayoutShorthandContext = {
25
25
 
26
26
  type LayoutShorthandBuilder = (children: VNode[], context: LayoutShorthandContext) => VNode[]
27
27
 
28
- function failMarkdownContract(label: string, message: string): never {
29
- throw new Error(`[Slide] ${label} ${message}`)
28
+ export type SlideMarkdownContractError = Error & {
29
+ name: 'SlideMarkdownContractError'
30
+ hint?: string
31
+ }
32
+
33
+ function failMarkdownContract(label: string, message: string, hint?: string): never {
34
+ const error = new Error(`[Slide] ${label} ${message}`) as SlideMarkdownContractError
35
+ error.name = 'SlideMarkdownContractError'
36
+ if (hint)
37
+ error.hint = hint
38
+ throw error
30
39
  }
31
40
 
32
41
  function isHeadingNode(node: VNode) {
@@ -53,10 +62,6 @@ function readTextNode(node: VNode) {
53
62
  return typeof node.children === 'string' ? node.children : ''
54
63
  }
55
64
 
56
- function normalizeInlineText(value: string) {
57
- return value.replace(/\s+/g, ' ').trim()
58
- }
59
-
60
65
  function readInlineText(node: VNode): string {
61
66
  if (node.type === NativeText)
62
67
  return normalizeInlineText(readTextNode(node))
@@ -80,7 +85,17 @@ function ensureOnlySupportedChildren(label: string, children: VNode[], allow: (n
80
85
  return
81
86
 
82
87
  const unsupportedLabels = unsupportedNodes.map(readVNodeTypeName).join(', ')
83
- failMarkdownContract(label, `поддерживает только каноническую markdown-структуру. Неподдерживаемые children: ${unsupportedLabels}.`)
88
+ const suggestion = label === 'message:centered'
89
+ ? 'Оставьте только один markdown-заголовок `# …`. Подзаголовок перенесите в заметки автора или объедините с заголовком.'
90
+ : label === 'message:closing'
91
+ ? 'Оставьте один markdown-заголовок `# …` и не более одного markdown-абзаца под ним.'
92
+ : undefined
93
+
94
+ failMarkdownContract(
95
+ label,
96
+ `поддерживает только каноническую markdown-структуру. Неподдерживаемые children: ${unsupportedLabels}.`,
97
+ suggestion,
98
+ )
84
99
  }
85
100
 
86
101
  function readHeadingText(children: VNode[], label: string) {
@@ -203,6 +218,34 @@ function readRecordInput(record: Record<string, unknown>, keys: readonly string[
203
218
  return undefined
204
219
  }
205
220
 
221
+ function readListItemText(li: VNode) {
222
+ if (typeof li.children === 'string')
223
+ return normalizeInlineText(li.children)
224
+
225
+ if (!Array.isArray(li.children))
226
+ return ''
227
+
228
+ const parts: string[] = []
229
+
230
+ for (const child of li.children) {
231
+ if (!isVNode(child)) {
232
+ const text = String(child ?? '').trim()
233
+ if (text)
234
+ parts.push(text)
235
+ continue
236
+ }
237
+
238
+ if (child.type === 'ul' || child.type === 'ol')
239
+ continue
240
+
241
+ const text = readInlineText(child).trim()
242
+ if (text)
243
+ parts.push(text)
244
+ }
245
+
246
+ return normalizeInlineText(parts.join(' '))
247
+ }
248
+
206
249
  function readListItems(list: VNode, label: string) {
207
250
  if (!Array.isArray(list.children))
208
251
  failMarkdownContract(label, 'ожидает list items с текстом.')
@@ -210,10 +253,54 @@ function readListItems(list: VNode, label: string) {
210
253
  return list.children
211
254
  .filter(isVNode)
212
255
  .filter(node => node.type === 'li')
213
- .map(readInlineText)
256
+ .map(readListItemText)
214
257
  .filter(Boolean)
215
258
  }
216
259
 
260
+ function readListItemNodes(list: VNode, label: string) {
261
+ if (!Array.isArray(list.children))
262
+ failMarkdownContract(label, 'ожидает list items с текстом.')
263
+
264
+ return list.children
265
+ .filter(isVNode)
266
+ .filter(node => node.type === 'li')
267
+ }
268
+
269
+ function hasListItemContent(li: VNode) {
270
+ if (typeof li.children === 'string')
271
+ return Boolean(normalizeInlineText(li.children))
272
+
273
+ if (!Array.isArray(li.children))
274
+ return false
275
+
276
+ return li.children.some((child) => {
277
+ if (!isVNode(child))
278
+ return Boolean(String(child).trim())
279
+
280
+ if (child.type === 'ul' || child.type === 'ol')
281
+ return Array.isArray(child.children) && child.children.some(node => isVNode(node) && node.type === 'li')
282
+
283
+ return Boolean(readInlineText(child))
284
+ })
285
+ }
286
+
287
+ function readListItemContent(li: VNode): VNode[] {
288
+ if (typeof li.children === 'string')
289
+ return normalizeInlineText(li.children) ? [h('p', li.children)] : []
290
+
291
+ if (!Array.isArray(li.children))
292
+ return []
293
+
294
+ return li.children.flatMap((child) => {
295
+ if (!isVNode(child)) {
296
+ const text = String(child).trim()
297
+ return text ? [h('p', text)] : []
298
+ }
299
+
300
+ return [cloneVNode(child)]
301
+ })
302
+ }
303
+
217
304
  type NestedListItem = { value: string, label: string }
218
305
 
219
306
  function readNestedListItems(list: VNode, label: string): NestedListItem[] {
@@ -229,10 +316,28 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
229
316
  return { value: text, label: '' }
230
317
  }
231
318
 
232
- const childNodes = li.children.filter(isVNode)
233
- const nestedList = childNodes.find(node => node.type === 'ul' || node.type === 'ol')
234
- const valueNodes = childNodes.filter(node => node !== nestedList)
235
- const value = normalizeInlineText(valueNodes.map(readInlineText).join(' '))
319
+ const valueParts: string[] = []
320
+ let nestedList: VNode | undefined
321
+
322
+ for (const child of li.children) {
323
+ if (!isVNode(child)) {
324
+ const text = String(child ?? '').trim()
325
+ if (text)
326
+ valueParts.push(text)
327
+ continue
328
+ }
329
+
330
+ if (child.type === 'ul' || child.type === 'ol') {
331
+ nestedList = child
332
+ continue
333
+ }
334
+
335
+ const text = readInlineText(child).trim()
336
+ if (text)
337
+ valueParts.push(text)
338
+ }
339
+
340
+ const value = normalizeInlineText(valueParts.join(' '))
236
341
 
237
342
  let label = ''
238
343
  if (nestedList && Array.isArray(nestedList.children)) {
@@ -241,7 +346,7 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
241
346
  .find(node => node.type === 'li')
242
347
 
243
348
  if (firstNestedLi)
244
- label = readInlineText(firstNestedLi)
349
+ label = readListItemText(firstNestedLi)
245
350
  }
246
351
 
247
352
  return { value, label }
@@ -249,6 +354,14 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
249
354
  .filter(item => item.value)
250
355
  }
251
356
 
357
+ const FACTS_VARIANT_COUNTS: Record<string, number> = {
358
+ 'facts-duo': 2,
359
+ 'facts-trio': 3,
360
+ 'facts-quartet': 4,
361
+ 'facts-stacked': 3,
362
+ 'facts-featured': 3,
363
+ }
364
+
252
365
  export function shouldUseExplicitRoleChildren(
253
366
  children: VNode[],
254
367
  shorthandKey: string,
@@ -436,15 +549,20 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
436
549
 
437
550
  function buildClosingChildren(children: VNode[], context: LayoutShorthandContext) {
438
551
  const title = readHeadingText(children, 'message:closing')
552
+ const paragraph = readParagraphText(children)
439
553
 
440
554
  ensureOnlySupportedChildren('message:closing', children, node =>
441
555
  node === title.node
556
+ || node === paragraph?.node
442
557
  || isBlankTextNode(node),
443
558
  )
444
559
 
445
560
  return [
446
561
  hSlot(context, 'primary', 'slide-message-closing-primary', { centered: true }, [
447
562
  hText(context, 'h1', '7', title.text, { align: 'middle' }),
563
+ ...(paragraph
564
+ ? [hText(context, 'p', '3', paragraph.text, { muted: true })]
565
+ : []),
448
566
  ]),
449
567
  ]
450
568
  }
@@ -473,14 +591,56 @@ function buildDefinitionChildren(children: VNode[], context: LayoutShorthandCont
473
591
  ]
474
592
  }
475
593
 
594
+ function isRichTextBodyNode(node: VNode) {
595
+ return isParagraphNode(node) || isListNode(node) || isBlockquoteNode(node)
596
+ }
597
+
598
+ function buildTitleRichSideChildren(
599
+ children: VNode[],
600
+ context: LayoutShorthandContext,
601
+ contract: 'explainer:title-body' | 'explainer:title-supports',
602
+ options: { muted?: boolean } = {},
603
+ ) {
604
+ const title = readHeadingText(children, contract)
605
+ const bodyNodes = children.filter(node =>
606
+ node !== title.node
607
+ && !isBlankTextNode(node),
608
+ )
609
+
610
+ if (!bodyNodes.length)
611
+ failMarkdownContract(contract, 'ожидает markdown paragraph, list или blockquote под heading.')
612
+
613
+ ensureOnlySupportedChildren(contract, children, node =>
614
+ node === title.node
615
+ || isRichTextBodyNode(node)
616
+ || isBlankTextNode(node),
617
+ )
618
+
619
+ const slotPrefix = contract === 'explainer:title-body' ? 'title-body' : 'title-supports'
620
+ const bodyClass = options.muted ? 'Slide-TitleBody Slide-TitleBody_muted' : 'Slide-TitleBody'
621
+
622
+ return [
623
+ hSlot(context, 'primary', `slide-explainer-${slotPrefix}-primary`, {}, [
624
+ hText(context, 'h1', '6', title.text),
625
+ ]),
626
+ hSlot(context, 'secondary', `slide-explainer-${slotPrefix}-secondary`, {}, [
627
+ h('div', { class: bodyClass }, bodyNodes.map(node => cloneVNode(node))),
628
+ ]),
629
+ ]
630
+ }
631
+
476
632
  function buildTitleSupportsChildren(
477
633
  children: VNode[],
478
634
  context: LayoutShorthandContext,
479
635
  options: { placement?: 'side' | 'bottom', muted?: boolean, supportSize?: ThemeTextSizeInput } = {},
480
636
  ) {
637
+ const placement = options.placement ?? 'side'
638
+
639
+ if (placement === 'side')
640
+ return buildTitleRichSideChildren(children, context, 'explainer:title-supports', { muted: true })
641
+
481
642
  const title = readHeadingText(children, 'explainer:title-supports')
482
643
  const list = children.find(isListNode)
483
- const placement = options.placement ?? 'side'
484
644
  const muted = options.muted ?? true
485
645
  const supportSize = options.supportSize ?? '4'
486
646
 
@@ -490,12 +650,17 @@ function buildTitleSupportsChildren(
490
650
  if (list.type !== 'ul')
491
651
  failMarkdownContract('explainer:title-supports', 'ожидает ненумерованный markdown list (`-` или `*`).')
492
652
 
493
- const supports = readListItems(list, 'explainer:title-supports')
494
- if (!supports.length)
653
+ const items = readListItemNodes(list, 'explainer:title-supports')
654
+ if (!items.length || !items.every(hasListItemContent))
495
655
  failMarkdownContract('explainer:title-supports', 'ожидает list items с текстом.')
496
656
 
497
- if (placement === 'bottom' && supports.length !== 2)
498
- failMarkdownContract('explainer:title-supports', 'ожидает ровно два list items для нижнего 50/50 layout.')
657
+ if (items.length !== 2) {
658
+ failMarkdownContract(
659
+ 'explainer:title-supports',
660
+ 'ожидает ровно два list items для нижнего 50/50 layout.',
661
+ 'Нужны ровно 2 пункта `-` верхнего уровня под заголовком. Сожмите список или смените variant на title-supports / title-body.',
662
+ )
663
+ }
499
664
 
500
665
  ensureOnlySupportedChildren('explainer:title-supports', children, node =>
501
666
  node === title.node
@@ -503,57 +668,30 @@ function buildTitleSupportsChildren(
503
668
  || isBlankTextNode(node),
504
669
  )
505
670
 
506
- if (placement === 'bottom') {
507
- return [
508
- hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
509
- hText(context, 'h1', '6', title.text),
510
- ]),
511
- ...supports.map((item, index) =>
512
- hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
513
- hText(context, 'div', supportSize, item, { muted }),
514
- ]),
515
- ),
516
- ]
517
- }
671
+ const bodyClass = muted ? 'Slide-TitleBody Slide-TitleBody_muted' : 'Slide-TitleBody'
518
672
 
519
673
  return [
520
- hSlot(context, 'primary', 'slide-explainer-title-supports-primary', {}, [
674
+ hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
521
675
  hText(context, 'h1', '6', title.text),
522
676
  ]),
523
- hSlot(context, 'secondary', 'slide-explainer-title-supports-secondary', {}, [
524
- cloneVNode(list, { class: 'Slide-TitleSupportsList Slide-TitleSupportsList_size_large' }),
525
- ]),
677
+ ...items.map((item, index) =>
678
+ hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
679
+ h(context.components.Text, {
680
+ as: 'div',
681
+ size: supportSize,
682
+ muted,
683
+ }, {
684
+ default: withCtx(() => [
685
+ h('div', { class: bodyClass }, readListItemContent(item)),
686
+ ]),
687
+ }),
688
+ ]),
689
+ ),
526
690
  ]
527
691
  }
528
692
 
529
- function isRichTextBodyNode(node: VNode) {
530
- return isParagraphNode(node) || isListNode(node) || isBlockquoteNode(node)
531
- }
532
-
533
693
  function buildTitleBodyChildren(children: VNode[], context: LayoutShorthandContext) {
534
- const title = readHeadingText(children, 'explainer:title-body')
535
- const bodyNodes = children.filter(node =>
536
- node !== title.node
537
- && !isBlankTextNode(node),
538
- )
539
-
540
- if (!bodyNodes.length)
541
- failMarkdownContract('explainer:title-body', 'ожидает markdown paragraph, list или blockquote под heading.')
542
-
543
- ensureOnlySupportedChildren('explainer:title-body', children, node =>
544
- node === title.node
545
- || isRichTextBodyNode(node)
546
- || isBlankTextNode(node),
547
- )
548
-
549
- return [
550
- hSlot(context, 'primary', 'slide-explainer-title-body-primary', {}, [
551
- hText(context, 'h1', '6', title.text),
552
- ]),
553
- hSlot(context, 'secondary', 'slide-explainer-title-body-secondary', {}, [
554
- h('div', { class: 'Slide-TitleBody' }, bodyNodes.map(node => cloneVNode(node))),
555
- ]),
556
- ]
694
+ return buildTitleRichSideChildren(children, context, 'explainer:title-body')
557
695
  }
558
696
 
559
697
  function buildPointsChildren(children: VNode[], context: LayoutShorthandContext) {
@@ -726,6 +864,7 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
726
864
  const featuredIndex = metrics.findIndex(metric => metric.featured)
727
865
  const featured = metrics[featuredIndex >= 0 ? featuredIndex : 0]
728
866
  const supportMetrics = metrics.filter(metric => metric !== featured).slice(0, 2)
867
+ const supportMetricsFitGroup = `collection-${context.variant}-support-metrics`
729
868
 
730
869
  if (supportMetrics.length < 2)
731
870
  failMarkdownContract('collection:metrics', 'ожидает два вторичных metric item.')
@@ -758,7 +897,10 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
758
897
  ]),
759
898
  ),
760
899
  ...supportMetrics.map((metric, index) =>
761
- hSlot(context, 'support', `slide-collection-metrics-support-${index + 1}`, { gap: '2' }, [
900
+ hSlot(context, 'support', `slide-collection-metrics-support-${index + 1}`, {
901
+ fitGroup: supportMetricsFitGroup,
902
+ gap: '2',
903
+ }, [
762
904
  hText(context, 'div', '7', metric.value, { priority: 1 }),
763
905
  hText(context, 'div', '2-6', metric.label, { priority: 2, muted: true, typography: 'compact' }),
764
906
  ]),
@@ -797,26 +939,45 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
797
939
  )
798
940
 
799
941
  const facts = readNestedListItems(list, 'collection:facts')
800
-
801
- if (facts.length < 3)
802
- failMarkdownContract('collection:facts', 'ожидает минимум три факта в markdown-списке.')
942
+ const expectedFactCount = FACTS_VARIANT_COUNTS[context.variant] ?? 3
943
+
944
+ if (facts.length !== expectedFactCount) {
945
+ failMarkdownContract(
946
+ 'collection:facts',
947
+ `${context.variant} ожидает ровно ${expectedFactCount} факта в markdown-списке.`,
948
+ `Для ${context.variant} нужно ровно ${expectedFactCount} пункта верхнего уровня, у каждого — вложенная подпись.`,
949
+ )
950
+ }
803
951
 
804
952
  facts.forEach((fact, index) => {
805
- if (!fact.label)
806
- failMarkdownContract('collection:facts', `ожидает вложенный пункт с подписью у facts[${index}].`)
953
+ if (!fact.label) {
954
+ failMarkdownContract(
955
+ 'collection:facts',
956
+ `ожидает вложенный пункт с подписью у facts[${index}].`,
957
+ 'Каждый факт: `- значение` и на следующей строке ` - подпись`. Не пишите `- роль: текст` одной строкой.',
958
+ )
959
+ }
807
960
  })
808
961
 
809
- const [featured, ...rest] = facts
810
- const supportFacts = rest.slice(0, 2)
811
-
812
- if (supportFacts.length < 2)
813
- failMarkdownContract('collection:facts', 'ожидает минимум два дополнительных факта в markdown-списке.')
814
-
962
+ const [featured, ...supportFacts] = facts
815
963
  const isFeatured = context.variant === 'facts-featured'
816
964
  const isStacked = context.variant === 'facts-stacked'
965
+ const isDuo = context.variant === 'facts-duo'
966
+ const isQuartet = context.variant === 'facts-quartet'
967
+ const shouldCoordinateFactSizes = isStacked || isDuo || context.variant === 'facts-trio' || isQuartet
968
+ const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
969
+ const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
817
970
  const headingText = readInlineText(heading)
818
971
  const descriptionText = description ? readInlineText(description) : ''
819
972
  const footnoteText = footnote ? readInlineText(footnote) : ''
973
+ const rowFactSize: ThemeTextSizeInput = isQuartet ? '5-8' : isDuo ? '5-10' : '5-9'
974
+ const stackedFactSize: ThemeTextSizeInput = '5-9'
975
+ const stackedLabelSize: ThemeTextSizeInput = '2-4'
976
+ const rowLabelSize: ThemeTextSizeInput = '2-4'
977
+ const featuredFactSize: ThemeTextSizeInput = isFeatured ? '9-12' : isStacked ? stackedFactSize : rowFactSize
978
+ const supportFactSize: ThemeTextSizeInput = isFeatured ? '5-7' : isStacked ? stackedFactSize : rowFactSize
979
+ const featuredLabelSize: ThemeTextSizeInput = isFeatured ? '4-6' : isStacked ? stackedLabelSize : rowLabelSize
980
+ const supportLabelSize: ThemeTextSizeInput = isFeatured ? '2-4' : isStacked ? stackedLabelSize : rowLabelSize
820
981
 
821
982
  const primaryChildren: VNode[] = [
822
983
  hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingText, { priority: 1 }),
@@ -825,12 +986,15 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
825
986
  if (descriptionText)
826
987
  primaryChildren.push(hText(context, 'p', isStacked ? '4-5' : '3-4', descriptionText, { priority: 2, muted: true }))
827
988
 
989
+ if (footnoteText && factFitGroup)
990
+ primaryChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
991
+
828
992
  const featuredChildren: VNode[] = [
829
- hText(context, 'div', isFeatured ? '9-12' : '7-10', featured.value, { priority: 1 }),
830
- hText(context, 'div', isFeatured ? '4-6' : '3-5', featured.label, { priority: 2, muted: true }),
993
+ hText(context, 'div', featuredFactSize, featured.value, { priority: 1 }),
994
+ hText(context, 'div', featuredLabelSize, featured.label, { priority: 2, muted: true }),
831
995
  ]
832
996
 
833
- if (footnoteText)
997
+ if (footnoteText && !factFitGroup)
834
998
  featuredChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
835
999
 
836
1000
  return [
@@ -838,11 +1002,15 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
838
1002
  hSlot(context, 'support', `slide-collection-${context.variant}-support-1`, {
839
1003
  gap: '2',
840
1004
  centered: false,
1005
+ ...(factFitGroup ? { fitGroup: factFitGroup } : {}),
841
1006
  }, featuredChildren),
842
1007
  ...supportFacts.map((fact, index) =>
843
- hSlot(context, 'support', `slide-collection-${context.variant}-support-${index + 2}`, { gap: '2' }, [
844
- hText(context, 'div', isFeatured ? '5-7' : '7-9', fact.value, { priority: 1 }),
845
- hText(context, 'div', '2-4', fact.label, { priority: 2, muted: true }),
1008
+ hSlot(context, 'support', `slide-collection-${context.variant}-support-${index + 2}`, {
1009
+ gap: '2',
1010
+ ...(supportFactFitGroup ? { fitGroup: supportFactFitGroup } : {}),
1011
+ }, [
1012
+ hText(context, 'div', supportFactSize, fact.value, { priority: 1 }),
1013
+ hText(context, 'div', supportLabelSize, fact.label, { priority: 2, muted: true }),
846
1014
  ]),
847
1015
  ),
848
1016
  ]
@@ -851,7 +1019,9 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
851
1019
  const LAYOUT_SHORTHANDS = {
852
1020
  'collection:agenda': buildAgendaChildren,
853
1021
  'collection:facts-stacked': buildFactsChildren,
1022
+ 'collection:facts-duo': buildFactsChildren,
854
1023
  'collection:facts-trio': buildFactsChildren,
1024
+ 'collection:facts-quartet': buildFactsChildren,
855
1025
  'collection:facts-featured': buildFactsChildren,
856
1026
  'collection:points-trio': buildPointsChildren,
857
1027
  'collection:timeline': buildTimelineChildren,
@@ -0,0 +1 @@
1
+ module.exports = require('./markdown-to-vnodes.mjs')