slidev-theme-practicum 0.1.2 → 0.1.4
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 +1 -1
- package/components/Slot.vue +3 -53
- package/components/TextFitGroup.vue +71 -0
- package/components/Timeline.vue +39 -15
- package/composables/decor-catalog.mjs +0 -1
- package/composables/layout-shorthands.ts +1 -1
- package/composables/text-fit-runtime.ts +1 -1
- package/composables/timeline-grid.ts +4 -0
- package/package.json +1 -1
- package/scripts/browser-smoke.mjs +13 -1
package/README.md
CHANGED
|
@@ -148,7 +148,7 @@ layout: cover | message | explainer | collection | none
|
|
|
148
148
|
| `decor` | объект | нет | семантический декоративный слой |
|
|
149
149
|
| `background` | объект или строка изображения | нет | явно заданное фоновое изображение |
|
|
150
150
|
|
|
151
|
-
`Slot` согласует размер до трёх прямых `Text`-детей с диапазонами `size` и `priority`. Несколько `Slot` с одинаковым `fit-group` получают общий размерный вектор: берётся максимальный вариант, который помещается во все ячейки группы. Атрибуты потока принадлежат дочерним элементам: `Text align="end"` прибивает строку внутри потока. Отдельный flow-атрибут есть у `Person`, когда блок автора используется как прямой ребёнок `Slot`.
|
|
151
|
+
`Slot` согласует размер до трёх прямых `Text`-детей с диапазонами `size` и `priority`. Несколько `Slot` с одинаковым `fit-group` получают общий размерный вектор: берётся максимальный вариант, который помещается во все ячейки группы. Если диапазоны одноимённых позиций не пересекаются, слоты сохраняют независимую локальную подгонку вместо ошибки рендера. Атрибуты потока принадлежат дочерним элементам: `Text align="end"` прибивает строку внутри потока. Отдельный flow-атрибут есть у `Person`, когда блок автора используется как прямой ребёнок `Slot`.
|
|
152
152
|
|
|
153
153
|
## Text API
|
|
154
154
|
|
package/components/Slot.vue
CHANGED
|
@@ -2,18 +2,13 @@
|
|
|
2
2
|
import { useSlideContext } from '@slidev/client'
|
|
3
3
|
import {
|
|
4
4
|
computed,
|
|
5
|
-
defineComponent,
|
|
6
5
|
getCurrentInstance,
|
|
7
|
-
h,
|
|
8
|
-
isVNode,
|
|
9
6
|
onBeforeUnmount,
|
|
10
7
|
shallowRef,
|
|
11
8
|
watchEffect,
|
|
12
|
-
type CSSProperties,
|
|
13
|
-
type PropType,
|
|
14
|
-
type VNode,
|
|
15
9
|
} from 'vue'
|
|
16
10
|
import ImageRenderer from './ImageRenderer.vue'
|
|
11
|
+
import TextFitGroup from './TextFitGroup.vue'
|
|
17
12
|
import { createThemeMedia } from '../composables/theme-media.mjs'
|
|
18
13
|
import type { ThemeLayoutSlotSpec } from '../composables/layout-recipes'
|
|
19
14
|
import { useSlotPlacementSession, type ResolvedSlotPlacement } from '../composables/slot-placement'
|
|
@@ -23,8 +18,6 @@ import {
|
|
|
23
18
|
type SlotGapInput,
|
|
24
19
|
type SlotMarginInput,
|
|
25
20
|
} from '../composables/slot-spacing'
|
|
26
|
-
import { useTextFitScope } from '../composables/text-fit-scope'
|
|
27
|
-
import { createTextFitRuntime } from '../composables/text-fit-runtime'
|
|
28
21
|
import { useThemeConfig } from '../composables/use-theme-config'
|
|
29
22
|
import { resolveSlotTheme, themeVars, type ThemeSlotSurface } from '../composables/theme-foundation'
|
|
30
23
|
import { camelToKebab } from '../composables/layout-vnode'
|
|
@@ -65,49 +58,6 @@ const layoutOverrideKeys = [
|
|
|
65
58
|
'centered',
|
|
66
59
|
] as const satisfies readonly (keyof ThemeLayoutSlotSpec)[]
|
|
67
60
|
|
|
68
|
-
const textFit = createTextFitRuntime()
|
|
69
|
-
|
|
70
|
-
const SlotContent = defineComponent({
|
|
71
|
-
name: 'SlotContent',
|
|
72
|
-
props: {
|
|
73
|
-
contentStyle: {
|
|
74
|
-
type: Object as PropType<CSSProperties>,
|
|
75
|
-
required: true,
|
|
76
|
-
},
|
|
77
|
-
fitGroup: {
|
|
78
|
-
type: String,
|
|
79
|
-
default: '',
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
setup(contentProps, { slots }) {
|
|
83
|
-
const contentElement = shallowRef<HTMLElement | null>(null)
|
|
84
|
-
const textFitScope = useTextFitScope()
|
|
85
|
-
const sharedKey = computed(() => {
|
|
86
|
-
if (!contentProps.fitGroup)
|
|
87
|
-
return ''
|
|
88
|
-
|
|
89
|
-
return `${textFitScope}:${contentProps.fitGroup}`
|
|
90
|
-
})
|
|
91
|
-
let currentChildren: VNode[] = []
|
|
92
|
-
const { apply } = textFit.useGroup({
|
|
93
|
-
target: contentElement,
|
|
94
|
-
nodes: () => currentChildren,
|
|
95
|
-
sharedKey,
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
return () => {
|
|
99
|
-
const children = (slots.default?.() ?? []).filter(isVNode)
|
|
100
|
-
currentChildren = children
|
|
101
|
-
|
|
102
|
-
return h('div', {
|
|
103
|
-
ref: contentElement,
|
|
104
|
-
class: 'Slot-Content',
|
|
105
|
-
style: contentProps.contentStyle,
|
|
106
|
-
}, apply(children))
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
})
|
|
110
|
-
|
|
111
61
|
const props = withDefaults(defineProps<{
|
|
112
62
|
as?: string
|
|
113
63
|
role?: SlotRole
|
|
@@ -318,9 +268,9 @@ const surfaceClass = computed(() => {
|
|
|
318
268
|
class="Slot-BackgroundLayer"
|
|
319
269
|
:layer="backgroundLayer" />
|
|
320
270
|
</div>
|
|
321
|
-
<
|
|
271
|
+
<TextFitGroup class="Slot-Content" :style="contentStyle" :fit-group="props.fitGroup">
|
|
322
272
|
<slot />
|
|
323
|
-
</
|
|
273
|
+
</TextFitGroup>
|
|
324
274
|
</component>
|
|
325
275
|
</template>
|
|
326
276
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
isVNode,
|
|
7
|
+
shallowRef,
|
|
8
|
+
type PropType,
|
|
9
|
+
type VNode,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
import { createTextFitRuntime } from '../composables/text-fit-runtime'
|
|
12
|
+
import { useTextFitScope } from '../composables/text-fit-scope'
|
|
13
|
+
|
|
14
|
+
const textFit = createTextFitRuntime()
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
fitGroup: string
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const TextFitGroupContent = defineComponent({
|
|
21
|
+
name: 'TextFitGroupContent',
|
|
22
|
+
props: {
|
|
23
|
+
fitGroup: {
|
|
24
|
+
type: String as PropType<string>,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
setup(props, { slots }) {
|
|
29
|
+
const groupElement = shallowRef<HTMLElement | null>(null)
|
|
30
|
+
const textFitScope = useTextFitScope()
|
|
31
|
+
const sharedKey = computed(() => {
|
|
32
|
+
if (!props.fitGroup)
|
|
33
|
+
return ''
|
|
34
|
+
|
|
35
|
+
return `${textFitScope}:${props.fitGroup}`
|
|
36
|
+
})
|
|
37
|
+
let currentChildren: VNode[] = []
|
|
38
|
+
const { apply } = textFit.useGroup({
|
|
39
|
+
target: groupElement,
|
|
40
|
+
nodes: () => currentChildren,
|
|
41
|
+
sharedKey,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
const children = (slots.default?.() ?? []).filter(isVNode)
|
|
46
|
+
currentChildren = children
|
|
47
|
+
|
|
48
|
+
return h('div', {
|
|
49
|
+
ref: groupElement,
|
|
50
|
+
class: 'TextFitGroup',
|
|
51
|
+
}, apply(children))
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<TextFitGroupContent :fit-group="props.fitGroup">
|
|
59
|
+
<slot />
|
|
60
|
+
</TextFitGroupContent>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
.TextFitGroup {
|
|
65
|
+
min-width: 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: inherit;
|
|
70
|
+
}
|
|
71
|
+
</style>
|
package/components/Timeline.vue
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, shallowRef, type ComponentPublicInstance } from 'vue'
|
|
3
|
-
import { resolveTimelineItemGridStyle } from '../composables/timeline-grid'
|
|
2
|
+
import { computed, getCurrentInstance, shallowRef, type ComponentPublicInstance } from 'vue'
|
|
3
|
+
import { resolveTimelineItemGridStyle, resolveTimelineLabelSpan } from '../composables/timeline-grid'
|
|
4
4
|
import { createTextFitRuntime, type ThemeTextSize } from '../composables/text-fit-runtime'
|
|
5
|
+
import Text from './Text.vue'
|
|
6
|
+
import TextFitGroup from './TextFitGroup.vue'
|
|
5
7
|
|
|
6
8
|
const TIMELINE_ACTIVE_YEAR_FALLBACK_SIZE: ThemeTextSize = 9
|
|
9
|
+
const timelineFitGroup = `timeline-${getCurrentInstance()?.uid ?? 'default'}`
|
|
7
10
|
|
|
8
11
|
const props = defineProps({
|
|
9
12
|
items: {
|
|
@@ -16,6 +19,7 @@ const timelineElement = shallowRef<HTMLElement | null>(null)
|
|
|
16
19
|
const activeYearElement = shallowRef<HTMLElement | null>(null)
|
|
17
20
|
const hasActiveItem = computed(() => props.items.some(item => item.active))
|
|
18
21
|
const activeIndices = computed(() => props.items.flatMap((item, itemIndex) => item.active ? [itemIndex] : []))
|
|
22
|
+
const labelSpan = computed(() => resolveTimelineLabelSpan(props.items))
|
|
19
23
|
const TIMELINE_ACTIVE_YEAR_MAX_SIZE = computed<ThemeTextSize>(() => 12)
|
|
20
24
|
const textFit = createTextFitRuntime()
|
|
21
25
|
|
|
@@ -64,12 +68,15 @@ const { size: fittedActiveYearSize } = textFit.useElement({
|
|
|
64
68
|
],
|
|
65
69
|
})
|
|
66
70
|
|
|
67
|
-
const timelineStyle = computed(() =>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const timelineStyle = computed(() => ({
|
|
72
|
+
'--theme-timeline-label-width': `var(--theme-grid-span-${labelSpan.value}-width)`,
|
|
73
|
+
...(hasActiveItem.value
|
|
74
|
+
? {
|
|
75
|
+
'--theme-timeline-active-year-size': `var(--theme-text-size-${fittedActiveYearSize.value})`,
|
|
76
|
+
'--theme-timeline-active-year-line': `var(--theme-text-line-${fittedActiveYearSize.value})`,
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
}))
|
|
73
80
|
|
|
74
81
|
function getItemGridStyle(index: number) {
|
|
75
82
|
assertSingleActiveItem()
|
|
@@ -89,9 +96,21 @@ function getItemGridStyle(index: number) {
|
|
|
89
96
|
class="Timeline-Item"
|
|
90
97
|
:style="getItemGridStyle(index)"
|
|
91
98
|
:class="{ 'Timeline-Item_active': item.active }">
|
|
92
|
-
<div :ref="element => setYearElement(element, item.active)" class="Timeline-Year">
|
|
99
|
+
<div :ref="element => setYearElement(element, item.active)" class="Timeline-Year">
|
|
100
|
+
<span v-if="item.active">{{ item.year }}</span>
|
|
101
|
+
<TextFitGroup v-else
|
|
102
|
+
class="Timeline-YearFitGroup"
|
|
103
|
+
:fit-group="`${timelineFitGroup}-years`">
|
|
104
|
+
<Text size="4-7">{{ item.year }}</Text>
|
|
105
|
+
</TextFitGroup>
|
|
106
|
+
</div>
|
|
93
107
|
<div class="Timeline-Dot" />
|
|
94
|
-
<div class="Timeline-Label">
|
|
108
|
+
<div class="Timeline-Label">
|
|
109
|
+
<Text v-if="item.active" size="2-3">{{ item.label }}</Text>
|
|
110
|
+
<TextFitGroup v-else :fit-group="`${timelineFitGroup}-labels`">
|
|
111
|
+
<Text size="2-3">{{ item.label }}</Text>
|
|
112
|
+
</TextFitGroup>
|
|
113
|
+
</div>
|
|
95
114
|
</div>
|
|
96
115
|
</div>
|
|
97
116
|
</template>
|
|
@@ -102,6 +121,7 @@ function getItemGridStyle(index: number) {
|
|
|
102
121
|
--timeline-dot-size: calc(var(--theme-grid-module) * 4);
|
|
103
122
|
--theme-timeline-active-year-size: var(--theme-text-size-9);
|
|
104
123
|
--theme-timeline-active-year-line: var(--theme-text-line-9);
|
|
124
|
+
--theme-timeline-label-width: var(--theme-grid-span-3-width);
|
|
105
125
|
position: relative;
|
|
106
126
|
display: grid;
|
|
107
127
|
grid-template-columns: repeat(var(--theme-grid-columns), minmax(0, 1fr));
|
|
@@ -122,6 +142,7 @@ function getItemGridStyle(index: number) {
|
|
|
122
142
|
|
|
123
143
|
.Timeline-Item {
|
|
124
144
|
position: relative;
|
|
145
|
+
min-width: 0;
|
|
125
146
|
display: grid;
|
|
126
147
|
grid-template-rows:
|
|
127
148
|
var(--timeline-block-height)
|
|
@@ -131,14 +152,18 @@ function getItemGridStyle(index: number) {
|
|
|
131
152
|
}
|
|
132
153
|
|
|
133
154
|
.Timeline-Year {
|
|
155
|
+
min-width: 0;
|
|
134
156
|
min-height: var(--timeline-block-height);
|
|
157
|
+
padding-right: var(--theme-grid-gap);
|
|
135
158
|
display: flex;
|
|
136
159
|
align-items: flex-end;
|
|
137
|
-
font-size: var(--theme-text-size-7);
|
|
138
|
-
line-height: var(--theme-text-line-7);
|
|
139
160
|
color: var(--theme-text);
|
|
140
161
|
}
|
|
141
162
|
|
|
163
|
+
.Timeline-YearFitGroup {
|
|
164
|
+
padding-bottom: var(--theme-grid-module);
|
|
165
|
+
}
|
|
166
|
+
|
|
142
167
|
.Timeline-Dot {
|
|
143
168
|
width: var(--timeline-dot-size);
|
|
144
169
|
height: var(--timeline-dot-size);
|
|
@@ -150,11 +175,10 @@ function getItemGridStyle(index: number) {
|
|
|
150
175
|
|
|
151
176
|
.Timeline-Label {
|
|
152
177
|
min-height: var(--timeline-block-height);
|
|
153
|
-
|
|
178
|
+
width: min(100%, var(--theme-timeline-label-width));
|
|
179
|
+
padding-right: var(--theme-grid-gap);
|
|
154
180
|
padding-top: var(--theme-slot-margin-3);
|
|
155
181
|
color: var(--theme-text-muted);
|
|
156
|
-
font-size: var(--theme-text-size-2);
|
|
157
|
-
line-height: var(--theme-text-line-2);
|
|
158
182
|
}
|
|
159
183
|
|
|
160
184
|
.Timeline_has_active .Timeline-Year {
|
|
@@ -972,7 +972,7 @@ function buildFactsChildren(children: VNode[], context: LayoutShorthandContext)
|
|
|
972
972
|
const footnoteText = footnote ? readInlineText(footnote) : ''
|
|
973
973
|
const rowFactSize: ThemeTextSizeInput = isQuartet ? '5-8' : isDuo ? '5-10' : '5-9'
|
|
974
974
|
const stackedFactSize: ThemeTextSizeInput = '5-9'
|
|
975
|
-
const stackedLabelSize: ThemeTextSizeInput = '
|
|
975
|
+
const stackedLabelSize: ThemeTextSizeInput = '3-5'
|
|
976
976
|
const rowLabelSize: ThemeTextSizeInput = '2-4'
|
|
977
977
|
const featuredFactSize: ThemeTextSizeInput = isFeatured ? '9-12' : isStacked ? stackedFactSize : rowFactSize
|
|
978
978
|
const supportFactSize: ThemeTextSizeInput = isFeatured ? '5-7' : isStacked ? stackedFactSize : rowFactSize
|
|
@@ -366,7 +366,7 @@ function combineSlotTextFitGroups(groups: SlotTextFitGroup[]) {
|
|
|
366
366
|
})
|
|
367
367
|
|
|
368
368
|
if (items.some(item => item.minSize > item.maxSize))
|
|
369
|
-
|
|
369
|
+
return null
|
|
370
370
|
|
|
371
371
|
return {
|
|
372
372
|
initialSizes: firstTextFitCandidate(items),
|
|
@@ -2,6 +2,10 @@ export interface TimelineGridItem {
|
|
|
2
2
|
active?: boolean
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function resolveTimelineLabelSpan(items: readonly TimelineGridItem[]): 3 | 5 {
|
|
6
|
+
return items.length <= 2 ? 5 : 3
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
export function resolveTimelineItemGridStyle(
|
|
6
10
|
items: readonly TimelineGridItem[],
|
|
7
11
|
index: number,
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
const PROJECT_ROOT = resolve(import.meta.dirname, '..')
|
|
12
12
|
const DIST_DIR = resolve(PROJECT_ROOT, 'dist')
|
|
13
13
|
const REPRESENTATIVE_SLIDES = [1, 9, 15, 20, 32, 40, 41]
|
|
14
|
+
const VISUAL_ONLY_SLIDES = [17, 18]
|
|
14
15
|
const LIFECYCLE_EVENT = 'practicum:slide-lifecycle'
|
|
15
16
|
const LIFECYCLE_STATE_KEY = '__practicumBrowserLifecycle'
|
|
16
17
|
const THEME_MEDIA = [
|
|
@@ -388,6 +389,17 @@ export async function runBrowserSmoke() {
|
|
|
388
389
|
)
|
|
389
390
|
}
|
|
390
391
|
|
|
392
|
+
for (const slideNumber of VISUAL_ONLY_SLIDES) {
|
|
393
|
+
activeSlide = `слайд ${slideNumber}`
|
|
394
|
+
await inspectSlide(
|
|
395
|
+
page,
|
|
396
|
+
server.origin,
|
|
397
|
+
slideNumber,
|
|
398
|
+
undefined,
|
|
399
|
+
() => inFlightTracker.waitForIdle(),
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
391
403
|
for (const asset of THEME_MEDIA) {
|
|
392
404
|
activeSlide = `медиа: ${asset.label}`
|
|
393
405
|
await loadThemeImage(page, server.origin, asset)
|
|
@@ -401,7 +413,7 @@ export async function runBrowserSmoke() {
|
|
|
401
413
|
assertNoDiagnostics('неуспешные запросы того же источника', failedRequests)
|
|
402
414
|
|
|
403
415
|
console.log(
|
|
404
|
-
`Браузерная проверка: слайды=${REPRESENTATIVE_SLIDES.length}, `
|
|
416
|
+
`Браузерная проверка: слайды=${REPRESENTATIVE_SLIDES.length + VISUAL_ONLY_SLIDES.length}, `
|
|
405
417
|
+ `ошибкиСтраницы=${pageErrors.length}, ошибкиКонсоли=${consoleErrors.length}, `
|
|
406
418
|
+ `ошибочныеОтветы=${failedResponses.length}, активныеЗапросы=${inFlightTracker.size}, `
|
|
407
419
|
+ `переполнение=0, медиа=${THEME_MEDIA.length}, переходыЖЦ=${REPRESENTATIVE_SLIDES.length - 1}`,
|