slidev-theme-practicum 0.1.4 → 0.2.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 +91 -0
- package/components/Slide.vue +69 -0
- package/composables/layout-recipes.ts +67 -0
- package/composables/layout-shorthands.ts +243 -1
- package/example.md +135 -5
- package/package.json +1 -1
- package/scripts/browser-smoke.mjs +103 -5
package/README.md
CHANGED
|
@@ -58,8 +58,11 @@ theme: practicum
|
|
|
58
58
|
| Объяснить понятие, ситуацию или решение | Поясняющий слайд (`explainer`) | Заголовок и связное пояснение |
|
|
59
59
|
| Дать большой текстовый блок с разметкой | Текстовый блок (`explainer`, `title-body`) | Абзац, затем короткий список или критерий |
|
|
60
60
|
| Показать план, правила или шаги | Коллекцию (`collection`) | Однотипные пункты в одном формате |
|
|
61
|
+
| Сопоставить два состояния или подхода | Сравнение (`collection`, `comparison`) | Две стороны с самостоятельными заголовками |
|
|
62
|
+
| Показать последовательность действий | Шаги (`collection`, `steps`) | От 3 до 6 коротких действий |
|
|
61
63
|
| Показать события во времени | Таймлайн (`collection`, `timeline`) | Годы или месяцы и короткие подписи |
|
|
62
64
|
| Показать несколько чисел | Метрики (`collection`, `metrics`) | Главное число и подписи, которые объясняют масштаб |
|
|
65
|
+
| Дать четыре пронумерованные опоры | Факты (`collection`, `facts`) | Четыре значения с вложенными подписями |
|
|
63
66
|
| Собрать нестандартную композицию | Ручную сетку (`none`) | Только когда обычные типы не выражают задачу |
|
|
64
67
|
|
|
65
68
|
## Что писать
|
|
@@ -108,6 +111,94 @@ layout: cover | message | explainer | collection | none
|
|
|
108
111
|
|
|
109
112
|
`layout: cover` markdown shorthand **не** имеет: для всех `cover:*` нужны явные `<Slot role="...">` и `<Text>` (см. обложки в [example.md](example.md)).
|
|
110
113
|
|
|
114
|
+
### Варианты collection P0
|
|
115
|
+
|
|
116
|
+
| `variant` | `arrangement` | Контракт |
|
|
117
|
+
| ------------- | ------------------ | -------- |
|
|
118
|
+
| `comparison` | `before-after` | Markdown heading; `comparison.from` и `comparison.to` с обязательным `title`; необязательные `kicker`, `body` и `relation.label` |
|
|
119
|
+
| `comparison` | `stable-variable` | Тот же контракт; вертикальное направление задаёт arrangement |
|
|
120
|
+
| `steps` | `linear` | Markdown heading; `items` из 3–6 объектов с обязательным `title`, необязательными `body`, `label`; не больше одного `active: true` |
|
|
121
|
+
| `steps` | `staggered` | Тот же контракт, но ровно 5 элементов |
|
|
122
|
+
| `metrics` | `dashboard` | Markdown heading; ровно 5 `metrics` с обязательными `value` и одним из `body`, `label`, `title`; без `media` и произвольных spans |
|
|
123
|
+
| `facts` | `numbered-quartet` | Markdown heading; ровно 4 пункта вида «значение → вложенная подпись»; номера 1–4 добавляет тема |
|
|
124
|
+
|
|
125
|
+
Канонический alias для пронумерованных фактов — `variant: facts-numbered-quartet`. Для остальных вариантов также можно использовать aliases `comparison-before-after`, `comparison-stable-variable`, `steps-linear`, `steps-staggered` и `metrics-dashboard` без `arrangement`.
|
|
126
|
+
|
|
127
|
+
Минимальное сравнение:
|
|
128
|
+
|
|
129
|
+
```md
|
|
130
|
+
---
|
|
131
|
+
layout: collection
|
|
132
|
+
variant: comparison
|
|
133
|
+
arrangement: before-after
|
|
134
|
+
comparison:
|
|
135
|
+
from:
|
|
136
|
+
title: Ручная проверка
|
|
137
|
+
to:
|
|
138
|
+
title: Единый контур
|
|
139
|
+
relation:
|
|
140
|
+
label: переход
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
# Как меняется процесс
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Минимальные шаги:
|
|
147
|
+
|
|
148
|
+
```md
|
|
149
|
+
---
|
|
150
|
+
layout: collection
|
|
151
|
+
variant: steps
|
|
152
|
+
arrangement: linear
|
|
153
|
+
items:
|
|
154
|
+
- title: Собрать входы
|
|
155
|
+
- title: Проверить риски
|
|
156
|
+
active: true
|
|
157
|
+
- title: Зафиксировать результат
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
# Три шага
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Минимальный dashboard:
|
|
164
|
+
|
|
165
|
+
```md
|
|
166
|
+
---
|
|
167
|
+
layout: collection
|
|
168
|
+
variant: metrics
|
|
169
|
+
arrangement: dashboard
|
|
170
|
+
metrics:
|
|
171
|
+
- { value: 42%, label: прошли сценарий }
|
|
172
|
+
- { value: 18, label: команд }
|
|
173
|
+
- { value: 9, label: проверок }
|
|
174
|
+
- { value: 2×, label: быстрее обратная связь }
|
|
175
|
+
- { value: 0, label: ручных spans }
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
# Панель результата
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Минимальный `numbered-quartet`:
|
|
182
|
+
|
|
183
|
+
```md
|
|
184
|
+
---
|
|
185
|
+
layout: collection
|
|
186
|
+
variant: facts
|
|
187
|
+
arrangement: numbered-quartet
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
# Четыре опоры
|
|
191
|
+
|
|
192
|
+
- Входы
|
|
193
|
+
- собраны до старта
|
|
194
|
+
- Риски
|
|
195
|
+
- проверены явно
|
|
196
|
+
- Результат
|
|
197
|
+
- сохранён в репозитории
|
|
198
|
+
- Стоп
|
|
199
|
+
- задан до запуска
|
|
200
|
+
```
|
|
201
|
+
|
|
111
202
|
| Слой | Для чего |
|
|
112
203
|
| ------------- | ------------------------------------------------ |
|
|
113
204
|
| `layout` | семантика слайда на уровне Slidev |
|
package/components/Slide.vue
CHANGED
|
@@ -366,6 +366,75 @@ const slideClass = computed(() => ({
|
|
|
366
366
|
justify-content: flex-start;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
:deep(.Slide-ComparisonRelation) {
|
|
370
|
+
display: flex;
|
|
371
|
+
min-width: 0;
|
|
372
|
+
min-height: 100%;
|
|
373
|
+
flex-direction: column;
|
|
374
|
+
align-items: center;
|
|
375
|
+
justify-content: center;
|
|
376
|
+
gap: calc(var(--theme-grid-module) * 2);
|
|
377
|
+
text-align: center;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
:deep(.Slide-ComparisonDirection) {
|
|
381
|
+
font-size: var(--theme-text-size-5);
|
|
382
|
+
line-height: var(--theme-text-line-5);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
:deep(.Slide-StepsGrid) {
|
|
386
|
+
display: grid;
|
|
387
|
+
width: 100%;
|
|
388
|
+
height: 100%;
|
|
389
|
+
min-width: 0;
|
|
390
|
+
min-height: 0;
|
|
391
|
+
grid-template-columns: repeat(var(--slide-steps-count), minmax(0, 1fr));
|
|
392
|
+
gap: var(--theme-grid-gap);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
:deep(.Slide-Step) {
|
|
396
|
+
display: flex;
|
|
397
|
+
min-width: 0;
|
|
398
|
+
min-height: 0;
|
|
399
|
+
flex-direction: column;
|
|
400
|
+
gap: calc(var(--theme-grid-module) * 2);
|
|
401
|
+
padding: var(--theme-slot-margin-3);
|
|
402
|
+
border-radius: var(--theme-panel-radius);
|
|
403
|
+
background: var(--theme-surface-light);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
:deep(.Slide-StepsGrid_staggered) {
|
|
407
|
+
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
408
|
+
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
:deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(1)) {
|
|
412
|
+
grid-area: 1 / 1 / 2 / 3;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
:deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(2)) {
|
|
416
|
+
grid-area: 1 / 3 / 2 / 5;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
:deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(3)) {
|
|
420
|
+
grid-area: 1 / 5 / 2 / 7;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
:deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(4)) {
|
|
424
|
+
grid-area: 2 / 2 / 3 / 4;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
:deep(.Slide-StepsGrid_staggered .Slide-Step:nth-child(5)) {
|
|
428
|
+
grid-area: 2 / 4 / 3 / 6;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
:deep(.Slide-Step_active) {
|
|
432
|
+
background: var(--theme-current-color);
|
|
433
|
+
color: var(--theme-text-on-dark);
|
|
434
|
+
--theme-text: var(--theme-text-on-dark);
|
|
435
|
+
--theme-text-muted: var(--theme-text-muted-on-contrast);
|
|
436
|
+
}
|
|
437
|
+
|
|
369
438
|
:deep(.Slide-Quote) {
|
|
370
439
|
max-width: var(--theme-grid-span-11-width);
|
|
371
440
|
font-size: var(--theme-text-size-7);
|
|
@@ -239,6 +239,46 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
239
239
|
],
|
|
240
240
|
},
|
|
241
241
|
},
|
|
242
|
+
{
|
|
243
|
+
content: 'comparison',
|
|
244
|
+
aliases: [{ variant: 'comparison-before-after' }, { variant: 'comparison', arrangement: 'before-after' }],
|
|
245
|
+
regions: {
|
|
246
|
+
primary: '1 / 1 / 4 / -1',
|
|
247
|
+
secondary: [
|
|
248
|
+
{ area: '4 / 1 / -1 / 6', surface: 'light', margin: 4 },
|
|
249
|
+
{ area: '4 / 8 / -1 / -1', surface: 'light', margin: 4 },
|
|
250
|
+
],
|
|
251
|
+
support: { area: '4 / 6 / -1 / 8', centered: true },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
content: 'comparison',
|
|
256
|
+
aliases: [{ variant: 'comparison-stable-variable' }, { variant: 'comparison', arrangement: 'stable-variable' }],
|
|
257
|
+
regions: {
|
|
258
|
+
primary: '1 / 1 / 4 / -1',
|
|
259
|
+
secondary: [
|
|
260
|
+
{ area: '4 / 1 / 8 / -1', surface: 'light', margin: 4 },
|
|
261
|
+
{ area: '9 / 1 / -1 / -1', surface: 'light', margin: 4 },
|
|
262
|
+
],
|
|
263
|
+
support: { area: '8 / 1 / 9 / -1', centered: true },
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
content: 'steps',
|
|
268
|
+
aliases: [{ variant: 'steps-linear' }, { variant: 'steps', arrangement: 'linear' }],
|
|
269
|
+
regions: {
|
|
270
|
+
primary: '1 / 1 / 4 / -1',
|
|
271
|
+
support: '4 / 1 / -1 / -1',
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
content: 'steps',
|
|
276
|
+
aliases: [{ variant: 'steps-staggered' }, { variant: 'steps', arrangement: 'staggered' }],
|
|
277
|
+
regions: {
|
|
278
|
+
primary: '1 / 1 / 4 / -1',
|
|
279
|
+
support: '4 / 1 / -1 / -1',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
242
282
|
{
|
|
243
283
|
content: 'metrics',
|
|
244
284
|
aliases: [{ variant: 'metrics-featured-media' }, { variant: 'metrics', arrangement: 'featured-media' }],
|
|
@@ -251,6 +291,20 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
251
291
|
],
|
|
252
292
|
},
|
|
253
293
|
},
|
|
294
|
+
{
|
|
295
|
+
content: 'metrics',
|
|
296
|
+
aliases: [{ variant: 'metrics-dashboard' }, { variant: 'metrics', arrangement: 'dashboard' }],
|
|
297
|
+
regions: {
|
|
298
|
+
primary: '1 / 1 / 4 / -1',
|
|
299
|
+
support: [
|
|
300
|
+
{ area: '4 / 1 / 8 / 5', surface: 'light', margin: 3 },
|
|
301
|
+
{ area: '4 / 5 / 8 / 9', surface: 'light', margin: 3 },
|
|
302
|
+
{ area: '4 / 9 / 8 / -1', surface: 'light', margin: 3 },
|
|
303
|
+
{ area: '8 / 1 / -1 / 7', surface: 'light', margin: 3 },
|
|
304
|
+
{ area: '8 / 7 / -1 / -1', surface: 'light', margin: 3 },
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
254
308
|
{
|
|
255
309
|
content: 'metrics',
|
|
256
310
|
aliases: [{ variant: 'metrics-featured-copy' }, { variant: 'metrics', arrangement: 'featured-copy' }],
|
|
@@ -335,6 +389,19 @@ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
|
|
|
335
389
|
],
|
|
336
390
|
},
|
|
337
391
|
},
|
|
392
|
+
{
|
|
393
|
+
content: 'facts',
|
|
394
|
+
aliases: [{ variant: 'facts-numbered-quartet' }, { variant: 'facts', arrangement: 'numbered-quartet' }],
|
|
395
|
+
regions: {
|
|
396
|
+
primary: '1 / 1 / 4 / -1',
|
|
397
|
+
support: [
|
|
398
|
+
{ area: '4 / 1 / 8 / 7', surface: 'light', margin: 3 },
|
|
399
|
+
{ area: '4 / 7 / 8 / -1', surface: 'light', margin: 3 },
|
|
400
|
+
{ area: '8 / 1 / -1 / 7', surface: 'light', margin: 3 },
|
|
401
|
+
{ area: '8 / 7 / -1 / -1', surface: 'light', margin: 3 },
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
338
405
|
{
|
|
339
406
|
content: 'facts',
|
|
340
407
|
aliases: [{ variant: 'facts-featured' }, { variant: 'facts', arrangement: 'featured' }],
|
|
@@ -136,6 +136,7 @@ function hText(
|
|
|
136
136
|
priority?: 1 | 2 | 3
|
|
137
137
|
align?: 'start' | 'middle' | 'end'
|
|
138
138
|
className?: string
|
|
139
|
+
color?: 'light-0'
|
|
139
140
|
typography?: 'compact'
|
|
140
141
|
} = {},
|
|
141
142
|
) {
|
|
@@ -147,6 +148,7 @@ function hText(
|
|
|
147
148
|
priority?: 1 | 2 | 3
|
|
148
149
|
align?: 'start' | 'middle' | 'end'
|
|
149
150
|
class?: string
|
|
151
|
+
color?: 'light-0'
|
|
150
152
|
} & {
|
|
151
153
|
'data-typography'?: 'compact'
|
|
152
154
|
} = {
|
|
@@ -164,6 +166,8 @@ function hText(
|
|
|
164
166
|
textProps.align = options.align
|
|
165
167
|
if (options.className)
|
|
166
168
|
textProps.class = options.className
|
|
169
|
+
if (options.color)
|
|
170
|
+
textProps.color = options.color
|
|
167
171
|
if (options.typography)
|
|
168
172
|
textProps['data-typography'] = options.typography
|
|
169
173
|
|
|
@@ -358,6 +362,7 @@ const FACTS_VARIANT_COUNTS: Record<string, number> = {
|
|
|
358
362
|
'facts-duo': 2,
|
|
359
363
|
'facts-trio': 3,
|
|
360
364
|
'facts-quartet': 4,
|
|
365
|
+
'facts-numbered-quartet': 4,
|
|
361
366
|
'facts-stacked': 3,
|
|
362
367
|
'facts-featured': 3,
|
|
363
368
|
}
|
|
@@ -726,6 +731,166 @@ function buildPointsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
726
731
|
]
|
|
727
732
|
}
|
|
728
733
|
|
|
734
|
+
type ComparisonSide = {
|
|
735
|
+
title: string
|
|
736
|
+
kicker: string
|
|
737
|
+
body: string
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function normalizeComparisonSide(
|
|
741
|
+
value: unknown,
|
|
742
|
+
side: 'from' | 'to',
|
|
743
|
+
): ComparisonSide {
|
|
744
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
745
|
+
failMarkdownContract('collection:comparison', `ожидает object в comparison.${side}.`)
|
|
746
|
+
|
|
747
|
+
const record = value as Record<string, unknown>
|
|
748
|
+
const title = readRecordText(record, ['title'])
|
|
749
|
+
|
|
750
|
+
if (!title)
|
|
751
|
+
failMarkdownContract('collection:comparison', `ожидает обязательный title в comparison.${side}.`)
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
title,
|
|
755
|
+
kicker: readRecordText(record, ['kicker']),
|
|
756
|
+
body: readRecordText(record, ['body']),
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function buildComparisonSideChildren(
|
|
761
|
+
context: LayoutShorthandContext,
|
|
762
|
+
side: ComparisonSide,
|
|
763
|
+
index: number,
|
|
764
|
+
) {
|
|
765
|
+
return hSlot(context, 'secondary', `slide-collection-${context.variant}-side-${index + 1}`, {
|
|
766
|
+
gap: '3',
|
|
767
|
+
}, [
|
|
768
|
+
...(side.kicker
|
|
769
|
+
? [hText(context, 'div', '2', side.kicker, { priority: 3, muted: true })]
|
|
770
|
+
: []),
|
|
771
|
+
hText(context, 'div', '5-7', side.title, { priority: 1 }),
|
|
772
|
+
...(side.body
|
|
773
|
+
? [hText(context, 'div', '3-4', side.body, { priority: 2, muted: true })]
|
|
774
|
+
: []),
|
|
775
|
+
])
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function buildComparisonChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
779
|
+
const title = readHeadingText(children, 'collection:comparison')
|
|
780
|
+
const comparison = context.readFrontmatterObject('comparison')
|
|
781
|
+
|
|
782
|
+
if (!comparison)
|
|
783
|
+
failMarkdownContract('collection:comparison', 'ожидает frontmatter comparison с полями from и to.')
|
|
784
|
+
|
|
785
|
+
const relation = context.readFrontmatterObject('relation')
|
|
786
|
+
const from = normalizeComparisonSide(comparison.from, 'from')
|
|
787
|
+
const to = normalizeComparisonSide(comparison.to, 'to')
|
|
788
|
+
const relationLabel = relation ? readRecordText(relation, ['label']) : ''
|
|
789
|
+
const isBeforeAfter = context.variant === 'comparison-before-after'
|
|
790
|
+
|
|
791
|
+
ensureOnlySupportedChildren('collection:comparison', children, node =>
|
|
792
|
+
node === title.node
|
|
793
|
+
|| isBlankTextNode(node),
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
return [
|
|
797
|
+
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
798
|
+
hText(context, 'h1', '6', title.text),
|
|
799
|
+
]),
|
|
800
|
+
buildComparisonSideChildren(context, from, 0),
|
|
801
|
+
buildComparisonSideChildren(context, to, 1),
|
|
802
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-relation`, {}, [
|
|
803
|
+
h('div', {
|
|
804
|
+
class: [
|
|
805
|
+
'Slide-ComparisonRelation',
|
|
806
|
+
`Slide-ComparisonRelation_${isBeforeAfter ? 'horizontal' : 'vertical'}`,
|
|
807
|
+
],
|
|
808
|
+
}, [
|
|
809
|
+
h('span', { 'class': 'Slide-ComparisonDirection', 'aria-hidden': 'true' }, isBeforeAfter ? '→' : '↓'),
|
|
810
|
+
...(relationLabel
|
|
811
|
+
? [hText(context, 'div', '2-3', relationLabel, { align: 'middle', muted: true })]
|
|
812
|
+
: []),
|
|
813
|
+
]),
|
|
814
|
+
]),
|
|
815
|
+
]
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
type StepItem = {
|
|
819
|
+
title: string
|
|
820
|
+
body: string
|
|
821
|
+
label: string
|
|
822
|
+
active: boolean
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function normalizeStepItems(context: LayoutShorthandContext): StepItem[] {
|
|
826
|
+
const items = context.readFrontmatterArray('items')
|
|
827
|
+
const isStaggered = context.variant === 'steps-staggered'
|
|
828
|
+
|
|
829
|
+
if (items.length < 3 || items.length > 6)
|
|
830
|
+
failMarkdownContract('collection:steps', 'ожидает от 3 до 6 элементов во frontmatter items.')
|
|
831
|
+
|
|
832
|
+
if (isStaggered && items.length !== 5)
|
|
833
|
+
failMarkdownContract('collection:steps', 'arrangement: staggered ожидает ровно 5 элементов.')
|
|
834
|
+
|
|
835
|
+
const normalized = items.map((item, index) => {
|
|
836
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
837
|
+
failMarkdownContract('collection:steps', `ожидает object item в items[${index}].`)
|
|
838
|
+
|
|
839
|
+
const record = item as Record<string, unknown>
|
|
840
|
+
const title = readRecordText(record, ['title'])
|
|
841
|
+
|
|
842
|
+
if (!title)
|
|
843
|
+
failMarkdownContract('collection:steps', `ожидает обязательный title в items[${index}].`)
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
title,
|
|
847
|
+
body: readRecordText(record, ['body']),
|
|
848
|
+
label: readRecordText(record, ['label']) || String(index + 1),
|
|
849
|
+
active: record.active === true,
|
|
850
|
+
}
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
if (normalized.filter(item => item.active).length > 1)
|
|
854
|
+
failMarkdownContract('collection:steps', 'поддерживает не больше одного элемента с active: true.')
|
|
855
|
+
|
|
856
|
+
return normalized
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function buildStepsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
860
|
+
const title = readHeadingText(children, 'collection:steps')
|
|
861
|
+
const items = normalizeStepItems(context)
|
|
862
|
+
const arrangement = context.variant === 'steps-staggered' ? 'staggered' : 'linear'
|
|
863
|
+
|
|
864
|
+
ensureOnlySupportedChildren('collection:steps', children, node =>
|
|
865
|
+
node === title.node
|
|
866
|
+
|| isBlankTextNode(node),
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
return [
|
|
870
|
+
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, {}, [
|
|
871
|
+
hText(context, 'h1', '6', title.text),
|
|
872
|
+
]),
|
|
873
|
+
hSlot(context, 'support', `slide-collection-${context.variant}-support`, {}, [
|
|
874
|
+
h('div', {
|
|
875
|
+
'class': ['Slide-StepsGrid', `Slide-StepsGrid_${arrangement}`],
|
|
876
|
+
'data-count': String(items.length),
|
|
877
|
+
'style': { '--slide-steps-count': items.length },
|
|
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
|
+
)),
|
|
890
|
+
]),
|
|
891
|
+
]
|
|
892
|
+
}
|
|
893
|
+
|
|
729
894
|
function normalizeTimelineItems(context: LayoutShorthandContext) {
|
|
730
895
|
const items = context.readFrontmatterArray('items')
|
|
731
896
|
|
|
@@ -804,6 +969,32 @@ function normalizeMetricItems(context: LayoutShorthandContext) {
|
|
|
804
969
|
})
|
|
805
970
|
}
|
|
806
971
|
|
|
972
|
+
function normalizeDashboardMetrics(context: LayoutShorthandContext) {
|
|
973
|
+
const metrics = context.readFrontmatterArray('metrics')
|
|
974
|
+
|
|
975
|
+
if (metrics.length !== 5)
|
|
976
|
+
failMarkdownContract('collection:metrics', 'arrangement: dashboard ожидает ровно 5 элементов во frontmatter metrics.')
|
|
977
|
+
|
|
978
|
+
return metrics.map((item, index) => {
|
|
979
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
980
|
+
failMarkdownContract('collection:metrics', `ожидает object item в metrics[${index}].`)
|
|
981
|
+
|
|
982
|
+
const record = item as Record<string, unknown>
|
|
983
|
+
const value = readRecordText(record, ['value'])
|
|
984
|
+
const label = readRecordText(record, ['body', 'label', 'title'])
|
|
985
|
+
const hasArbitrarySpan = ['area', 'col', 'row', 'span', 'colSpan', 'rowSpan']
|
|
986
|
+
.some(key => record[key] !== undefined)
|
|
987
|
+
|
|
988
|
+
if (!value || !label)
|
|
989
|
+
failMarkdownContract('collection:metrics', `ожидает value и body/label/title в metrics[${index}].`)
|
|
990
|
+
|
|
991
|
+
if (hasArbitrarySpan)
|
|
992
|
+
failMarkdownContract('collection:metrics', `не принимает произвольные координаты или spans в metrics[${index}].`)
|
|
993
|
+
|
|
994
|
+
return { value, label }
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
|
|
807
998
|
function normalizeMetricItemsFromList(list: VNode) {
|
|
808
999
|
const entries = readNestedListItems(list, 'collection:metrics')
|
|
809
1000
|
|
|
@@ -841,6 +1032,32 @@ function normalizeMediaItems(context: LayoutShorthandContext) {
|
|
|
841
1032
|
}
|
|
842
1033
|
|
|
843
1034
|
function buildMetricsChildren(children: VNode[], context: LayoutShorthandContext) {
|
|
1035
|
+
if (context.variant === 'metrics-dashboard') {
|
|
1036
|
+
const title = readHeadingText(children, 'collection:metrics')
|
|
1037
|
+
const metrics = normalizeDashboardMetrics(context)
|
|
1038
|
+
const fitGroup = 'collection-metrics-dashboard'
|
|
1039
|
+
|
|
1040
|
+
ensureOnlySupportedChildren('collection:metrics', children, node =>
|
|
1041
|
+
node === title.node
|
|
1042
|
+
|| isBlankTextNode(node),
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
return [
|
|
1046
|
+
hSlot(context, 'primary', 'slide-collection-metrics-dashboard-primary', {}, [
|
|
1047
|
+
hText(context, 'h1', '6', title.text),
|
|
1048
|
+
]),
|
|
1049
|
+
...metrics.map((metric, index) =>
|
|
1050
|
+
hSlot(context, 'support', `slide-collection-metrics-dashboard-support-${index + 1}`, {
|
|
1051
|
+
fitGroup,
|
|
1052
|
+
gap: '2',
|
|
1053
|
+
}, [
|
|
1054
|
+
hText(context, 'div', index < 3 ? '5-7' : '6-8', metric.value, { priority: 1 }),
|
|
1055
|
+
hText(context, 'div', '2-4', metric.label, { priority: 2, muted: true, typography: 'compact' }),
|
|
1056
|
+
]),
|
|
1057
|
+
),
|
|
1058
|
+
]
|
|
1059
|
+
}
|
|
1060
|
+
|
|
844
1061
|
const list = children.find(isListNode)
|
|
845
1062
|
|
|
846
1063
|
ensureOnlySupportedChildren('collection:metrics', children, node =>
|
|
@@ -964,7 +1181,8 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
964
1181
|
const isStacked = context.variant === 'facts-stacked'
|
|
965
1182
|
const isDuo = context.variant === 'facts-duo'
|
|
966
1183
|
const isQuartet = context.variant === 'facts-quartet'
|
|
967
|
-
const
|
|
1184
|
+
const isNumberedQuartet = context.variant === 'facts-numbered-quartet'
|
|
1185
|
+
const shouldCoordinateFactSizes = isStacked || isDuo || context.variant === 'facts-trio' || isQuartet || isNumberedQuartet
|
|
968
1186
|
const factFitGroup = shouldCoordinateFactSizes ? `collection-${context.variant}-facts` : ''
|
|
969
1187
|
const supportFactFitGroup = isFeatured ? `collection-${context.variant}-support-facts` : factFitGroup
|
|
970
1188
|
const headingText = readInlineText(heading)
|
|
@@ -997,6 +1215,24 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
997
1215
|
if (footnoteText && !factFitGroup)
|
|
998
1216
|
featuredChildren.push(hText(context, 'div', '2-3', footnoteText, { priority: 3, muted: true }))
|
|
999
1217
|
|
|
1218
|
+
if (isNumberedQuartet) {
|
|
1219
|
+
return [
|
|
1220
|
+
hSlot(context, 'primary', 'slide-collection-facts-numbered-quartet-primary', { gap: '3' }, primaryChildren),
|
|
1221
|
+
...facts.map((fact, index) =>
|
|
1222
|
+
hSlot(context, 'support', `slide-collection-facts-numbered-quartet-support-${index + 1}`, {
|
|
1223
|
+
gap: '2',
|
|
1224
|
+
fitGroup: factFitGroup,
|
|
1225
|
+
}, [
|
|
1226
|
+
hText(context, 'div', '4', String(index + 1), {
|
|
1227
|
+
priority: 1,
|
|
1228
|
+
}),
|
|
1229
|
+
hText(context, 'div', '4', fact.value, { priority: 1, align: 'end' }),
|
|
1230
|
+
hText(context, 'div', '2-3', fact.label, { priority: 2, muted: true }),
|
|
1231
|
+
]),
|
|
1232
|
+
),
|
|
1233
|
+
]
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1000
1236
|
return [
|
|
1001
1237
|
hSlot(context, 'primary', `slide-collection-${context.variant}-primary`, { gap: '3' }, primaryChildren),
|
|
1002
1238
|
hSlot(context, 'support', `slide-collection-${context.variant}-support-1`, {
|
|
@@ -1018,16 +1254,22 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
1018
1254
|
|
|
1019
1255
|
const LAYOUT_SHORTHANDS = {
|
|
1020
1256
|
'collection:agenda': buildAgendaChildren,
|
|
1257
|
+
'collection:comparison-before-after': buildComparisonChildren,
|
|
1258
|
+
'collection:comparison-stable-variable': buildComparisonChildren,
|
|
1021
1259
|
'collection:facts-stacked': buildFactsChildren,
|
|
1022
1260
|
'collection:facts-duo': buildFactsChildren,
|
|
1023
1261
|
'collection:facts-trio': buildFactsChildren,
|
|
1024
1262
|
'collection:facts-quartet': buildFactsChildren,
|
|
1263
|
+
'collection:facts-numbered-quartet': buildFactsChildren,
|
|
1025
1264
|
'collection:facts-featured': buildFactsChildren,
|
|
1026
1265
|
'collection:points-trio': buildPointsChildren,
|
|
1266
|
+
'collection:steps-linear': buildStepsChildren,
|
|
1267
|
+
'collection:steps-staggered': buildStepsChildren,
|
|
1027
1268
|
'collection:timeline': buildTimelineChildren,
|
|
1028
1269
|
'collection:metrics-featured-media': buildMetricsChildren,
|
|
1029
1270
|
'collection:metrics-featured-copy': buildMetricsChildren,
|
|
1030
1271
|
'collection:metrics-featured-copy-split-media': buildMetricsChildren,
|
|
1272
|
+
'collection:metrics-dashboard': buildMetricsChildren,
|
|
1031
1273
|
'message:centered': buildCenteredChildren,
|
|
1032
1274
|
'message:quote': buildQuoteChildren,
|
|
1033
1275
|
'message:closing': buildClosingChildren,
|
package/example.md
CHANGED
|
@@ -306,9 +306,6 @@ variant: title-supports-bottom-plain
|
|
|
306
306
|
# Когда нужны два самостоятельных абзаца
|
|
307
307
|
|
|
308
308
|
- Этот вариант снимает приглушение с нижних опор. Каждый абзац читается как самостоятельный текст: его не воспринимают как «второстепенный к заголовку», а ставят рядом по весу.
|
|
309
|
-
- авава
|
|
310
|
-
- ава
|
|
311
|
-
авава
|
|
312
309
|
- Берите слайд, когда тезис распадается на два независимых направления: например, «что включить» и «чего избегать», «теория» и «практика», «причина» и «следствие». Третий блок не нужен — два аргумента уже задают сильную смысловую пару.
|
|
313
310
|
|
|
314
311
|
---
|
|
@@ -467,10 +464,143 @@ decor:
|
|
|
467
464
|
|
|
468
465
|
# Оранжевый тон: акцент
|
|
469
466
|
|
|
470
|
-
1.
|
|
467
|
+
1. Подсвечивайте один критический поворот
|
|
471
468
|
2. Не окрашивайте им весь ряд одинаковых фактов
|
|
472
469
|
3. Оставляйте рядом достаточно воздуха
|
|
473
470
|
|
|
471
|
+
---
|
|
472
|
+
layout: collection
|
|
473
|
+
variant: comparison
|
|
474
|
+
arrangement: before-after
|
|
475
|
+
comparison:
|
|
476
|
+
from:
|
|
477
|
+
kicker: До разбора
|
|
478
|
+
title: «Я просто запомнил формулу»
|
|
479
|
+
body: Ответ верный, но ученик не может объяснить выбор метода.
|
|
480
|
+
to:
|
|
481
|
+
kicker: После разбора
|
|
482
|
+
title: «Я выбрал формулу по условию»
|
|
483
|
+
body: Ученик связывает данные, метод и проверку результата.
|
|
484
|
+
relation:
|
|
485
|
+
label: объяснить ход мысли
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
<!-- Контракт: comparison before-after — заголовок в Markdown, две стороны и связь во frontmatter. -->
|
|
489
|
+
|
|
490
|
+
# Как обратная связь превращает ответ в решение
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
layout: collection
|
|
494
|
+
variant: comparison
|
|
495
|
+
arrangement: stable-variable
|
|
496
|
+
comparison:
|
|
497
|
+
from:
|
|
498
|
+
kicker: Сохраняем
|
|
499
|
+
title: Научиться проверять гипотезу
|
|
500
|
+
body: Результат занятия остаётся общим для любой группы.
|
|
501
|
+
to:
|
|
502
|
+
kicker: Меняем
|
|
503
|
+
title: Данные и уровень подсказок
|
|
504
|
+
body: Контекст и поддержка зависят от опыта студентов.
|
|
505
|
+
relation:
|
|
506
|
+
label: одна цель, разные маршруты
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
<!-- Контракт: comparison stable-variable — тот же shorthand, но вертикальная связь. -->
|
|
510
|
+
|
|
511
|
+
# Как адаптировать практику, сохраняя учебную цель
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
layout: collection
|
|
515
|
+
variant: steps
|
|
516
|
+
arrangement: linear
|
|
517
|
+
items:
|
|
518
|
+
- title: Назвать цель
|
|
519
|
+
body: Что студент сделает сам
|
|
520
|
+
- title: Дать контекст
|
|
521
|
+
body: Где возникает задача
|
|
522
|
+
- title: Показать пример
|
|
523
|
+
body: Один разобранный ход
|
|
524
|
+
- title: Оставить практику
|
|
525
|
+
body: Задание без подсказки
|
|
526
|
+
- title: Проверить критерий
|
|
527
|
+
body: Видимый признак качества
|
|
528
|
+
active: true
|
|
529
|
+
- title: Закрепить вывод
|
|
530
|
+
body: Что перенести дальше
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
<!-- Контракт: steps linear — ровно 6 шагов во frontmatter, один из них active. -->
|
|
534
|
+
|
|
535
|
+
# Шесть шагов учебного объяснения
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
layout: collection
|
|
539
|
+
variant: steps
|
|
540
|
+
arrangement: staggered
|
|
541
|
+
items:
|
|
542
|
+
- label: Сигнал
|
|
543
|
+
title: Заметить сбой
|
|
544
|
+
body: Зафиксировать наблюдение
|
|
545
|
+
- label: Факт
|
|
546
|
+
title: Повторить шаг
|
|
547
|
+
body: Отделить случайность
|
|
548
|
+
- label: Причина
|
|
549
|
+
title: Найти правило
|
|
550
|
+
body: Объяснить механизм
|
|
551
|
+
active: true
|
|
552
|
+
- label: Решение
|
|
553
|
+
title: Изменить подход
|
|
554
|
+
body: Проверить новый ход
|
|
555
|
+
- label: Перенос
|
|
556
|
+
title: Записать вывод
|
|
557
|
+
body: Применить в другой задаче
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
<!-- Контракт: steps staggered — ровно 5 шагов во frontmatter для ритма 3+2. -->
|
|
561
|
+
|
|
562
|
+
# Как команда разбирает ошибку
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
layout: collection
|
|
566
|
+
variant: metrics
|
|
567
|
+
arrangement: dashboard
|
|
568
|
+
metrics:
|
|
569
|
+
- value: '84%'
|
|
570
|
+
label: завершили тренажёр
|
|
571
|
+
- value: '18 мин'
|
|
572
|
+
label: медиана выполнения
|
|
573
|
+
- value: '3'
|
|
574
|
+
label: попытки до зачёта
|
|
575
|
+
- value: '12'
|
|
576
|
+
label: вопросов разобрали
|
|
577
|
+
- value: '+21 п. п.'
|
|
578
|
+
label: к входной диагностике
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
<!-- Контракт: metrics dashboard — ровно 5 метрик во frontmatter, без media и ручных spans. -->
|
|
582
|
+
|
|
583
|
+
# Что изменилось после практикума
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
layout: collection
|
|
587
|
+
variant: facts
|
|
588
|
+
arrangement: numbered-quartet
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
<!-- Контракт: facts numbered-quartet — заголовок и 4 вложенных Markdown-факта; номера добавляет тема. -->
|
|
592
|
+
|
|
593
|
+
# Четыре вопроса перед публикацией задания
|
|
594
|
+
|
|
595
|
+
- Цель
|
|
596
|
+
- Что студент сделает самостоятельно
|
|
597
|
+
- Контекст
|
|
598
|
+
- Почему задача нужна именно сейчас
|
|
599
|
+
- Критерий
|
|
600
|
+
- Как выглядит проверяемый результат
|
|
601
|
+
- Обратная связь
|
|
602
|
+
- Что поможет улучшить следующую попытку
|
|
603
|
+
|
|
474
604
|
---
|
|
475
605
|
layout: collection
|
|
476
606
|
variant: metrics
|
|
@@ -717,7 +847,7 @@ header: default
|
|
|
717
847
|
</Slot>
|
|
718
848
|
|
|
719
849
|
<Slot area="6 / 1 / -1 / 7" surface="color" tone="orange" margin="4" gap="2">
|
|
720
|
-
<Text size="5"
|
|
850
|
+
<Text size="5">Акцентная поверхность</Text>
|
|
721
851
|
<Text size="3">Слот целиком красится в текущий акцентный цвет — для смыслового выделения.</Text>
|
|
722
852
|
</Slot>
|
|
723
853
|
|
package/package.json
CHANGED
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
|
|
11
11
|
const PROJECT_ROOT = resolve(import.meta.dirname, '..')
|
|
12
12
|
const DIST_DIR = resolve(PROJECT_ROOT, 'dist')
|
|
13
|
-
const REPRESENTATIVE_SLIDES = [1, 9, 15, 20,
|
|
13
|
+
const REPRESENTATIVE_SLIDES = [1, 9, 15, 20, 38, 46, 47]
|
|
14
14
|
const VISUAL_ONLY_SLIDES = [17, 18]
|
|
15
|
+
const P0_REGRESSION_SLIDES = [23, 24, 25, 26, 27, 28]
|
|
15
16
|
const LIFECYCLE_EVENT = 'practicum:slide-lifecycle'
|
|
16
17
|
const LIFECYCLE_STATE_KEY = '__practicumBrowserLifecycle'
|
|
17
18
|
const THEME_MEDIA = [
|
|
@@ -168,6 +169,88 @@ async function assertClosingLayout(slide, slideNumber, expectedTextCount) {
|
|
|
168
169
|
)
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
async function assertStaggeredStepsLayout(slide, slideNumber) {
|
|
173
|
+
const grid = slide.locator('.Slide-StepsGrid_staggered')
|
|
174
|
+
const cards = grid.locator(':scope > .Slide-Step')
|
|
175
|
+
const cardCount = await cards.count()
|
|
176
|
+
|
|
177
|
+
assert.equal(cardCount, 5, `слайд ${slideNumber}: staggered должен содержать ровно 5 карточек`)
|
|
178
|
+
|
|
179
|
+
const [gridBox, cardBoxes, placements] = await Promise.all([
|
|
180
|
+
grid.boundingBox(),
|
|
181
|
+
Promise.all(Array.from({ length: cardCount }, (_, index) => cards.nth(index).boundingBox())),
|
|
182
|
+
cards.evaluateAll(elements => elements.map((element) => {
|
|
183
|
+
const style = getComputedStyle(element)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
columnEnd: style.gridColumnEnd,
|
|
187
|
+
columnStart: style.gridColumnStart,
|
|
188
|
+
rowEnd: style.gridRowEnd,
|
|
189
|
+
rowStart: style.gridRowStart,
|
|
190
|
+
}
|
|
191
|
+
})),
|
|
192
|
+
])
|
|
193
|
+
|
|
194
|
+
assert.ok(gridBox, `слайд ${slideNumber}: не найдена геометрия staggered grid`)
|
|
195
|
+
assert.ok(cardBoxes.every(Boolean), `слайд ${slideNumber}: не найдена геометрия всех staggered-карточек`)
|
|
196
|
+
assert.deepEqual(placements, [
|
|
197
|
+
{ columnEnd: '3', columnStart: '1', rowEnd: '2', rowStart: '1' },
|
|
198
|
+
{ columnEnd: '5', columnStart: '3', rowEnd: '2', rowStart: '1' },
|
|
199
|
+
{ columnEnd: '7', columnStart: '5', rowEnd: '2', rowStart: '1' },
|
|
200
|
+
{ columnEnd: '4', columnStart: '2', rowEnd: '3', rowStart: '2' },
|
|
201
|
+
{ columnEnd: '6', columnStart: '4', rowEnd: '3', rowStart: '2' },
|
|
202
|
+
], `слайд ${slideNumber}: staggered должен использовать центрированную геометрию 3+2`)
|
|
203
|
+
|
|
204
|
+
const boxes = cardBoxes
|
|
205
|
+
const topY = boxes[0].y
|
|
206
|
+
const bottomY = boxes[3].y
|
|
207
|
+
|
|
208
|
+
assert.ok(
|
|
209
|
+
boxes.slice(0, 3).every(box => Math.abs(box.y - topY) <= 1),
|
|
210
|
+
`слайд ${slideNumber}: первые три staggered-карточки должны быть в верхнем ряду`,
|
|
211
|
+
)
|
|
212
|
+
assert.ok(
|
|
213
|
+
boxes.slice(3).every(box => Math.abs(box.y - bottomY) <= 1) && bottomY > topY,
|
|
214
|
+
`слайд ${slideNumber}: последние две staggered-карточки должны быть в нижнем ряду`,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const lowerCenter = (boxes[3].x + boxes[4].x + boxes[4].width) / 2
|
|
218
|
+
const gridCenter = gridBox.x + gridBox.width / 2
|
|
219
|
+
|
|
220
|
+
assert.ok(
|
|
221
|
+
Math.abs(lowerCenter - gridCenter) <= 1,
|
|
222
|
+
`слайд ${slideNumber}: нижняя пара staggered не центрирована`,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function assertNumberedQuartet(slide, slideNumber) {
|
|
227
|
+
const markers = slide.locator('.Text_size_4')
|
|
228
|
+
assert.deepEqual(
|
|
229
|
+
(await markers.allTextContents()).map(text => text.trim()),
|
|
230
|
+
['1', 'Цель', '2', 'Контекст', '3', 'Критерий', '4', 'Обратная связь'],
|
|
231
|
+
`слайд ${slideNumber}: номер и название каждого факта должны быть plain Text размера 4`,
|
|
232
|
+
)
|
|
233
|
+
assert.equal(
|
|
234
|
+
await slide.locator('.Slide-FactNumber').count(),
|
|
235
|
+
0,
|
|
236
|
+
`слайд ${slideNumber}: номера не должны оформляться кружками`,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
const factTitles = slide.locator('.Text_size_4.Text_align_end')
|
|
240
|
+
assert.deepEqual(
|
|
241
|
+
(await factTitles.allTextContents()).map(text => text.trim()),
|
|
242
|
+
['Цель', 'Контекст', 'Критерий', 'Обратная связь'],
|
|
243
|
+
`слайд ${slideNumber}: названия фактов должны быть прижаты к нижнему краю карточки`,
|
|
244
|
+
)
|
|
245
|
+
assert.ok(
|
|
246
|
+
(await factTitles.evaluateAll(elements => elements.every((element) => {
|
|
247
|
+
const marginTop = Number.parseFloat(getComputedStyle(element).marginTop)
|
|
248
|
+
return Number.isFinite(marginTop) && marginTop > 0
|
|
249
|
+
}))),
|
|
250
|
+
`слайд ${slideNumber}: названия фактов должны быть отодвинуты вниз от номера`,
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
171
254
|
async function inspectSlide(
|
|
172
255
|
page,
|
|
173
256
|
origin,
|
|
@@ -222,12 +305,16 @@ async function inspectSlide(
|
|
|
222
305
|
result.heightOverflow <= 1,
|
|
223
306
|
`слайд ${slideNumber}: вертикальное переполнение ${result.heightOverflow}px`,
|
|
224
307
|
)
|
|
225
|
-
if (slideNumber ===
|
|
308
|
+
if (slideNumber === 38)
|
|
226
309
|
await assertStrongEmphasis(slide, slideNumber)
|
|
227
|
-
if (slideNumber ===
|
|
310
|
+
if (slideNumber === 46)
|
|
228
311
|
await assertClosingLayout(slide, slideNumber, 1)
|
|
229
|
-
if (slideNumber ===
|
|
312
|
+
if (slideNumber === 47)
|
|
230
313
|
await assertClosingLayout(slide, slideNumber, 2)
|
|
314
|
+
if (slideNumber === 26)
|
|
315
|
+
await assertStaggeredStepsLayout(slide, slideNumber)
|
|
316
|
+
if (slideNumber === 28)
|
|
317
|
+
await assertNumberedQuartet(slide, slideNumber)
|
|
231
318
|
assertLifecycleTransition(
|
|
232
319
|
beforeLifecycle,
|
|
233
320
|
afterLifecycle,
|
|
@@ -400,6 +487,17 @@ export async function runBrowserSmoke() {
|
|
|
400
487
|
)
|
|
401
488
|
}
|
|
402
489
|
|
|
490
|
+
for (const slideNumber of P0_REGRESSION_SLIDES) {
|
|
491
|
+
activeSlide = `P0-регрессия: слайд ${slideNumber}`
|
|
492
|
+
await inspectSlide(
|
|
493
|
+
page,
|
|
494
|
+
server.origin,
|
|
495
|
+
slideNumber,
|
|
496
|
+
undefined,
|
|
497
|
+
() => inFlightTracker.waitForIdle(),
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
403
501
|
for (const asset of THEME_MEDIA) {
|
|
404
502
|
activeSlide = `медиа: ${asset.label}`
|
|
405
503
|
await loadThemeImage(page, server.origin, asset)
|
|
@@ -413,7 +511,7 @@ export async function runBrowserSmoke() {
|
|
|
413
511
|
assertNoDiagnostics('неуспешные запросы того же источника', failedRequests)
|
|
414
512
|
|
|
415
513
|
console.log(
|
|
416
|
-
`Браузерная проверка: слайды=${REPRESENTATIVE_SLIDES.length + VISUAL_ONLY_SLIDES.length}, `
|
|
514
|
+
`Браузерная проверка: слайды=${REPRESENTATIVE_SLIDES.length + VISUAL_ONLY_SLIDES.length + P0_REGRESSION_SLIDES.length}, `
|
|
417
515
|
+ `ошибкиСтраницы=${pageErrors.length}, ошибкиКонсоли=${consoleErrors.length}, `
|
|
418
516
|
+ `ошибочныеОтветы=${failedResponses.length}, активныеЗапросы=${inFlightTracker.size}, `
|
|
419
517
|
+ `переполнение=0, медиа=${THEME_MEDIA.length}, переходыЖЦ=${REPRESENTATIVE_SLIDES.length - 1}`,
|