slidev-theme-practicum 0.3.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 +78 -4
- package/components/Slide.vue +1 -1
- package/components/Slot.vue +2 -2
- package/components/StepsGrid.vue +19 -14
- package/components/Text.vue +5 -0
- package/composables/deck-slot-markup.cjs +19 -5
- package/composables/layout-shorthands.ts +152 -67
- package/composables/text-fit-runtime.ts +7 -3
- package/composables/theme-foundation.ts +7 -3
- package/composables/typography-guard.cjs +59 -0
- package/composables/validate-deck-layouts.cjs +18 -1
- package/composables/validate-deck-typography.cjs +101 -0
- package/example.md +1 -1
- package/package.json +14 -5
- package/scripts/browser-smoke.mjs +20 -23
- package/scripts/check-accessibility.mjs +364 -0
- package/scripts/check-consumer.mjs +159 -0
- package/scripts/check-pixels.mjs +251 -0
- package/scripts/check-typography.mjs +108 -0
- package/scripts/test-typography.mjs +89 -0
- package/scripts/typography-browser.mjs +134 -0
- package/scripts/validate-deck.cjs +22 -2
- package/skills/slidev-practicum/SKILL.md +1 -0
- package/styles/index.css +20 -27
- package/styles/vars.css +6 -2
|
@@ -118,10 +118,64 @@ function readHeadingText(children: VNode[], label: string) {
|
|
|
118
118
|
failMarkdownContract(label, 'ожидает один markdown heading внутри default slot.')
|
|
119
119
|
}
|
|
120
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
|
+
|
|
121
171
|
function readParagraphText(children: VNode[]) {
|
|
122
172
|
const paragraph = children.find(isParagraphNode)
|
|
123
173
|
if (paragraph)
|
|
124
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
node: paragraph,
|
|
176
|
+
text: readInlineText(paragraph),
|
|
177
|
+
content: readParagraphSlotContent(paragraph, readInlineText(paragraph)),
|
|
178
|
+
}
|
|
125
179
|
|
|
126
180
|
return null
|
|
127
181
|
}
|
|
@@ -130,7 +184,7 @@ function hText(
|
|
|
130
184
|
context: LayoutShorthandContext,
|
|
131
185
|
as: string,
|
|
132
186
|
size: ThemeTextSizeInput,
|
|
133
|
-
content: string
|
|
187
|
+
content: string | VNode | Array<string | VNode>,
|
|
134
188
|
options: {
|
|
135
189
|
muted?: boolean
|
|
136
190
|
maxSize?: boolean
|
|
@@ -172,10 +226,12 @@ function hText(
|
|
|
172
226
|
if (options.typography)
|
|
173
227
|
textProps['data-typography'] = options.typography
|
|
174
228
|
|
|
229
|
+
const slotChildren = Array.isArray(content) ? content : [content]
|
|
230
|
+
|
|
175
231
|
return h(context.components.Text, {
|
|
176
232
|
...textProps,
|
|
177
233
|
}, {
|
|
178
|
-
default: withCtx(() =>
|
|
234
|
+
default: withCtx(() => slotChildren),
|
|
179
235
|
})
|
|
180
236
|
}
|
|
181
237
|
|
|
@@ -251,15 +307,26 @@ function readListItemText(li: VNode) {
|
|
|
251
307
|
return normalizeInlineText(parts.join(' '))
|
|
252
308
|
}
|
|
253
309
|
|
|
254
|
-
function
|
|
255
|
-
if (
|
|
256
|
-
|
|
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
|
+
}
|
|
257
315
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
})
|
|
263
330
|
}
|
|
264
331
|
|
|
265
332
|
function readListItemNodes(list: VNode, label: string) {
|
|
@@ -296,7 +363,7 @@ function readListItemContent(li: VNode): VNode[] {
|
|
|
296
363
|
if (!Array.isArray(li.children))
|
|
297
364
|
return []
|
|
298
365
|
|
|
299
|
-
return li.children.flatMap((child) => {
|
|
366
|
+
return li.children.flatMap<VNode>((child): VNode[] => {
|
|
300
367
|
if (!isVNode(child)) {
|
|
301
368
|
const text = String(child).trim()
|
|
302
369
|
return text ? [h('p', text)] : []
|
|
@@ -306,7 +373,10 @@ function readListItemContent(li: VNode): VNode[] {
|
|
|
306
373
|
})
|
|
307
374
|
}
|
|
308
375
|
|
|
309
|
-
type NestedListItem = {
|
|
376
|
+
type NestedListItem = {
|
|
377
|
+
value: Array<string | VNode>
|
|
378
|
+
label: Array<string | VNode>
|
|
379
|
+
}
|
|
310
380
|
|
|
311
381
|
function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
312
382
|
if (!Array.isArray(list.children))
|
|
@@ -317,46 +387,38 @@ function readNestedListItems(list: VNode, label: string): NestedListItem[] {
|
|
|
317
387
|
.filter(node => node.type === 'li')
|
|
318
388
|
.map((li) => {
|
|
319
389
|
if (!Array.isArray(li.children)) {
|
|
320
|
-
const text = typeof li.children === 'string' ?
|
|
321
|
-
|
|
390
|
+
const text = typeof li.children === 'string' ? li.children : ''
|
|
391
|
+
const value = text.trim() ? [text] : []
|
|
392
|
+
return { value, label: [] }
|
|
322
393
|
}
|
|
323
394
|
|
|
324
|
-
const valueParts: string[] = []
|
|
325
395
|
let nestedList: VNode | undefined
|
|
326
|
-
|
|
327
|
-
for (const child of li.children) {
|
|
396
|
+
const value = li.children.flatMap<string | VNode>((child): Array<string | VNode> => {
|
|
328
397
|
if (!isVNode(child)) {
|
|
329
|
-
const text = String(child
|
|
330
|
-
|
|
331
|
-
valueParts.push(text)
|
|
332
|
-
continue
|
|
398
|
+
const text = String(child)
|
|
399
|
+
return text.trim() ? [text] : []
|
|
333
400
|
}
|
|
334
401
|
|
|
335
402
|
if (child.type === 'ul' || child.type === 'ol') {
|
|
336
403
|
nestedList = child
|
|
337
|
-
|
|
404
|
+
return []
|
|
338
405
|
}
|
|
339
406
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
valueParts.push(text)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const value = normalizeInlineText(valueParts.join(' '))
|
|
407
|
+
return [cloneVNode(child)]
|
|
408
|
+
})
|
|
346
409
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const firstNestedLi = nestedList.children
|
|
410
|
+
const nestedLabel = nestedList && Array.isArray(nestedList.children)
|
|
411
|
+
? nestedList.children
|
|
350
412
|
.filter(isVNode)
|
|
351
413
|
.find(node => node.type === 'li')
|
|
414
|
+
: null
|
|
352
415
|
|
|
353
|
-
|
|
354
|
-
|
|
416
|
+
return {
|
|
417
|
+
value,
|
|
418
|
+
label: nestedLabel ? readListItemSlotContent(nestedLabel) : [],
|
|
355
419
|
}
|
|
356
|
-
|
|
357
|
-
return { value, label }
|
|
358
420
|
})
|
|
359
|
-
.filter(item => item.value)
|
|
421
|
+
.filter(item => hasRichSlotContent(item.value))
|
|
360
422
|
}
|
|
361
423
|
|
|
362
424
|
const FACTS_VARIANT_COUNTS: Record<string, number> = {
|
|
@@ -540,6 +602,7 @@ function buildQuoteChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
540
602
|
|
|
541
603
|
function buildCenteredChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
542
604
|
const title = readHeadingText(children, 'message:centered')
|
|
605
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
543
606
|
|
|
544
607
|
ensureOnlySupportedChildren('message:centered', children, node =>
|
|
545
608
|
node === title.node
|
|
@@ -548,7 +611,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
548
611
|
|
|
549
612
|
return [
|
|
550
613
|
hSlot(context, 'primary', 'slide-message-centered-primary', { centered: true }, [
|
|
551
|
-
hText(context, 'h1', '7-12',
|
|
614
|
+
hText(context, 'h1', '7-12', titleContent, { align: 'middle' }),
|
|
552
615
|
]),
|
|
553
616
|
]
|
|
554
617
|
}
|
|
@@ -556,6 +619,7 @@ function buildCenteredChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
556
619
|
function buildClosingChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
557
620
|
const title = readHeadingText(children, 'message:closing')
|
|
558
621
|
const paragraph = readParagraphText(children)
|
|
622
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
559
623
|
|
|
560
624
|
ensureOnlySupportedChildren('message:closing', children, node =>
|
|
561
625
|
node === title.node
|
|
@@ -565,9 +629,9 @@ function buildClosingChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
565
629
|
|
|
566
630
|
return [
|
|
567
631
|
hSlot(context, 'primary', 'slide-message-closing-primary', { centered: true }, [
|
|
568
|
-
hText(context, 'h1', '7',
|
|
632
|
+
hText(context, 'h1', '7', titleContent, { align: 'middle' }),
|
|
569
633
|
...(paragraph
|
|
570
|
-
? [hText(context, 'p', '3', paragraph.
|
|
634
|
+
? [hText(context, 'p', '3', paragraph.content, { muted: true })]
|
|
571
635
|
: []),
|
|
572
636
|
]),
|
|
573
637
|
]
|
|
@@ -575,8 +639,9 @@ function buildClosingChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
575
639
|
|
|
576
640
|
function buildDefinitionChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
577
641
|
const title = readHeadingText(children, 'explainer:definition')
|
|
642
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
578
643
|
const paragraph = readParagraphText(children)
|
|
579
|
-
const body = context.readFrontmatterText('body') || paragraph?.
|
|
644
|
+
const body = context.readFrontmatterText('body') || paragraph?.content || ''
|
|
580
645
|
const label = context.readFrontmatterText('label')
|
|
581
646
|
|
|
582
647
|
if (!body)
|
|
@@ -591,7 +656,7 @@ function buildDefinitionChildren(children: VNode[], context: LayoutShorthandCont
|
|
|
591
656
|
return [
|
|
592
657
|
hSlot(context, 'primary', 'slide-explainer-definition-primary', {}, [
|
|
593
658
|
...(label ? [hText(context, 'div', '3', label, { muted: true, className: 'Slide-DefinitionLabel' })] : []),
|
|
594
|
-
hText(context, 'h1', '6',
|
|
659
|
+
hText(context, 'h1', '6', titleContent, { priority: 1 }),
|
|
595
660
|
hText(context, 'div', '6', body, { muted: true, priority: 1 }),
|
|
596
661
|
]),
|
|
597
662
|
]
|
|
@@ -608,6 +673,7 @@ function buildTitleRichSideChildren(
|
|
|
608
673
|
options: { muted?: boolean } = {},
|
|
609
674
|
) {
|
|
610
675
|
const title = readHeadingText(children, contract)
|
|
676
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
611
677
|
const bodyNodes = children.filter(node =>
|
|
612
678
|
node !== title.node
|
|
613
679
|
&& !isBlankTextNode(node),
|
|
@@ -627,7 +693,7 @@ function buildTitleRichSideChildren(
|
|
|
627
693
|
|
|
628
694
|
return [
|
|
629
695
|
hSlot(context, 'primary', `slide-explainer-${slotPrefix}-primary`, {}, [
|
|
630
|
-
hText(context, 'h1', '6',
|
|
696
|
+
hText(context, 'h1', '6', titleContent),
|
|
631
697
|
]),
|
|
632
698
|
hSlot(context, 'secondary', `slide-explainer-${slotPrefix}-secondary`, {}, [
|
|
633
699
|
h('div', { class: bodyClass }, bodyNodes.map(node => cloneVNode(node))),
|
|
@@ -646,6 +712,7 @@ function buildTitleSupportsChildren(
|
|
|
646
712
|
return buildTitleRichSideChildren(children, context, 'explainer:title-supports', { muted: true })
|
|
647
713
|
|
|
648
714
|
const title = readHeadingText(children, 'explainer:title-supports')
|
|
715
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
649
716
|
const list = children.find(isListNode)
|
|
650
717
|
const muted = options.muted ?? true
|
|
651
718
|
const supportSize = options.supportSize ?? '4'
|
|
@@ -678,7 +745,7 @@ function buildTitleSupportsChildren(
|
|
|
678
745
|
|
|
679
746
|
return [
|
|
680
747
|
hSlot(context, 'primary', 'slide-explainer-title-supports-bottom-primary', {}, [
|
|
681
|
-
hText(context, 'h1', '6',
|
|
748
|
+
hText(context, 'h1', '6', titleContent),
|
|
682
749
|
]),
|
|
683
750
|
...items.map((item, index) =>
|
|
684
751
|
hSlot(context, 'support', `slide-explainer-title-supports-bottom-support-${index + 1}`, {}, [
|
|
@@ -702,12 +769,21 @@ function buildTitleBodyChildren(children: VNode[], context: LayoutShorthandConte
|
|
|
702
769
|
|
|
703
770
|
function buildPointsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
704
771
|
const title = readHeadingText(children, 'collection:points')
|
|
772
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
705
773
|
const list = children.find(isListNode)
|
|
706
774
|
|
|
707
775
|
if (!list)
|
|
708
776
|
failMarkdownContract('collection:points', 'ожидает markdown list с тремя пунктами.')
|
|
709
777
|
|
|
710
|
-
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
|
+
|
|
711
787
|
if (items.length !== 3)
|
|
712
788
|
failMarkdownContract('collection:points', 'ожидает ровно три пункта для arrangement: trio.')
|
|
713
789
|
|
|
@@ -721,7 +797,7 @@ function buildPointsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
721
797
|
hSlot(context, 'primary', 'slide-collection-points-primary', {
|
|
722
798
|
...(context.frontmatterDecor ? { decor: context.frontmatterDecor } : {}),
|
|
723
799
|
}, [
|
|
724
|
-
hText(context, 'h1', '7',
|
|
800
|
+
hText(context, 'h1', '7', titleContent, { className: 'Slide-PointsTitle' }),
|
|
725
801
|
]),
|
|
726
802
|
...items.map((item, index) =>
|
|
727
803
|
hSlot(context, 'support', `slide-collection-points-support-${index + 1}`, {}, [
|
|
@@ -778,6 +854,7 @@ function buildComparisonSideChildren(
|
|
|
778
854
|
|
|
779
855
|
function buildComparisonChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
780
856
|
const title = readHeadingText(children, 'collection:comparison')
|
|
857
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
781
858
|
const comparison = context.readFrontmatterObject('comparison')
|
|
782
859
|
|
|
783
860
|
if (!comparison)
|
|
@@ -796,7 +873,7 @@ function buildComparisonChildren(children: VNode[], context: LayoutShorthandCont
|
|
|
796
873
|
|
|
797
874
|
return [
|
|
798
875
|
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
799
|
-
hText(context, 'h1', '6',
|
|
876
|
+
hText(context, 'h1', '6', titleContent),
|
|
800
877
|
]),
|
|
801
878
|
buildComparisonSideChildren(context, from, 0),
|
|
802
879
|
buildComparisonSideChildren(context, to, 1),
|
|
@@ -826,12 +903,13 @@ type StepItem = {
|
|
|
826
903
|
function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
|
|
827
904
|
const items = context.readFrontmatterArray('items')
|
|
828
905
|
const isStaggered = context.variant === 'steps-staggered'
|
|
906
|
+
const minItems = isStaggered ? 2 : 3
|
|
829
907
|
|
|
830
|
-
if (items.length <
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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
|
+
}
|
|
835
913
|
|
|
836
914
|
const normalized = items.map((item, index) => {
|
|
837
915
|
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
@@ -859,6 +937,7 @@ function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
|
|
|
859
937
|
|
|
860
938
|
function buildStepsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
861
939
|
const title = readHeadingText(children, 'collection:steps')
|
|
940
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
862
941
|
const items = normalizeStepItems(context)
|
|
863
942
|
const arrangement = context.variant === 'steps-staggered' ? 'staggered' : 'linear'
|
|
864
943
|
|
|
@@ -869,7 +948,7 @@ function buildStepsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
869
948
|
|
|
870
949
|
return [
|
|
871
950
|
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
872
|
-
hText(context, 'h1', '6',
|
|
951
|
+
hText(context, 'h1', '6', titleContent),
|
|
873
952
|
]),
|
|
874
953
|
hSlot(context, 'support', `slide-collection-${context.variant}-support`, {}, [
|
|
875
954
|
h(context.components.StepsGrid, {
|
|
@@ -907,8 +986,9 @@ function normalizeTimelineItems(context: LayoutShorthandContext) {
|
|
|
907
986
|
|
|
908
987
|
function buildTimelineChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
909
988
|
const title = readHeadingText(children, 'collection:timeline')
|
|
989
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
910
990
|
const paragraph = readParagraphText(children)
|
|
911
|
-
const body = context.readFrontmatterText('body') || paragraph?.
|
|
991
|
+
const body = context.readFrontmatterText('body') || paragraph?.content || ''
|
|
912
992
|
|
|
913
993
|
ensureOnlySupportedChildren('collection:timeline', children, node =>
|
|
914
994
|
node === title.node
|
|
@@ -918,7 +998,7 @@ function buildTimelineChildren(children: VNode[], context: LayoutShorthandContex
|
|
|
918
998
|
|
|
919
999
|
return [
|
|
920
1000
|
hSlot(context, 'primary', 'slide-collection-timeline-primary', {}, [
|
|
921
|
-
hText(context, 'h1', '6',
|
|
1001
|
+
hText(context, 'h1', '6', titleContent),
|
|
922
1002
|
]),
|
|
923
1003
|
...(body
|
|
924
1004
|
? [
|
|
@@ -991,7 +1071,7 @@ function normalizeMetricItemsFromList(list: VNode) {
|
|
|
991
1071
|
failMarkdownContract('collection:metrics', 'ожидает минимум три пункта в markdown-списке.')
|
|
992
1072
|
|
|
993
1073
|
return entries.map((entry, index) => {
|
|
994
|
-
if (!entry.label)
|
|
1074
|
+
if (!hasRichSlotContent(entry.label))
|
|
995
1075
|
failMarkdownContract('collection:metrics', `ожидает вложенный пункт с подписью у metric[${index}].`)
|
|
996
1076
|
|
|
997
1077
|
return {
|
|
@@ -1023,6 +1103,7 @@ function normalizeMediaItems(context: LayoutShorthandContext) {
|
|
|
1023
1103
|
function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
1024
1104
|
if (context.variant === 'metrics-dashboard') {
|
|
1025
1105
|
const title = readHeadingText(children, 'collection:metrics')
|
|
1106
|
+
const titleContent = readHeadingSlotContent(title.node, title.text)
|
|
1026
1107
|
const metrics = normalizeDashboardMetrics(context)
|
|
1027
1108
|
const fitGroup = 'collection-metrics-dashboard'
|
|
1028
1109
|
|
|
@@ -1033,7 +1114,7 @@ function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext
|
|
|
1033
1114
|
|
|
1034
1115
|
return [
|
|
1035
1116
|
hSlot(context, 'primary', 'slide-collection-metrics-dashboard-primary', {}, [
|
|
1036
|
-
hText(context, 'h1', '6',
|
|
1117
|
+
hText(context, 'h1', '6', titleContent),
|
|
1037
1118
|
]),
|
|
1038
1119
|
...metrics.map((metric, index) =>
|
|
1039
1120
|
hSlot(context, 'support', `slide-collection-metrics-dashboard-support-${index + 1}`, {
|
|
@@ -1156,7 +1237,7 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1156
1237
|
}
|
|
1157
1238
|
|
|
1158
1239
|
facts.forEach((fact, index) => {
|
|
1159
|
-
if (!fact.label) {
|
|
1240
|
+
if (!hasRichSlotContent(fact.label)) {
|
|
1160
1241
|
failMarkdownContract(
|
|
1161
1242
|
'collection:facts',
|
|
1162
1243
|
`ожидает вложенный пункт с подписью у facts[${index}].`,
|
|
@@ -1175,8 +1256,6 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1175
1256
|
const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
|
|
1176
1257
|
const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
|
|
1177
1258
|
const headingText = readInlineText(heading)
|
|
1178
|
-
const descriptionText = description ? readInlineText(description) : ''
|
|
1179
|
-
const footnoteText = footnote ? readInlineText(footnote) : ''
|
|
1180
1259
|
const rowFactSize: ThemeTextSizeInput = isQuartet ? '5-8' : isDuo ? '5-10' : '5-9'
|
|
1181
1260
|
const stackedFactSize: ThemeTextSizeInput = '5-9'
|
|
1182
1261
|
const stackedLabelSize: ThemeTextSizeInput = '3-5'
|
|
@@ -1185,24 +1264,30 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1185
1264
|
const supportFactSize: ThemeTextSizeInput = isFeatured ? '5-7' : isStacked ? stackedFactSize : rowFactSize
|
|
1186
1265
|
const featuredLabelSize: ThemeTextSizeInput = isFeatured ? '4-6' : isStacked ? stackedLabelSize : rowLabelSize
|
|
1187
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)
|
|
1188
1273
|
|
|
1189
1274
|
const primaryChildren: VNode[] = [
|
|
1190
|
-
hText(context, 'h2', isFeatured ? '5-7' : '6-8',
|
|
1275
|
+
hText(context, 'h2', isFeatured ? '5-7' : '6-8', headingContent, { priority: 1 }),
|
|
1191
1276
|
]
|
|
1192
1277
|
|
|
1193
|
-
if (
|
|
1194
|
-
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 }))
|
|
1195
1280
|
|
|
1196
|
-
if (
|
|
1197
|
-
primaryChildren.push(hText(context, 'div', '2-3',
|
|
1281
|
+
if (hasFootnote && factFitGroup)
|
|
1282
|
+
primaryChildren.push(hText(context, 'div', '2-3', footnoteContent, { priority: 3, muted: true }))
|
|
1198
1283
|
|
|
1199
1284
|
const featuredChildren: VNode[] = [
|
|
1200
1285
|
hText(context, 'div', featuredFactSize, featured.value, { priority: 1 }),
|
|
1201
1286
|
hText(context, 'div', featuredLabelSize, featured.label, { priority: 2, muted: true }),
|
|
1202
1287
|
]
|
|
1203
1288
|
|
|
1204
|
-
if (
|
|
1205
|
-
featuredChildren.push(hText(context, 'div', '2-3',
|
|
1289
|
+
if (hasFootnote && !factFitGroup)
|
|
1290
|
+
featuredChildren.push(hText(context, 'div', '2-3', footnoteContent, { priority: 3, muted: true }))
|
|
1206
1291
|
|
|
1207
1292
|
if (isNumberedQuartet) {
|
|
1208
1293
|
return [
|
|
@@ -190,8 +190,8 @@ function readBounds(input: ElementFitInput) {
|
|
|
190
190
|
return bounds
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function readFitObserverElements(element: HTMLElement) {
|
|
194
|
-
const elements: HTMLElement[] = [element]
|
|
193
|
+
function readFitObserverElements(element: HTMLElement, observeOwnSize: boolean) {
|
|
194
|
+
const elements: HTMLElement[] = observeOwnSize ? [element] : []
|
|
195
195
|
let parent = element.parentElement
|
|
196
196
|
|
|
197
197
|
while (parent && elements.length < 8) {
|
|
@@ -496,6 +496,7 @@ function createTextFitCleanupOwner(isDisposed: () => boolean) {
|
|
|
496
496
|
type TextFitLifecycleOwnerInput = {
|
|
497
497
|
adapter: TextFitAdapter
|
|
498
498
|
fit: () => void | Promise<void>
|
|
499
|
+
observeOwnSize?: boolean
|
|
499
500
|
onDispose: () => void
|
|
500
501
|
target: Ref<HTMLElement | null>
|
|
501
502
|
watchTarget?: boolean
|
|
@@ -504,6 +505,7 @@ type TextFitLifecycleOwnerInput = {
|
|
|
504
505
|
function createTextFitLifecycleOwner({
|
|
505
506
|
adapter,
|
|
506
507
|
fit,
|
|
508
|
+
observeOwnSize = true,
|
|
507
509
|
onDispose,
|
|
508
510
|
target,
|
|
509
511
|
watchTarget = false,
|
|
@@ -532,7 +534,7 @@ function createTextFitLifecycleOwner({
|
|
|
532
534
|
if (!element)
|
|
533
535
|
return null
|
|
534
536
|
|
|
535
|
-
return adapter.observe(readFitObserverElements(element), scheduleFit)
|
|
537
|
+
return adapter.observe(readFitObserverElements(element, observeOwnSize), scheduleFit)
|
|
536
538
|
})
|
|
537
539
|
}
|
|
538
540
|
|
|
@@ -734,6 +736,7 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
|
|
|
734
736
|
const { scheduleFit } = createTextFitLifecycleOwner({
|
|
735
737
|
adapter,
|
|
736
738
|
fit: fitNow,
|
|
739
|
+
observeOwnSize: false,
|
|
737
740
|
onDispose: () => {
|
|
738
741
|
runId += 1
|
|
739
742
|
},
|
|
@@ -876,6 +879,7 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
|
|
|
876
879
|
const { scheduleFit } = createTextFitLifecycleOwner({
|
|
877
880
|
adapter,
|
|
878
881
|
fit: fitNow,
|
|
882
|
+
observeOwnSize: false,
|
|
879
883
|
onDispose: () => {
|
|
880
884
|
runId += 1
|
|
881
885
|
unregisterSharedItem()
|
|
@@ -13,6 +13,7 @@ export type ResolvedThemeMode = {
|
|
|
13
13
|
text: string
|
|
14
14
|
muted: string
|
|
15
15
|
contrast: boolean
|
|
16
|
+
foreground: 'light' | 'dark'
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export type ResolvedSlideTheme = ResolvedThemeMode
|
|
@@ -112,6 +113,7 @@ export function resolveThemeMode(options: ResolveThemeModeOptions = {}): Resolve
|
|
|
112
113
|
text: 'var(--theme-color-dark-0)',
|
|
113
114
|
muted: 'var(--theme-color-dark-2)',
|
|
114
115
|
contrast: false,
|
|
116
|
+
foreground: 'dark',
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
@@ -123,6 +125,7 @@ export function resolveThemeMode(options: ResolveThemeModeOptions = {}): Resolve
|
|
|
123
125
|
text: 'var(--theme-color-light-0)',
|
|
124
126
|
muted: 'var(--theme-text-muted-on-contrast)',
|
|
125
127
|
contrast: true,
|
|
128
|
+
foreground: 'light',
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -130,9 +133,10 @@ export function resolveThemeMode(options: ResolveThemeModeOptions = {}): Resolve
|
|
|
130
133
|
mode,
|
|
131
134
|
tone,
|
|
132
135
|
background: `var(--theme-color-${tone}-0)`,
|
|
133
|
-
text: 'var(--theme-
|
|
134
|
-
muted: 'var(--theme-text-muted-on-
|
|
136
|
+
text: 'var(--theme-text-on-color)',
|
|
137
|
+
muted: 'var(--theme-text-muted-on-color)',
|
|
135
138
|
contrast: true,
|
|
139
|
+
foreground: 'light',
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -176,7 +180,7 @@ export function themeVars(
|
|
|
176
180
|
'--theme-text-muted': slideTheme.muted,
|
|
177
181
|
'--theme-inline-code-text': slideTheme.text,
|
|
178
182
|
'--theme-text-on-dark': 'var(--theme-color-light-0)',
|
|
179
|
-
'--theme-link': slideTheme.
|
|
183
|
+
'--theme-link': slideTheme.foreground === 'light' ? 'var(--theme-color-light-0)' : slideTheme.mode === 'color' ? 'var(--theme-text-on-color)' : 'var(--theme-current-color)',
|
|
180
184
|
'--theme-current-color': `var(--theme-color-${slideTheme.tone}-0)`,
|
|
181
185
|
}
|
|
182
186
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{ text: string, size?: string, color?: string, slot: string, keyNumber: boolean, muted?: boolean }} TypographyText
|
|
3
|
+
* @typedef {{ code: string, message: string, hint?: string }} TypographyIssue
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @param {TypographyText[]} texts */
|
|
7
|
+
function describeSizes(texts) {
|
|
8
|
+
return [...new Set(texts.map(text => `${text.size}: «${text.text}»`))].join(', ')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Общая политика для объявленных токенов и вычисленных значений браузера.
|
|
13
|
+
* Отсутствующее значение означает «неизвестно», а не значение по умолчанию.
|
|
14
|
+
* @param {TypographyText[]} texts
|
|
15
|
+
* @returns {TypographyIssue[]}
|
|
16
|
+
*/
|
|
17
|
+
function inspectTypography(texts) {
|
|
18
|
+
/** @type {TypographyIssue[]} */
|
|
19
|
+
const issues = []
|
|
20
|
+
const regular = texts.filter(text => !text.keyNumber && text.size !== undefined)
|
|
21
|
+
const numbers = texts.filter(text => text.keyNumber && text.size !== undefined)
|
|
22
|
+
if (new Set(regular.map(text => text.size)).size > 2) {
|
|
23
|
+
issues.push({
|
|
24
|
+
code: 'text-sizes',
|
|
25
|
+
message: `Больше двух размеров обычного текста: ${describeSizes(regular)}.`,
|
|
26
|
+
hint: 'Объедините подписи с основным поясняющим текстом по размеру или измените композицию. Заголовок тоже входит в подсчёт.',
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
if (new Set(numbers.map(text => text.size)).size > 1) {
|
|
30
|
+
issues.push({
|
|
31
|
+
code: 'number-sizes',
|
|
32
|
+
message: `Ключевые числа имеют разные размеры: ${describeSizes(numbers)}.`,
|
|
33
|
+
hint: 'Все Text as="data" на слайде должны иметь один размер, в том числе после автоматического подбора.',
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const slot of new Set(texts.map(text => text.slot))) {
|
|
38
|
+
const contents = texts.filter(text => text.slot === slot)
|
|
39
|
+
const colored = contents.filter(text => text.color !== undefined)
|
|
40
|
+
if (new Set(colored.map(text => text.color)).size > 1) {
|
|
41
|
+
issues.push({
|
|
42
|
+
code: 'slot-colors',
|
|
43
|
+
message: `${slot}: разные цвета текста: ${colored.map(text => `${text.color}: «${text.text}»`).join(', ')}.`,
|
|
44
|
+
hint: 'Выберите один основной цвет для всего текста внутри Slot.',
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
const muted = contents.filter(text => text.muted)
|
|
48
|
+
if (muted.length) {
|
|
49
|
+
issues.push({
|
|
50
|
+
code: 'muted-text',
|
|
51
|
+
message: `${slot}: приглушённый текст: ${muted.map(text => `«${text.text}»`).join(', ')}.`,
|
|
52
|
+
hint: 'Удалите muted и токены text-muted, используйте основной цвет поверхности.',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return issues
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { inspectTypography }
|
|
@@ -5,6 +5,7 @@ const { createCommentVNode, createTextVNode, createVNode } = require('vue')
|
|
|
5
5
|
const { registerTypeScript } = require('./typescript-require.cjs')
|
|
6
6
|
const { extractDeckLiveStructure } = require('./deck-slot-markup.cjs')
|
|
7
7
|
const { markdownToVnodes } = require('./markdown-to-vnodes.cjs')
|
|
8
|
+
const { validateDeclaredTypography } = require('./validate-deck-typography.cjs')
|
|
8
9
|
|
|
9
10
|
/** @typedef {import('@slidev/types').SourceSlideInfo} SourceSlideInfo */
|
|
10
11
|
/** @typedef {import('./layout-recipes').ThemeLayout} ThemeLayout */
|
|
@@ -138,9 +139,10 @@ function slideRef(slide, index) {
|
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* @param {string} deckPath
|
|
142
|
+
* @param {{ typographyGuard?: boolean }} [options]
|
|
141
143
|
* @returns {Promise<DeckValidationIssue[]>}
|
|
142
144
|
*/
|
|
143
|
-
async function validateDeckLayouts(deckPath) {
|
|
145
|
+
async function validateDeckLayouts(deckPath, options = {}) {
|
|
144
146
|
const absolutePath = isAbsolute(deckPath) ? deckPath : resolve(deckPath)
|
|
145
147
|
const source = readFileSync(absolutePath, 'utf8')
|
|
146
148
|
const deck = parseSync(source, absolutePath)
|
|
@@ -158,6 +160,21 @@ async function validateDeckLayouts(deckPath) {
|
|
|
158
160
|
const layout = String(slide.frontmatter?.layout ?? '').trim()
|
|
159
161
|
const ref = slideRef(slide, index)
|
|
160
162
|
|
|
163
|
+
if (options.typographyGuard) {
|
|
164
|
+
if (slide.frontmatter?.src) {
|
|
165
|
+
issues.push({ ...ref, message: 'Статическая проверка типографики пока не раскрывает включения src. Соберите слайды в один Markdown-файл перед проверкой.' })
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const structure = extractDeckLiveStructure(slide.content ?? '')
|
|
170
|
+
issues.push(...validateDeclaredTypography(structure.children).map(issue => ({ ...ref, ...issue })))
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
issues.push({ ...ref, message: error instanceof Error ? error.message : String(error) })
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
161
178
|
if (index > 0 && Object.prototype.hasOwnProperty.call(slide.frontmatter ?? {}, 'title')) {
|
|
162
179
|
issues.push({
|
|
163
180
|
...ref,
|