slidev-theme-practicum 0.1.0 → 0.1.3
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/LICENSE +21 -0
- package/README.md +72 -10
- package/components/Slide.vue +81 -26
- package/components/Slot.vue +20 -85
- package/components/Text.vue +68 -2
- package/components/TextFitGroup.vue +71 -0
- package/components/Timeline.vue +40 -37
- package/composables/deck-slot-markup.cjs +249 -0
- package/composables/decor-catalog.mjs +169 -47
- package/composables/layout-authoring.ts +6 -2
- package/composables/layout-markdown.mjs +33 -0
- package/composables/layout-recipes.ts +51 -5
- package/composables/layout-shorthands.ts +251 -81
- package/composables/markdown-to-vnodes.cjs +1 -0
- package/composables/markdown-to-vnodes.mjs +237 -0
- package/composables/slot-placement.ts +1 -1
- package/composables/slot-spacing.ts +66 -0
- package/composables/text-fit-runtime.ts +458 -76
- package/composables/text-fit-scope.ts +13 -0
- package/composables/text-slot-markdown.mjs +193 -0
- package/composables/text-typography.mjs +2 -0
- package/composables/theme-assets.mjs +60 -0
- package/composables/timeline-grid.ts +31 -0
- package/composables/typescript-require.cjs +29 -0
- package/composables/validate-deck-layouts.cjs +277 -0
- package/decor-library.md +2 -0
- package/example.md +132 -10
- package/package.json +25 -11
- package/public/photos/photo-1.webp +0 -0
- package/public/photos/photo-10.webp +0 -0
- package/public/photos/photo-11.webp +0 -0
- package/public/photos/photo-12.webp +0 -0
- package/public/photos/photo-13.webp +0 -0
- package/public/photos/photo-14.webp +0 -0
- package/public/photos/photo-15.webp +0 -0
- package/public/photos/photo-16.webp +0 -0
- package/public/photos/photo-17.webp +0 -0
- package/public/photos/photo-18.webp +0 -0
- package/public/photos/photo-19.webp +0 -0
- package/public/photos/photo-2.webp +0 -0
- package/public/photos/photo-20.webp +0 -0
- package/public/photos/photo-21.webp +0 -0
- package/public/photos/photo-22.webp +0 -0
- package/public/photos/photo-23.webp +0 -0
- package/public/photos/photo-24.webp +0 -0
- package/public/photos/photo-25.webp +0 -0
- package/public/photos/photo-26.webp +0 -0
- package/public/photos/photo-27.webp +0 -0
- package/public/photos/photo-28.webp +0 -0
- package/public/photos/photo-29.webp +0 -0
- package/public/photos/photo-3.webp +0 -0
- package/public/photos/photo-30.webp +0 -0
- package/public/photos/photo-4.webp +0 -0
- package/public/photos/photo-5.webp +0 -0
- package/public/photos/photo-6.webp +0 -0
- package/public/photos/photo-7.webp +0 -0
- package/public/photos/photo-8.webp +0 -0
- package/public/photos/photo-9.webp +0 -0
- package/scripts/browser-smoke-runtime.mjs +428 -0
- package/scripts/browser-smoke.mjs +436 -0
- package/scripts/build-artifact-assets.mjs +55 -0
- package/scripts/check-build-artifact.mjs +243 -0
- package/scripts/check-package.mjs +132 -0
- package/scripts/check-test-architecture.mjs +176 -0
- package/scripts/npm-spawn.mjs +31 -0
- package/scripts/test-architecture-policy.mjs +245 -0
- package/scripts/theme-asset-inventory.mjs +83 -0
- package/scripts/validate-deck.cjs +18 -0
- package/setup/decor-save-middleware.ts +244 -0
- package/setup/vite-plugins.ts +14 -84
- package/skills/slidev-practicum/SKILL.md +25 -0
- package/skills/slidev-practicum/agents/openai.yaml +4 -0
- package/styles/index.css +10 -1
- package/composables/layout-registry.ts +0 -47
- package/env.d.ts +0 -6
- package/public/photos/photo-1.png +0 -0
- package/public/photos/photo-10.png +0 -0
- package/public/photos/photo-2.png +0 -0
- package/public/photos/photo-3.png +0 -0
- package/public/photos/photo-4.png +0 -0
- package/public/photos/photo-5.png +0 -0
- package/public/photos/photo-6.png +0 -0
- package/public/photos/photo-7.png +0 -0
- package/public/photos/photo-8.png +0 -0
- package/public/photos/photo-9.png +0 -0
- package/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +0 -102
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 veged
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -19,8 +19,23 @@
|
|
|
19
19
|
```bash
|
|
20
20
|
npm install
|
|
21
21
|
npm run dev
|
|
22
|
+
npm run validate-deck example.md
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
Проверка markdown-контрактов своей колоды (после `npm install slidev-theme-practicum`):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm exec -- slidev-practicum-validate path/to/slides.md
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
В `package.json` колоды:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"validate:slides": "npm exec -- slidev-practicum-validate slides.md"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
В `example.md` у канонических слайдов в заметках автора (`<!-- Контракт … -->`) описано, сколько пунктов и какой вложенности ждёт каждый variant.
|
|
38
|
+
|
|
24
39
|
Для использования в другой Slidev-презентации:
|
|
25
40
|
|
|
26
41
|
```bash
|
|
@@ -85,6 +100,14 @@ layout: cover | message | explainer | collection | none
|
|
|
85
100
|
|
|
86
101
|
Для канонических макетов внутри `layout: cover|message|explainer|collection` используйте `Slot role="..."`; внутри layout-слайда не задавайте `area`, `col` и `row`. Для ручного режима используйте `Slot area`, `Slot col` или `Slot row`.
|
|
87
102
|
|
|
103
|
+
### Markdown shorthand
|
|
104
|
+
|
|
105
|
+
Для `layout: message`, `explainer` и `collection` часть `variant` принимает markdown в default slot слайда (`#` заголовок, списки, blockquote, frontmatter) — тема разворачивает его в `Slot` / `Text`. Реестр: `composables/layout-shorthands.ts`.
|
|
106
|
+
|
|
107
|
+
Для `message:closing` обязателен один заголовок `# …`; под ним можно добавить один необязательный абзац. Дополнительные абзацы, списки и изображения не поддерживаются.
|
|
108
|
+
|
|
109
|
+
`layout: cover` markdown shorthand **не** имеет: для всех `cover:*` нужны явные `<Slot role="...">` и `<Text>` (см. обложки в [example.md](example.md)).
|
|
110
|
+
|
|
88
111
|
| Слой | Для чего |
|
|
89
112
|
| ------------- | ------------------------------------------------ |
|
|
90
113
|
| `layout` | семантика слайда на уровне Slidev |
|
|
@@ -120,11 +143,12 @@ layout: cover | message | explainer | collection | none
|
|
|
120
143
|
| `margin` | `1..8` | нет | общий внутренний отступ |
|
|
121
144
|
| `margin-top`, `margin-right`, `margin-bottom`, `margin-left` | `1..8` | `margin` | отступ отдельной стороны |
|
|
122
145
|
| `gap` | `0..7` | `0` | расстояние между прямыми дочерними элементами |
|
|
146
|
+
| `fit-group` | строка | нет | общий подбор размеров для нескольких слотов |
|
|
123
147
|
| `centered` | boolean | `false` | центрирование содержимого |
|
|
124
148
|
| `decor` | объект | нет | семантический декоративный слой |
|
|
125
149
|
| `background` | объект или строка изображения | нет | явно заданное фоновое изображение |
|
|
126
150
|
|
|
127
|
-
`Slot` согласует размер до трёх прямых `Text`-детей с диапазонами `size` и `priority`. Атрибуты потока принадлежат дочерним элементам: `Text align="end"` прибивает строку внутри потока. Отдельный flow-атрибут есть у `Person`, когда блок автора используется как прямой ребёнок `Slot`.
|
|
151
|
+
`Slot` согласует размер до трёх прямых `Text`-детей с диапазонами `size` и `priority`. Несколько `Slot` с одинаковым `fit-group` получают общий размерный вектор: берётся максимальный вариант, который помещается во все ячейки группы. Если диапазоны одноимённых позиций не пересекаются, слоты сохраняют независимую локальную подгонку вместо ошибки рендера. Атрибуты потока принадлежат дочерним элементам: `Text align="end"` прибивает строку внутри потока. Отдельный flow-атрибут есть у `Person`, когда блок автора используется как прямой ребёнок `Slot`.
|
|
128
152
|
|
|
129
153
|
## Text API
|
|
130
154
|
|
|
@@ -140,6 +164,24 @@ layout: cover | message | explainer | collection | none
|
|
|
140
164
|
|
|
141
165
|
`max-size` выбирает самый крупный помещающийся токен; `muted` является булевым сокращением для `color="text-muted"` и учитывает текущую поверхность.
|
|
142
166
|
|
|
167
|
+
### Markdown в default slot
|
|
168
|
+
|
|
169
|
+
В `Text` можно писать блочный Markdown темы (списки, абзацы, цитаты) — не полный GFM Slidev. Это не layout shorthand: на `cover:*` по-прежнему нужны явные `Slot` / `Text`.
|
|
170
|
+
|
|
171
|
+
Inline-разметка `**жирный**` сохраняется как `<strong>` и отображается начертанием YS Text Bold с весом `600`.
|
|
172
|
+
|
|
173
|
+
Slidev парсит markdown внутри компонента только если контент **отделён пустой строкой** от открывающего и закрывающего тега (как в [доке Slidev](https://sli.dev/builtin/components)). Без пустых строк список схлопнется в одну строку и сломается. Не добавляйте лишний отступ в 4 пробела у пунктов — Slidev превратит блок в code.
|
|
174
|
+
|
|
175
|
+
```md
|
|
176
|
+
<Text size="4-5">
|
|
177
|
+
|
|
178
|
+
1. где мы сейчас
|
|
179
|
+
2. какой сценарий делегирования хотим
|
|
180
|
+
3. как безопасно перейти
|
|
181
|
+
|
|
182
|
+
</Text>
|
|
183
|
+
```
|
|
184
|
+
|
|
143
185
|
| Размер | Шрифт | Интерлиньяж |
|
|
144
186
|
| ------ | ------- | ----------- |
|
|
145
187
|
| `0` | `16px` | `1.2` |
|
|
@@ -173,7 +215,7 @@ layout: cover | message | explainer | collection | none
|
|
|
173
215
|
|
|
174
216
|
```md
|
|
175
217
|
<Slot area="6 / 7 / -1 / 10">
|
|
176
|
-
<Image src="/photos/photo-4.
|
|
218
|
+
<Image src="/theme/photos/photo-4.webp" fit="cover" position="50% 42%" zoom="1.12" />
|
|
177
219
|
</Slot>
|
|
178
220
|
```
|
|
179
221
|
|
|
@@ -182,10 +224,12 @@ layout: cover | message | explainer | collection | none
|
|
|
182
224
|
```md
|
|
183
225
|
<Slot
|
|
184
226
|
area="1 / 7 / -1 / -1"
|
|
185
|
-
:background="{ src: '/photos/photo-6.
|
|
227
|
+
:background="{ src: '/theme/photos/photo-6.webp', fit: 'cover', position: '50% 40%', zoom: 1.04 }"
|
|
186
228
|
/>
|
|
187
229
|
```
|
|
188
230
|
|
|
231
|
+
Встроенные файлы темы доступны по префиксу `/theme`: например, `/theme/photos/photo-6.webp` и `/theme/decor/decor-10.svg`. Файлы из `public` самой колоды остаются пользовательскими и задаются от корня, например `/decor/custom-data.png`; префикс `/theme` к ним добавлять не нужно.
|
|
232
|
+
|
|
189
233
|
Даже когда используется `Image` или `Slot.background`, фотография остаётся декоративной. Ключевые факты, различия, инструкции и выводы должны быть записаны текстом или числом.
|
|
190
234
|
|
|
191
235
|
### Decor
|
|
@@ -239,11 +283,21 @@ themeConfig:
|
|
|
239
283
|
|
|
240
284
|
В записи каталога `cols` и `rows` описывают допустимый размер слота в сетке 12x12, а `ratio` ограничивает соотношение `cols / rows` для ориентации. Вместо `ratio` можно использовать более явный алиас `aspectRatio`.
|
|
241
285
|
|
|
242
|
-
| Поле
|
|
243
|
-
|
|
|
244
|
-
| `deckTitle`
|
|
245
|
-
| `debugGrid`
|
|
246
|
-
| `decors`
|
|
286
|
+
| Поле | Тип | По умолчанию | Смысл |
|
|
287
|
+
| ----------------- | --------------- | ------------------ | -------------------------------------------------------- |
|
|
288
|
+
| `deckTitle` | строка | заголовок слайда | заголовок в шапке |
|
|
289
|
+
| `debugGrid` | boolean | `false` | отладочная сетка 12x12 |
|
|
290
|
+
| `decors` | записи каталога | встроенный каталог | добавляет или переопределяет записи декора по `decor.id` |
|
|
291
|
+
| `decorSaveOrigin` | origin URL | origin dev-сервера | разрешённый origin для сохранения настроек декора |
|
|
292
|
+
|
|
293
|
+
Если dev-сервер работает за reverse proxy с завершением TLS, задайте внешний origin явно:
|
|
294
|
+
|
|
295
|
+
```yaml
|
|
296
|
+
themeConfig:
|
|
297
|
+
decorSaveOrigin: https://slides.example
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Endpoint сохранения принимает только точное значение `Origin`. Без явного `decorSaveOrigin` он выводит origin из TLS-соединения и заголовка `Host`; заголовкам `X-Forwarded-*` endpoint не доверяет.
|
|
247
301
|
|
|
248
302
|
## Цветовые токены
|
|
249
303
|
|
|
@@ -263,8 +317,16 @@ themeConfig:
|
|
|
263
317
|
| `red-0` | `#e84033` |
|
|
264
318
|
| `yellow-0` | `#ffd20a` |
|
|
265
319
|
|
|
266
|
-
## Проверка
|
|
320
|
+
## Проверка перед выпуском
|
|
267
321
|
|
|
268
322
|
```bash
|
|
269
|
-
npm
|
|
323
|
+
npm test
|
|
270
324
|
```
|
|
325
|
+
|
|
326
|
+
Единая команда последовательно проверяет:
|
|
327
|
+
|
|
328
|
+
- lint и типы;
|
|
329
|
+
- модульные, контрактные и архитектурные тесты;
|
|
330
|
+
- продукционную сборку и состав её артефактов;
|
|
331
|
+
- репрезентативные слайды в Chromium, переполнение холста и загрузку медиа по `/theme/...`;
|
|
332
|
+
- состав и размер npm-пакета.
|
package/components/Slide.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, defineComponent, h, isVNode, onMounted, onUpdated, shallowRef, useSlots, type PropType, type VNode } from 'vue'
|
|
2
|
+
import { computed, defineComponent, h, isVNode, onBeforeUnmount, onMounted, onUpdated, shallowRef, unref, useSlots, type PropType, type VNode } from 'vue'
|
|
3
3
|
import { useSlideContext } from '@slidev/client'
|
|
4
4
|
import DebugGrid from './DebugGrid.vue'
|
|
5
5
|
import Header from './Header.vue'
|
|
@@ -9,10 +9,12 @@ import Slot from './Slot.vue'
|
|
|
9
9
|
import Text from './Text.vue'
|
|
10
10
|
import Timeline from './Timeline.vue'
|
|
11
11
|
import { createSlideLayout } from '../composables/slide-layout'
|
|
12
|
-
import type {
|
|
12
|
+
import type { SlideMarkdownContractError } from '../composables/layout-shorthands'
|
|
13
|
+
import type { ThemeLayout, ThemeLayoutHeader } from '../composables/layout-recipes'
|
|
13
14
|
import { typographElement } from '../composables/text-typography.mjs'
|
|
14
15
|
import { themeVars, type ThemeSlideMode, type ThemeTone } from '../composables/theme-foundation'
|
|
15
16
|
import { provideSlotPlacementSession } from '../composables/slot-placement'
|
|
17
|
+
import { provideTextFitScope } from '../composables/text-fit-scope'
|
|
16
18
|
import { useThemeConfig } from '../composables/use-theme-config'
|
|
17
19
|
|
|
18
20
|
const props = withDefaults(defineProps<{
|
|
@@ -33,8 +35,12 @@ const props = withDefaults(defineProps<{
|
|
|
33
35
|
|
|
34
36
|
const slots = useSlots()
|
|
35
37
|
const slideElement = shallowRef<HTMLElement | null>(null)
|
|
38
|
+
const browserLifecycleToken = typeof window !== 'undefined'
|
|
39
|
+
&& '__practicumBrowserLifecycle' in window
|
|
40
|
+
? `${Date.now()}-${Math.random()}`
|
|
41
|
+
: ''
|
|
36
42
|
|
|
37
|
-
const { $frontmatter } = useSlideContext()
|
|
43
|
+
const { $frontmatter, $page } = useSlideContext()
|
|
38
44
|
const { debugGrid, defaultTone } = useThemeConfig()
|
|
39
45
|
const slideLayout = createSlideLayout({
|
|
40
46
|
components: {
|
|
@@ -46,14 +52,38 @@ const slideLayout = createSlideLayout({
|
|
|
46
52
|
},
|
|
47
53
|
})
|
|
48
54
|
|
|
55
|
+
const layoutContractLabel = computed(() => {
|
|
56
|
+
const layout = props.layout || String($frontmatter.layout ?? '')
|
|
57
|
+
const variant = props.variant || String($frontmatter.variant ?? '')
|
|
58
|
+
return variant ? `${layout}:${variant}` : layout
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function reportLayoutContractError(error: SlideMarkdownContractError) {
|
|
62
|
+
const lines = [
|
|
63
|
+
`[Slidev] Слайд ${$page ?? '?'}: ${layoutContractLabel.value}`,
|
|
64
|
+
error.message,
|
|
65
|
+
]
|
|
66
|
+
if (error.hint)
|
|
67
|
+
lines.push(` Подсказка: ${error.hint}`)
|
|
68
|
+
lines.push(' См. example.md в slidev-theme-practicum (блоки «Контракт»).')
|
|
69
|
+
console.error(lines.join('\n'))
|
|
70
|
+
}
|
|
71
|
+
|
|
49
72
|
function compileCurrentAuthoring(children?: readonly VNode[]) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
try {
|
|
74
|
+
return slideLayout.compile({
|
|
75
|
+
props,
|
|
76
|
+
frontmatter: $frontmatter as Record<string, unknown>,
|
|
77
|
+
children,
|
|
78
|
+
defaultTone: defaultTone.value,
|
|
79
|
+
debugGrid: debugGrid.value,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error && error.name === 'SlideMarkdownContractError')
|
|
84
|
+
reportLayoutContractError(error as SlideMarkdownContractError)
|
|
85
|
+
throw error
|
|
86
|
+
}
|
|
57
87
|
}
|
|
58
88
|
|
|
59
89
|
const authored = computed(() => compileCurrentAuthoring())
|
|
@@ -65,19 +95,51 @@ const resolvedTheme = computed(() => authored.value.theme)
|
|
|
65
95
|
const isContrast = computed(() => authored.value.contrast)
|
|
66
96
|
const showDebugGrid = computed(() => authored.value.showDebugGrid)
|
|
67
97
|
provideSlotPlacementSession(slideLayout)
|
|
98
|
+
provideTextFitScope()
|
|
68
99
|
|
|
69
100
|
function applySlideTypography() {
|
|
70
101
|
typographElement(slideElement.value)
|
|
71
102
|
}
|
|
72
103
|
|
|
104
|
+
function reportBrowserLifecycle(
|
|
105
|
+
phase: 'mounted' | 'unmounted',
|
|
106
|
+
page: number | string,
|
|
107
|
+
token: string,
|
|
108
|
+
) {
|
|
109
|
+
if (typeof window === 'undefined' || !('__practicumBrowserLifecycle' in window))
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
window.dispatchEvent(new CustomEvent('practicum:slide-lifecycle', {
|
|
113
|
+
detail: {
|
|
114
|
+
phase,
|
|
115
|
+
slide: page,
|
|
116
|
+
token,
|
|
117
|
+
},
|
|
118
|
+
}))
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
onMounted(() => {
|
|
74
122
|
applySlideTypography()
|
|
123
|
+
reportBrowserLifecycle(
|
|
124
|
+
'mounted',
|
|
125
|
+
unref($page) ?? 0,
|
|
126
|
+
browserLifecycleToken,
|
|
127
|
+
)
|
|
75
128
|
})
|
|
76
129
|
|
|
77
130
|
onUpdated(() => {
|
|
78
131
|
applySlideTypography()
|
|
79
132
|
})
|
|
80
133
|
|
|
134
|
+
onBeforeUnmount(() => {
|
|
135
|
+
reportBrowserLifecycle(
|
|
136
|
+
'unmounted',
|
|
137
|
+
unref($page) ?? 0,
|
|
138
|
+
browserLifecycleToken,
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// eslint-disable-next-line vue/one-component-per-file -- Рендер-адаптер намеренно локален: он не является самостоятельным публичным компонентом.
|
|
81
143
|
const RenderVNode = defineComponent({
|
|
82
144
|
name: 'RenderVNode',
|
|
83
145
|
props: {
|
|
@@ -93,6 +155,7 @@ const RenderVNode = defineComponent({
|
|
|
93
155
|
|
|
94
156
|
const slideStyle = computed(() => themeVars('slide', resolvedTheme.value))
|
|
95
157
|
|
|
158
|
+
// eslint-disable-next-line vue/one-component-per-file -- Компонент замыкает compileCurrentAuthoring текущего слайда и не имеет самостоятельного API.
|
|
96
159
|
const LayoutBody = defineComponent({
|
|
97
160
|
name: 'LayoutBody',
|
|
98
161
|
setup(_, { slots: layoutSlots }) {
|
|
@@ -219,22 +282,6 @@ const slideClass = computed(() => ({
|
|
|
219
282
|
margin-bottom: var(--theme-slot-margin-4);
|
|
220
283
|
}
|
|
221
284
|
|
|
222
|
-
:deep(.Slide-TitleSupportsList) {
|
|
223
|
-
max-width: var(--theme-grid-span-6-width);
|
|
224
|
-
padding-left: 0;
|
|
225
|
-
list-style: none;
|
|
226
|
-
color: var(--theme-text-muted);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
:deep(.Slide-TitleSupportsList li) {
|
|
230
|
-
margin-bottom: calc(var(--theme-grid-module) * 2);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
:deep(.Slide-TitleSupportsList_size_large li) {
|
|
234
|
-
font-size: var(--theme-text-size-4);
|
|
235
|
-
line-height: var(--theme-text-line-4);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
285
|
:deep(.Slide-TitleBody) {
|
|
239
286
|
max-width: var(--theme-grid-span-6-width);
|
|
240
287
|
color: var(--theme-text);
|
|
@@ -242,6 +289,10 @@ const slideClass = computed(() => ({
|
|
|
242
289
|
line-height: var(--theme-text-line-4);
|
|
243
290
|
}
|
|
244
291
|
|
|
292
|
+
:deep(.Slide-TitleBody_muted) {
|
|
293
|
+
color: var(--theme-text-muted);
|
|
294
|
+
}
|
|
295
|
+
|
|
245
296
|
:deep(.Slide-TitleBody :where(p, ul, ol, blockquote)) {
|
|
246
297
|
margin: 0;
|
|
247
298
|
color: inherit;
|
|
@@ -272,6 +323,10 @@ const slideClass = computed(() => ({
|
|
|
272
323
|
content: '●';
|
|
273
324
|
}
|
|
274
325
|
|
|
326
|
+
:deep(.Slide-TitleBody ul ul li)::marker {
|
|
327
|
+
content: '▪︎';
|
|
328
|
+
}
|
|
329
|
+
|
|
275
330
|
:deep(.Slide-TitleBody :where(p, ul, ol, blockquote) + :where(p, ul, ol, blockquote)) {
|
|
276
331
|
margin-top: calc(var(--theme-grid-module) * 3);
|
|
277
332
|
}
|
package/components/Slot.vue
CHANGED
|
@@ -2,30 +2,26 @@
|
|
|
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
|
-
import type { ThemeLayoutSlotSpec } from '../composables/layout-
|
|
13
|
+
import type { ThemeLayoutSlotSpec } from '../composables/layout-recipes'
|
|
19
14
|
import { useSlotPlacementSession, type ResolvedSlotPlacement } from '../composables/slot-placement'
|
|
20
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
resolveSlotGapCssValue,
|
|
17
|
+
resolveSlotSpacingStyle,
|
|
18
|
+
type SlotGapInput,
|
|
19
|
+
type SlotMarginInput,
|
|
20
|
+
} from '../composables/slot-spacing'
|
|
21
21
|
import { useThemeConfig } from '../composables/use-theme-config'
|
|
22
22
|
import { resolveSlotTheme, themeVars, type ThemeSlotSurface } from '../composables/theme-foundation'
|
|
23
23
|
import { camelToKebab } from '../composables/layout-vnode'
|
|
24
24
|
|
|
25
|
-
type SlotMarginLevel = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
|
|
26
|
-
type SlotMarginInput = SlotMarginLevel | `${SlotMarginLevel}`
|
|
27
|
-
type SlotGap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
|
|
28
|
-
type SlotGapInput = SlotGap | `${SlotGap}`
|
|
29
25
|
type SlotRole = 'primary' | 'secondary' | 'support' | 'media'
|
|
30
26
|
type SlotSurfaceInput = ThemeSlotSurface
|
|
31
27
|
type ImageDataInput = {
|
|
@@ -51,8 +47,6 @@ type SlotDecorInput = Omit<Partial<ImageDataInput>, 'src'> & {
|
|
|
51
47
|
tone?: string
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
const SLOT_MARGIN_LEVELS = new Set<`${SlotMarginLevel}`>(['1', '2', '3', '4', '5', '6', '7', '8'])
|
|
55
|
-
const SLOT_GAP_LEVELS = new Set<`${SlotGap}`>(['0', '1', '2', '3', '4', '5', '6', '7'])
|
|
56
50
|
const layoutOverrideKeys = [
|
|
57
51
|
'surface',
|
|
58
52
|
'tone',
|
|
@@ -64,36 +58,6 @@ const layoutOverrideKeys = [
|
|
|
64
58
|
'centered',
|
|
65
59
|
] as const satisfies readonly (keyof ThemeLayoutSlotSpec)[]
|
|
66
60
|
|
|
67
|
-
const SlotContent = defineComponent({
|
|
68
|
-
name: 'SlotContent',
|
|
69
|
-
props: {
|
|
70
|
-
contentStyle: {
|
|
71
|
-
type: Object as PropType<CSSProperties>,
|
|
72
|
-
required: true,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
setup(contentProps, { slots }) {
|
|
76
|
-
const contentElement = shallowRef<HTMLElement | null>(null)
|
|
77
|
-
let currentChildren: VNode[] = []
|
|
78
|
-
const textFit = createTextFitRuntime()
|
|
79
|
-
const { apply } = textFit.useGroup({
|
|
80
|
-
target: contentElement,
|
|
81
|
-
nodes: () => currentChildren,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
return () => {
|
|
85
|
-
const children = (slots.default?.() ?? []).filter(isVNode)
|
|
86
|
-
currentChildren = children
|
|
87
|
-
|
|
88
|
-
return h('div', {
|
|
89
|
-
ref: contentElement,
|
|
90
|
-
class: 'Slot-Content',
|
|
91
|
-
style: contentProps.contentStyle,
|
|
92
|
-
}, apply(children))
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
|
|
97
61
|
const props = withDefaults(defineProps<{
|
|
98
62
|
as?: string
|
|
99
63
|
role?: SlotRole
|
|
@@ -111,6 +75,7 @@ const props = withDefaults(defineProps<{
|
|
|
111
75
|
marginBottom?: SlotMarginInput
|
|
112
76
|
marginLeft?: SlotMarginInput
|
|
113
77
|
gap?: SlotGapInput
|
|
78
|
+
fitGroup?: string
|
|
114
79
|
centered?: boolean
|
|
115
80
|
}>(), {
|
|
116
81
|
as: 'section',
|
|
@@ -124,6 +89,7 @@ const props = withDefaults(defineProps<{
|
|
|
124
89
|
background: undefined,
|
|
125
90
|
decor: undefined,
|
|
126
91
|
gap: '0',
|
|
92
|
+
fitGroup: '',
|
|
127
93
|
centered: false,
|
|
128
94
|
})
|
|
129
95
|
|
|
@@ -259,52 +225,21 @@ const backgroundLayers = computed(() =>
|
|
|
259
225
|
}),
|
|
260
226
|
)
|
|
261
227
|
|
|
262
|
-
function resolveMarginCssValue(value: SlotMarginInput | undefined) {
|
|
263
|
-
if (value == null)
|
|
264
|
-
return undefined
|
|
265
|
-
|
|
266
|
-
const normalizedValue = String(value) as `${SlotMarginLevel}`
|
|
267
|
-
if (!SLOT_MARGIN_LEVELS.has(normalizedValue)) {
|
|
268
|
-
throw new Error(`[${props.label || slotId}] margin должен быть уровнем 1..8. Получено: "${String(value)}".`)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return `var(--theme-slot-margin-${normalizedValue})`
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function resolveGapCssValue(value: SlotGapInput | undefined) {
|
|
275
|
-
if (value == null)
|
|
276
|
-
return '0'
|
|
277
|
-
|
|
278
|
-
const normalizedValue = String(value) as `${SlotGap}`
|
|
279
|
-
if (normalizedValue === '0')
|
|
280
|
-
return '0'
|
|
281
|
-
|
|
282
|
-
if (!SLOT_GAP_LEVELS.has(normalizedValue)) {
|
|
283
|
-
throw new Error(`[${props.label || slotId}] gap должен быть уровнем 0..7. Получено: "${String(value)}".`)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return `var(--theme-slot-margin-${normalizedValue})`
|
|
287
|
-
}
|
|
288
|
-
|
|
289
228
|
const marginStyle = computed(() => {
|
|
290
229
|
const layoutSlot = resolvedLayoutSlot.value
|
|
291
|
-
const margin = layoutSlot?.margin ?? props.margin
|
|
292
|
-
const style = {
|
|
293
|
-
paddingTop: resolveMarginCssValue(layoutSlot?.marginTop ?? props.marginTop ?? margin),
|
|
294
|
-
paddingRight: resolveMarginCssValue(layoutSlot?.marginRight ?? props.marginRight ?? margin),
|
|
295
|
-
paddingBottom: resolveMarginCssValue(layoutSlot?.marginBottom ?? props.marginBottom ?? margin),
|
|
296
|
-
paddingLeft: resolveMarginCssValue(layoutSlot?.marginLeft ?? props.marginLeft ?? margin),
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (Object.values(style).every(value => value == null))
|
|
300
|
-
return null
|
|
301
230
|
|
|
302
|
-
return
|
|
231
|
+
return resolveSlotSpacingStyle({
|
|
232
|
+
margin: layoutSlot?.margin ?? props.margin,
|
|
233
|
+
marginTop: layoutSlot?.marginTop ?? props.marginTop,
|
|
234
|
+
marginRight: layoutSlot?.marginRight ?? props.marginRight,
|
|
235
|
+
marginBottom: layoutSlot?.marginBottom ?? props.marginBottom,
|
|
236
|
+
marginLeft: layoutSlot?.marginLeft ?? props.marginLeft,
|
|
237
|
+
}, props.label || slotId)
|
|
303
238
|
})
|
|
304
239
|
|
|
305
240
|
const contentStyle = computed(() => ({
|
|
306
241
|
...(marginStyle.value ?? {}),
|
|
307
|
-
'--theme-slot-gap':
|
|
242
|
+
'--theme-slot-gap': resolveSlotGapCssValue(props.gap, props.label || slotId),
|
|
308
243
|
}))
|
|
309
244
|
|
|
310
245
|
const alignmentClass = computed(() => ({
|
|
@@ -333,9 +268,9 @@ const surfaceClass = computed(() => {
|
|
|
333
268
|
class="Slot-BackgroundLayer"
|
|
334
269
|
:layer="backgroundLayer" />
|
|
335
270
|
</div>
|
|
336
|
-
<
|
|
271
|
+
<TextFitGroup class="Slot-Content" :style="contentStyle" :fit-group="props.fitGroup">
|
|
337
272
|
<slot />
|
|
338
|
-
</
|
|
273
|
+
</TextFitGroup>
|
|
339
274
|
</component>
|
|
340
275
|
</template>
|
|
341
276
|
|
package/components/Text.vue
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, shallowRef } from 'vue'
|
|
2
|
+
import { computed, shallowRef, useSlots } from 'vue'
|
|
3
3
|
import {
|
|
4
4
|
createTextFitRuntime,
|
|
5
5
|
parseThemeTextSizeRange,
|
|
6
6
|
type ThemeTextSize,
|
|
7
7
|
type ThemeTextSizeInput,
|
|
8
8
|
} from '../composables/text-fit-runtime'
|
|
9
|
+
import { resolveTextSlotMarkdown } from '../composables/text-slot-markdown.mjs'
|
|
9
10
|
|
|
10
11
|
type ThemeTextPriority = 1 | 2 | 3
|
|
11
12
|
type ThemeTextPriorityInput = ThemeTextPriority | `${ThemeTextPriority}`
|
|
@@ -71,6 +72,15 @@ const props = withDefaults(defineProps<{
|
|
|
71
72
|
color: 'current',
|
|
72
73
|
})
|
|
73
74
|
|
|
75
|
+
const slots = useSlots()
|
|
76
|
+
const markdownNodes = computed(() => {
|
|
77
|
+
const tag = props.as.toLowerCase()
|
|
78
|
+
|
|
79
|
+
if (/^h[1-6]$/.test(tag))
|
|
80
|
+
return null
|
|
81
|
+
|
|
82
|
+
return resolveTextSlotMarkdown(slots.default?.() ?? [])
|
|
83
|
+
})
|
|
74
84
|
const textElement = shallowRef<HTMLElement | null>(null)
|
|
75
85
|
const normalizedSizeRange = computed(() => parseThemeTextSizeRange(props.size, 2))
|
|
76
86
|
const fitMax = computed(() => props.maxSize || normalizedSizeRange.value.isRange)
|
|
@@ -120,7 +130,10 @@ const textStyle = computed(() => ({
|
|
|
120
130
|
},
|
|
121
131
|
]"
|
|
122
132
|
:style="textStyle">
|
|
123
|
-
<slot />
|
|
133
|
+
<slot v-if="!markdownNodes" />
|
|
134
|
+
<component v-for="(node, index) in markdownNodes"
|
|
135
|
+
:is="node"
|
|
136
|
+
:key="index" />
|
|
124
137
|
</component>
|
|
125
138
|
</template>
|
|
126
139
|
|
|
@@ -129,6 +142,7 @@ const textStyle = computed(() => ({
|
|
|
129
142
|
min-width: 0;
|
|
130
143
|
margin: 0;
|
|
131
144
|
font-weight: 400;
|
|
145
|
+
--theme-text-list-marker-width: 2ch;
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
.Text_fit_max {
|
|
@@ -213,4 +227,56 @@ const textStyle = computed(() => ({
|
|
|
213
227
|
font-size: var(--theme-text-size-12);
|
|
214
228
|
line-height: var(--theme-text-line-12);
|
|
215
229
|
}
|
|
230
|
+
|
|
231
|
+
.Text :deep(:where(p, li)) {
|
|
232
|
+
font-size: inherit;
|
|
233
|
+
line-height: inherit;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.Text :deep(p) {
|
|
237
|
+
margin: 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.Text :deep(:where(ul, ol)) {
|
|
241
|
+
margin: 0;
|
|
242
|
+
padding-left: 0;
|
|
243
|
+
list-style-position: outside;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.Text :deep(ul) {
|
|
247
|
+
list-style-type: disc;
|
|
248
|
+
padding-inline-start: 1.25ch;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.Text :deep(ol) {
|
|
252
|
+
list-style-type: decimal;
|
|
253
|
+
padding-inline-start: var(--theme-text-list-marker-width);
|
|
254
|
+
font-variant-numeric: tabular-nums;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.Text :deep(li > :is(ul, ol)) {
|
|
258
|
+
padding-inline-start: calc(var(--theme-grid-module) * 2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.Text :deep(li) {
|
|
262
|
+
display: list-item;
|
|
263
|
+
margin-bottom: var(--theme-space-flow);
|
|
264
|
+
color: inherit;
|
|
265
|
+
font-size: inherit;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Slidev pass-through: <li><p>…</p></li> ломает outside-маркеры */
|
|
269
|
+
.Text :deep(li > p) {
|
|
270
|
+
display: inline;
|
|
271
|
+
margin: 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.Text :deep(:where(p, li) + :where(p, ul, ol)) {
|
|
275
|
+
margin-top: var(--theme-space-flow);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.Text :deep(li)::marker {
|
|
279
|
+
color: currentcolor;
|
|
280
|
+
font-size: inherit;
|
|
281
|
+
}
|
|
216
282
|
</style>
|