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 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
 
@@ -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
- <SlotContent :content-style="contentStyle" :fit-group="props.fitGroup">
271
+ <TextFitGroup class="Slot-Content" :style="contentStyle" :fit-group="props.fitGroup">
322
272
  <slot />
323
- </SlotContent>
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>
@@ -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(() => hasActiveItem.value
68
- ? {
69
- '--theme-timeline-active-year-size': `var(--theme-text-size-${fittedActiveYearSize.value})`,
70
- '--theme-timeline-active-year-line': `var(--theme-text-line-${fittedActiveYearSize.value})`,
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">{{ item.year }}</div>
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">{{ item.label }}</div>
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
- max-width: var(--theme-grid-span-2-width);
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 {
@@ -159,7 +159,6 @@ export const DEFAULT_DECOR_DATA = Object.freeze([
159
159
  x: '50%',
160
160
  y: '12%',
161
161
  zoom: 3,
162
- color: 'var(--theme-color-dark-0)',
163
162
  opacity: 1,
164
163
  },
165
164
  },
@@ -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 = '2-4'
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
- throw new Error('[Text] shared size ranges cannot be coordinated because their ranges do not overlap.')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-theme-practicum",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Strict Slidev presentation theme with a 12x12 grid, editorial layouts, and low-level slot composition API",
5
5
  "keywords": [
6
6
  "slidev-theme",
@@ -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}`,