slidev-theme-practicum 0.1.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.
Files changed (88) hide show
  1. package/README.md +270 -0
  2. package/components/DebugGrid.vue +50 -0
  3. package/components/DecorLibrary.vue +1117 -0
  4. package/components/Header.vue +124 -0
  5. package/components/Image.vue +82 -0
  6. package/components/ImageRenderer.vue +126 -0
  7. package/components/Logo.vue +142 -0
  8. package/components/Person.vue +89 -0
  9. package/components/Slide.vue +324 -0
  10. package/components/Slot.vue +419 -0
  11. package/components/Text.vue +216 -0
  12. package/components/Timeline.vue +202 -0
  13. package/composables/decor-catalog.mjs +339 -0
  14. package/composables/decor-config.mjs +488 -0
  15. package/composables/decor-file-store.ts +192 -0
  16. package/composables/decor-http-store.ts +25 -0
  17. package/composables/decor-store.ts +39 -0
  18. package/composables/decor-tuning-overrides.mjs +2 -0
  19. package/composables/decor-workbench.ts +799 -0
  20. package/composables/grid-placement.ts +396 -0
  21. package/composables/image-config.mjs +484 -0
  22. package/composables/layout-authoring.ts +372 -0
  23. package/composables/layout-markdown.mjs +56 -0
  24. package/composables/layout-recipes.ts +347 -0
  25. package/composables/layout-registry.ts +47 -0
  26. package/composables/layout-shorthands.ts +891 -0
  27. package/composables/layout-vnode.ts +43 -0
  28. package/composables/logo-mark.mjs +5 -0
  29. package/composables/slide-layout.ts +43 -0
  30. package/composables/slot-placement.ts +195 -0
  31. package/composables/slot-rules.ts +147 -0
  32. package/composables/text-fit-runtime.ts +577 -0
  33. package/composables/text-priority-fit.mjs +166 -0
  34. package/composables/text-typography.mjs +138 -0
  35. package/composables/theme-foundation.ts +192 -0
  36. package/composables/theme-media.mjs +202 -0
  37. package/composables/use-theme-config.ts +71 -0
  38. package/decor-library.md +10 -0
  39. package/env.d.ts +6 -0
  40. package/example.md +786 -0
  41. package/layouts/collection.vue +5 -0
  42. package/layouts/cover.vue +5 -0
  43. package/layouts/default.vue +5 -0
  44. package/layouts/explainer.vue +5 -0
  45. package/layouts/message.vue +5 -0
  46. package/layouts/none.vue +5 -0
  47. package/layouts/section.vue +5 -0
  48. package/layouts/two-cols.vue +11 -0
  49. package/package.json +84 -0
  50. package/public/decor/decor-1.svg +83 -0
  51. package/public/decor/decor-10.svg +40 -0
  52. package/public/decor/decor-11.svg +162 -0
  53. package/public/decor/decor-12.svg +83 -0
  54. package/public/decor/decor-13.svg +5 -0
  55. package/public/decor/decor-14.svg +7 -0
  56. package/public/decor/decor-15.svg +10 -0
  57. package/public/decor/decor-16.svg +9 -0
  58. package/public/decor/decor-17.png +0 -0
  59. package/public/decor/decor-18.png +0 -0
  60. package/public/decor/decor-19.svg +5 -0
  61. package/public/decor/decor-2.png +0 -0
  62. package/public/decor/decor-3.svg +9 -0
  63. package/public/decor/decor-4.svg +31 -0
  64. package/public/decor/decor-5.png +0 -0
  65. package/public/decor/decor-6.png +0 -0
  66. package/public/decor/decor-7.png +0 -0
  67. package/public/decor/decor-8.svg +21 -0
  68. package/public/decor/decor-9.svg +38 -0
  69. package/public/favicon.svg +11 -0
  70. package/public/photos/photo-1.png +0 -0
  71. package/public/photos/photo-10.png +0 -0
  72. package/public/photos/photo-2.png +0 -0
  73. package/public/photos/photo-3.png +0 -0
  74. package/public/photos/photo-4.png +0 -0
  75. package/public/photos/photo-5.png +0 -0
  76. package/public/photos/photo-6.png +0 -0
  77. package/public/photos/photo-7.png +0 -0
  78. package/public/photos/photo-8.png +0 -0
  79. package/public/photos/photo-9.png +0 -0
  80. package/setup/main.ts +3 -0
  81. package/setup/shiki.ts +10 -0
  82. package/setup/vite-plugins.ts +106 -0
  83. package/slidev-theme-practicum.png +0 -0
  84. package/slidev-theme-practicum.svg +102 -0
  85. package/styles/example.css +0 -0
  86. package/styles/index.css +192 -0
  87. package/styles/vars.css +119 -0
  88. package/types/slidev-client.d.ts +15 -0
@@ -0,0 +1,372 @@
1
+ import { Fragment, Text as NativeText, isVNode, type VNode } from 'vue'
2
+ import { resolveAgendaDecor } from './decor-config.mjs'
3
+ import {
4
+ buildLayoutShorthandChildren,
5
+ hasLayoutShorthand,
6
+ shouldUseExplicitRoleChildren,
7
+ type LayoutShorthandComponents,
8
+ type LayoutShorthandContext,
9
+ } from './layout-shorthands'
10
+ import {
11
+ resolveLayoutSpec,
12
+ resolveLayoutVariant,
13
+ type ThemeLayout,
14
+ type ThemeLayoutHeader,
15
+ type ThemeLayoutRole,
16
+ type ThemeLayoutSlotSpec,
17
+ } from './layout-registry'
18
+ import { resolveLayoutRecipe, resolveLayoutSlotProps, THEME_LAYOUT_ROLES, type ThemeLayoutRecipe, type ThemeLayoutSpec } from './layout-recipes'
19
+ import { isBlankTextVNode, isCommentVNode } from './layout-vnode'
20
+ import { createSlotRules, type SlotRules } from './slot-rules'
21
+ import {
22
+ isContrastSlideMode,
23
+ resolveSlideMode,
24
+ resolveThemeMode,
25
+ type ResolvedThemeMode,
26
+ type ThemeSlideMode,
27
+ type ThemeTone,
28
+ } from './theme-foundation'
29
+
30
+ export type SlideLayoutProps = {
31
+ layout?: ThemeLayout | ''
32
+ variant?: string
33
+ arrangement?: string
34
+ mode?: ThemeSlideMode
35
+ tone?: ThemeTone | ''
36
+ header?: ThemeLayoutHeader
37
+ }
38
+
39
+ export type LayoutAuthoringInput = {
40
+ props: SlideLayoutProps
41
+ frontmatter: Record<string, unknown>
42
+ children?: readonly VNode[]
43
+ components: LayoutShorthandComponents
44
+ defaultTone: ThemeTone
45
+ debugGrid?: boolean
46
+ slotPlanState?: LayoutSlotPlanState
47
+ }
48
+
49
+ export type LayoutSlotPlanState = Map<string, { role: ThemeLayoutRole, index: number }>
50
+
51
+ export type ResolvedLayoutSlot = ThemeLayoutSlotSpec & {
52
+ role: ThemeLayoutRole
53
+ index: number
54
+ }
55
+
56
+ export type LayoutSlotPlan = {
57
+ recipe: ThemeLayoutRecipe
58
+ resolveRoleSlot(input: {
59
+ id: string
60
+ role: ThemeLayoutRole
61
+ overrides: Partial<ThemeLayoutSlotSpec>
62
+ label: string
63
+ }): ResolvedLayoutSlot
64
+ unregisterRoleSlot(id: string): void
65
+ }
66
+
67
+ export type CompiledLayoutAuthoring = {
68
+ mode: 'manual' | 'layout'
69
+ layout: ThemeLayout | ''
70
+ variant: string
71
+ header: Exclude<ThemeLayoutHeader, 'auto'>
72
+ slideMode: ThemeSlideMode
73
+ theme: ResolvedThemeMode
74
+ contrast: boolean
75
+ showDebugGrid: boolean
76
+ children: VNode[]
77
+ slotPlan: LayoutSlotPlan | null
78
+ }
79
+
80
+ const THEME_LAYOUTS = new Set<ThemeLayout>(['cover', 'message', 'explainer', 'collection'])
81
+
82
+ export function createLayoutSlotPlanState(): LayoutSlotPlanState {
83
+ return new Map()
84
+ }
85
+
86
+ function readFrontmatterValue(frontmatter: Record<string, unknown>, key: string) {
87
+ return frontmatter[key]
88
+ }
89
+
90
+ function readFrontmatterString(frontmatter: Record<string, unknown>, key: string) {
91
+ const value = readFrontmatterValue(frontmatter, key)
92
+ return typeof value === 'string' ? value.trim() : ''
93
+ }
94
+
95
+ function readFrontmatterText(frontmatter: Record<string, unknown>, key: string) {
96
+ const value = readFrontmatterValue(frontmatter, key)
97
+
98
+ if (typeof value === 'string')
99
+ return value.trim()
100
+
101
+ if (typeof value === 'number')
102
+ return String(value)
103
+
104
+ return ''
105
+ }
106
+
107
+ function readFrontmatterBoolean(frontmatter: Record<string, unknown>, key: string) {
108
+ const value = readFrontmatterValue(frontmatter, key)
109
+ if (typeof value === 'boolean')
110
+ return value
111
+
112
+ return undefined
113
+ }
114
+
115
+ function readFrontmatterArray(frontmatter: Record<string, unknown>, key: string) {
116
+ const value = readFrontmatterValue(frontmatter, key)
117
+ return Array.isArray(value) ? value : []
118
+ }
119
+
120
+ function readFrontmatterObject(frontmatter: Record<string, unknown>, key: string) {
121
+ const value = readFrontmatterValue(frontmatter, key)
122
+
123
+ if (typeof value === 'object' && value !== null && !Array.isArray(value))
124
+ return value as Record<string, unknown>
125
+
126
+ return null
127
+ }
128
+
129
+ function readHeader(value: unknown): ThemeLayoutHeader | undefined {
130
+ if (typeof value !== 'string')
131
+ return undefined
132
+
133
+ const normalized = value.trim()
134
+ if (normalized === 'auto' || normalized === 'default' || normalized === 'cover' || normalized === 'none')
135
+ return normalized
136
+
137
+ return undefined
138
+ }
139
+
140
+ function isThemeLayout(value: string): value is ThemeLayout {
141
+ return THEME_LAYOUTS.has(value as ThemeLayout)
142
+ }
143
+
144
+ function failMarkdownContract(label: string, message: string): never {
145
+ throw new Error(`[Slide] ${label} ${message}`)
146
+ }
147
+
148
+ function flattenVNodes(children: readonly VNode[], preserveText = false): VNode[] {
149
+ return children.flatMap((child) => {
150
+ if (!isVNode(child))
151
+ return []
152
+
153
+ if (isCommentVNode(child))
154
+ return []
155
+
156
+ if (child.type === NativeText)
157
+ return preserveText ? [child] : []
158
+
159
+ if (child.type === Fragment && Array.isArray(child.children))
160
+ return flattenVNodes(child.children.filter(isVNode), preserveText)
161
+
162
+ return [child]
163
+ })
164
+ }
165
+
166
+ function buildShorthandContext(input: {
167
+ frontmatter: Record<string, unknown>
168
+ variant: string
169
+ frontmatterDecor: Record<string, unknown> | null
170
+ agendaDecor: Record<string, unknown> | null
171
+ components: LayoutShorthandComponents
172
+ }): LayoutShorthandContext {
173
+ return {
174
+ variant: input.variant,
175
+ readFrontmatterValue: key => readFrontmatterValue(input.frontmatter, key),
176
+ readFrontmatterText: key => readFrontmatterText(input.frontmatter, key),
177
+ readFrontmatterArray: key => readFrontmatterArray(input.frontmatter, key),
178
+ readFrontmatterObject: key => readFrontmatterObject(input.frontmatter, key),
179
+ frontmatterDecor: input.frontmatterDecor,
180
+ agendaDecor: input.agendaDecor,
181
+ components: input.components,
182
+ }
183
+ }
184
+
185
+ function buildSourceChildren(input: {
186
+ rawChildren: readonly VNode[]
187
+ layout: ThemeLayout
188
+ variant: string
189
+ layoutSpec: ThemeLayoutSpec | null
190
+ context: LayoutShorthandContext
191
+ slotRules: SlotRules
192
+ }) {
193
+ const shorthandKey = `${input.layout}:${input.variant}`
194
+ const preserveText = hasLayoutShorthand(shorthandKey)
195
+ const children = flattenVNodes(input.rawChildren, preserveText)
196
+ const readRole = (node: VNode) => input.slotRules.read(node)?.role ?? ''
197
+
198
+ if (shouldUseExplicitRoleChildren(children, shorthandKey, readRole)) {
199
+ input.slotRules.assertChildren(children, {
200
+ label: `${input.layout}:${input.variant}`,
201
+ })
202
+ return children.filter(node => !isBlankTextVNode(node))
203
+ }
204
+
205
+ const shorthandChildren = buildLayoutShorthandChildren(shorthandKey, children, input.context)
206
+ if (shorthandChildren)
207
+ return shorthandChildren
208
+
209
+ if (input.layoutSpec && children.some(node => !isBlankTextVNode(node))) {
210
+ failMarkdownContract(
211
+ `${input.layout}:${input.variant}`,
212
+ `ожидает поддерживаемую markdown shorthand-структуру или Slot role="${THEME_LAYOUT_ROLES.join('|')}".`,
213
+ )
214
+ }
215
+
216
+ return children
217
+ }
218
+
219
+ function validateLayoutChild(input: {
220
+ node: VNode
221
+ layout: ThemeLayout
222
+ variant: string
223
+ slotRules: SlotRules
224
+ }) {
225
+ return input.slotRules.assertLayoutChild(input.node, {
226
+ label: `${input.layout}:${input.variant}`,
227
+ })
228
+ }
229
+
230
+ function resolveLayoutRoleIndex(state: LayoutSlotPlanState, id: string, role: ThemeLayoutRole) {
231
+ const current = state.get(id)
232
+ if (current?.role === role)
233
+ return current.index
234
+
235
+ const occupied = new Set(
236
+ [...state.entries()]
237
+ .filter(([entryId, entry]) => entryId !== id && entry.role === role)
238
+ .map(([, entry]) => entry.index),
239
+ )
240
+
241
+ let index = 0
242
+ while (occupied.has(index))
243
+ index += 1
244
+
245
+ state.set(id, { role, index })
246
+ return index
247
+ }
248
+
249
+ function createLayoutSlotPlan(input: {
250
+ recipe: ThemeLayoutRecipe
251
+ themeTone: ThemeTone
252
+ state: LayoutSlotPlanState
253
+ }): LayoutSlotPlan {
254
+ return {
255
+ recipe: input.recipe,
256
+ resolveRoleSlot({ id, role, overrides }) {
257
+ const index = resolveLayoutRoleIndex(input.state, id, role)
258
+ const slot = resolveLayoutSlotProps({
259
+ recipe: input.recipe,
260
+ role,
261
+ index,
262
+ overrides,
263
+ themeTone: input.themeTone,
264
+ })
265
+
266
+ return { ...slot, role, index }
267
+ },
268
+ unregisterRoleSlot(id) {
269
+ input.state.delete(id)
270
+ },
271
+ }
272
+ }
273
+
274
+ function resolveHeader(input: {
275
+ props: SlideLayoutProps
276
+ frontmatter: Record<string, unknown>
277
+ active: boolean
278
+ layout: ThemeLayout | ''
279
+ variant: string
280
+ }) {
281
+ const requestedHeader = input.props.header
282
+ ?? readHeader(readFrontmatterString(input.frontmatter, 'header'))
283
+ ?? 'auto'
284
+
285
+ if (requestedHeader !== 'auto')
286
+ return requestedHeader
287
+
288
+ if (!input.active)
289
+ return 'none'
290
+
291
+ if (input.layout === 'message' && input.variant === 'closing') {
292
+ const closingLogo = readFrontmatterString(input.frontmatter, 'logo') || 'full'
293
+ return closingLogo === 'none' ? 'none' : 'cover'
294
+ }
295
+
296
+ return input.layout === 'cover' ? 'cover' : 'default'
297
+ }
298
+
299
+ export function compileLayoutAuthoring(input: LayoutAuthoringInput): CompiledLayoutAuthoring {
300
+ const props = input.props
301
+ const frontmatter = input.frontmatter
302
+ const explicitLayout = props.layout || readFrontmatterString(frontmatter, 'layout')
303
+ const layout = isThemeLayout(explicitLayout) ? explicitLayout : ''
304
+ const active = Boolean(layout)
305
+ const slideMode = resolveSlideMode(props.mode ?? readFrontmatterString(frontmatter, 'mode'), 'Slide.mode', 'light')
306
+ const variant = resolveLayoutVariant(
307
+ layout,
308
+ props.variant || readFrontmatterString(frontmatter, 'variant'),
309
+ props.arrangement || readFrontmatterString(frontmatter, 'arrangement'),
310
+ )
311
+ const layoutSpec = active ? resolveLayoutSpec(layout as ThemeLayout, variant) : null
312
+ const recipe = active ? resolveLayoutRecipe(layout as ThemeLayout, variant) : null
313
+ const contrast = isContrastSlideMode(slideMode)
314
+ const frontmatterDecor = readFrontmatterObject(frontmatter, 'decor')
315
+ const agendaDecor = resolveAgendaDecor({
316
+ decor: frontmatterDecor,
317
+ tone: props.tone || readFrontmatterString(frontmatter, 'tone') || input.defaultTone,
318
+ contrast,
319
+ })
320
+ const theme = resolveThemeMode({
321
+ mode: slideMode,
322
+ tone: props.tone || readFrontmatterString(frontmatter, 'tone'),
323
+ defaultTone: input.defaultTone,
324
+ label: 'Slide',
325
+ })
326
+ const context = buildShorthandContext({
327
+ frontmatter,
328
+ variant,
329
+ frontmatterDecor,
330
+ agendaDecor,
331
+ components: input.components,
332
+ })
333
+ const rawChildren = input.children?.filter(isVNode) ?? []
334
+ const shouldCompileChildren = input.children !== undefined
335
+ const slotRules = createSlotRules({ Slot: input.components.Slot })
336
+ const children = active && shouldCompileChildren
337
+ ? buildSourceChildren({
338
+ rawChildren,
339
+ layout: layout as ThemeLayout,
340
+ variant,
341
+ layoutSpec,
342
+ context,
343
+ slotRules,
344
+ }).map(node =>
345
+ validateLayoutChild({
346
+ node,
347
+ layout: layout as ThemeLayout,
348
+ variant,
349
+ slotRules,
350
+ }),
351
+ )
352
+ : rawChildren
353
+
354
+ return {
355
+ mode: active ? 'layout' : 'manual',
356
+ layout,
357
+ variant,
358
+ header: resolveHeader({ props, frontmatter, active, layout, variant }),
359
+ slideMode,
360
+ theme,
361
+ contrast,
362
+ showDebugGrid: readFrontmatterBoolean(frontmatter, 'debug') ?? Boolean(input.debugGrid),
363
+ children,
364
+ slotPlan: recipe
365
+ ? createLayoutSlotPlan({
366
+ recipe,
367
+ themeTone: theme.tone,
368
+ state: input.slotPlanState ?? createLayoutSlotPlanState(),
369
+ })
370
+ : null,
371
+ }
372
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @param {unknown} input
3
+ * @returns {{ level: number, text: string } | null}
4
+ */
5
+ export function extractMarkdownHeading(input) {
6
+ if (typeof input !== 'string')
7
+ return null
8
+
9
+ const lines = input
10
+ .split(/\r?\n/)
11
+ .map(line => line.trim())
12
+ .filter(Boolean)
13
+
14
+ if (lines.length !== 1)
15
+ return null
16
+
17
+ const match = lines[0].match(/^(#{1,6})\s+(.+?)\s*$/)
18
+ if (!match)
19
+ return null
20
+
21
+ return {
22
+ level: match[1].length,
23
+ text: match[2].trim(),
24
+ }
25
+ }
26
+
27
+ /**
28
+ * @param {unknown} input
29
+ * @returns {string | null}
30
+ */
31
+ export function extractMarkdownQuote(input) {
32
+ if (typeof input !== 'string')
33
+ return null
34
+
35
+ const trimmed = input.trim()
36
+ if (!trimmed.startsWith('>'))
37
+ return null
38
+
39
+ const lines = trimmed
40
+ .split(/\r?\n/)
41
+ .map(line => line.trim())
42
+ .filter(Boolean)
43
+
44
+ if (!lines.length)
45
+ return null
46
+
47
+ const parts = lines.map((line) => {
48
+ const match = line.match(/^>\s?(.*)$/)
49
+ return match ? match[1].trim() : null
50
+ })
51
+
52
+ if (parts.some(part => part == null))
53
+ return null
54
+
55
+ return parts.join(' ').trim() || null
56
+ }