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,347 @@
1
+ import type { ThemeTone } from './theme-foundation'
2
+
3
+ export type ThemeLayout = 'cover' | 'message' | 'explainer' | 'collection'
4
+ export type ThemeLayoutRole = 'primary' | 'secondary' | 'support' | 'media'
5
+ export type ThemeLayoutHeader = 'auto' | 'default' | 'cover' | 'none'
6
+ export type ThemeLayoutSurface = 'none' | 'light' | 'dark' | 'color' | 'tint'
7
+ export type ThemeLayoutMarginLevel = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
8
+
9
+ export type ThemeLayoutSlotSpec = {
10
+ area: string
11
+ surface?: ThemeLayoutSurface
12
+ tone?: ThemeTone
13
+ margin?: ThemeLayoutMarginLevel
14
+ marginTop?: ThemeLayoutMarginLevel
15
+ marginRight?: ThemeLayoutMarginLevel
16
+ marginBottom?: ThemeLayoutMarginLevel
17
+ marginLeft?: ThemeLayoutMarginLevel
18
+ centered?: boolean
19
+ }
20
+
21
+ export type ThemeLayoutSpec = {
22
+ slots: Partial<Record<ThemeLayoutRole, ThemeLayoutSlotSpec[]>>
23
+ }
24
+
25
+ export type ThemeLayoutAlias = {
26
+ variant: string
27
+ arrangement?: string
28
+ }
29
+
30
+ export type ThemeLayoutRegionInput = string | ThemeLayoutSlotSpec
31
+
32
+ export type ThemeLayoutRecipe = {
33
+ layout: ThemeLayout
34
+ content: string
35
+ aliases: ThemeLayoutAlias[]
36
+ regions: Partial<Record<ThemeLayoutRole, ThemeLayoutRegionInput | ThemeLayoutRegionInput[]>>
37
+ shorthand?: string
38
+ }
39
+
40
+ type RecipeInput = Omit<ThemeLayoutRecipe, 'layout'>
41
+
42
+ export const THEME_LAYOUT_ROLES = ['primary', 'secondary', 'support', 'media'] as const satisfies readonly ThemeLayoutRole[]
43
+
44
+ function defineLayoutRecipes(layout: ThemeLayout, recipes: RecipeInput[]): ThemeLayoutRecipe[] {
45
+ return recipes.map(recipe => ({ layout, ...recipe }))
46
+ }
47
+
48
+ function normalizeRegionList(input: ThemeLayoutRegionInput | ThemeLayoutRegionInput[] | undefined) {
49
+ return input == null
50
+ ? []
51
+ : (Array.isArray(input) ? input : [input]).map(normalizeRegion)
52
+ }
53
+
54
+ function fail(label: string, message: string): never {
55
+ throw new Error(`[${label}] ${message}`)
56
+ }
57
+
58
+ export function normalizeRegion(input: ThemeLayoutRegionInput): ThemeLayoutSlotSpec {
59
+ return typeof input === 'string' ? { area: input } : input
60
+ }
61
+
62
+ export function resolveLayoutSlotProps(options: {
63
+ recipe: ThemeLayoutRecipe
64
+ role: ThemeLayoutRole
65
+ index: number
66
+ overrides: Partial<ThemeLayoutSlotSpec>
67
+ themeTone?: ThemeTone
68
+ }) {
69
+ const { recipe, role, index, overrides, themeTone } = options
70
+ const regions = recipe.regions[role]
71
+ const region = Array.isArray(regions)
72
+ ? regions[index]
73
+ : index === 0
74
+ ? regions
75
+ : undefined
76
+
77
+ if (!region)
78
+ fail('Slide', `Recipe "${recipe.layout}:${recipe.content}" не поддерживает ${role}[${index}].`)
79
+
80
+ const base = normalizeRegion(region)
81
+ const surface = overrides.surface ?? base.surface
82
+ const allowsTone = surface === 'color' || surface === 'tint'
83
+ const inheritedTone = overrides.surface !== undefined && overrides.surface !== base.surface
84
+ ? undefined
85
+ : base.tone
86
+ const tone = allowsTone
87
+ ? overrides.tone ?? inheritedTone ?? themeTone
88
+ : overrides.tone ?? inheritedTone
89
+
90
+ return { ...base, ...overrides, surface, tone }
91
+ }
92
+
93
+ export function normalizeRecipeRegions(recipe: ThemeLayoutRecipe): ThemeLayoutSpec {
94
+ return {
95
+ slots: Object.fromEntries(
96
+ THEME_LAYOUT_ROLES
97
+ .map(role => [role, normalizeRegionList(recipe.regions[role])] as const)
98
+ .filter(([, regions]) => regions.length > 0),
99
+ ) as ThemeLayoutSpec['slots'],
100
+ }
101
+ }
102
+
103
+ export const LAYOUT_RECIPES: ThemeLayoutRecipe[] = [
104
+ ...defineLayoutRecipes('cover', [
105
+ {
106
+ content: 'title-media',
107
+ aliases: [{ variant: 'grid' }],
108
+ regions: {
109
+ primary: '1 / 1 / -1 / 7',
110
+ media: ['1 / 7 / 5 / 10', '1 / 10 / 5 / -1', '5 / 7 / -1 / -1'],
111
+ },
112
+ },
113
+ {
114
+ content: 'title-media-balanced',
115
+ aliases: [{ variant: 'grid-balanced' }],
116
+ regions: {
117
+ primary: '1 / 1 / -1 / 7',
118
+ media: ['1 / 7 / 7 / 10', '1 / 10 / 7 / -1', '7 / 7 / -1 / -1'],
119
+ },
120
+ },
121
+ {
122
+ content: 'banner-media',
123
+ aliases: [{ variant: 'banner' }],
124
+ regions: {
125
+ primary: '1 / 1 / 7 / -1',
126
+ media: ['7 / 1 / -1 / 4', '7 / 4 / -1 / 8', '7 / 8 / -1 / -1'],
127
+ },
128
+ },
129
+ {
130
+ content: 'signal-media',
131
+ aliases: [{ variant: 'signal' }],
132
+ regions: {
133
+ primary: '1 / 1 / 7 / 9',
134
+ media: [
135
+ { area: '1 / 9 / -1 / -1', surface: 'dark' },
136
+ '7 / 1 / -1 / 4',
137
+ '7 / 4 / -1 / 9',
138
+ ],
139
+ },
140
+ },
141
+ {
142
+ content: 'split-media-triptych',
143
+ aliases: [{ variant: 'split-media-triptych' }],
144
+ regions: {
145
+ primary: '1 / 1 / -1 / 8',
146
+ media: ['1 / 8 / 5 / 10', '1 / 10 / 5 / -1', '5 / 8 / -1 / -1'],
147
+ },
148
+ },
149
+ ]),
150
+ ...defineLayoutRecipes('message', [
151
+ {
152
+ content: 'centered',
153
+ aliases: [{ variant: 'centered' }],
154
+ regions: {
155
+ primary: { area: '3 / 3 / 10 / 11', centered: true },
156
+ },
157
+ },
158
+ {
159
+ content: 'quote',
160
+ aliases: [{ variant: 'quote' }],
161
+ regions: {
162
+ primary: { area: '1 / 1 / 9 / -1', marginTop: 3 },
163
+ support: '10 / 1 / -1 / 5',
164
+ },
165
+ },
166
+ {
167
+ content: 'closing',
168
+ aliases: [{ variant: 'closing' }],
169
+ regions: {
170
+ primary: { area: '4 / 2 / 10 / 12', centered: true },
171
+ support: '12 / 1 / -1 / 4',
172
+ },
173
+ },
174
+ ]),
175
+ ...defineLayoutRecipes('explainer', [
176
+ {
177
+ content: 'title-supports',
178
+ aliases: [{ variant: 'title-supports' }],
179
+ regions: {
180
+ primary: '2 / 1 / -1 / 7',
181
+ secondary: '2 / 7 / -1 / -1',
182
+ },
183
+ },
184
+ {
185
+ content: 'title-supports-bottom-muted',
186
+ aliases: [{ variant: 'title-supports-bottom-muted' }],
187
+ regions: {
188
+ primary: '2 / 1 / 5 / 7',
189
+ support: [
190
+ { area: '6 / 1 / 10 / 7', marginTop: 2 },
191
+ { area: '6 / 7 / 10 / -1', marginTop: 2 },
192
+ ],
193
+ },
194
+ },
195
+ {
196
+ content: 'title-supports-bottom-plain',
197
+ aliases: [{ variant: 'title-supports-bottom-plain' }],
198
+ regions: {
199
+ primary: '2 / 1 / 5 / 7',
200
+ support: [
201
+ { area: '6 / 1 / -1 / 7', marginTop: 2 },
202
+ { area: '6 / 7 / -1 / -1', marginTop: 2 },
203
+ ],
204
+ },
205
+ },
206
+ {
207
+ content: 'title-body',
208
+ aliases: [{ variant: 'title-body' }],
209
+ regions: {
210
+ primary: { area: '2 / 1 / -1 / 7', marginRight: 4 },
211
+ secondary: '2 / 7 / -1 / -1',
212
+ },
213
+ },
214
+ {
215
+ content: 'definition',
216
+ aliases: [{ variant: 'definition' }],
217
+ regions: {
218
+ primary: { area: '1 / 1 / -1 / -1', marginTop: 8 },
219
+ },
220
+ },
221
+ ]),
222
+ ...defineLayoutRecipes('collection', [
223
+ {
224
+ content: 'agenda',
225
+ aliases: [{ variant: 'agenda' }],
226
+ regions: {
227
+ primary: { area: '1 / 1 / -1 / 9', margin: 4 },
228
+ media: '1 / 9 / -1 / -1',
229
+ },
230
+ },
231
+ {
232
+ content: 'points',
233
+ aliases: [{ variant: 'points-trio' }, { variant: 'points', arrangement: 'trio' }],
234
+ regions: {
235
+ primary: { area: '1 / 1 / 7 / -1', surface: 'light', margin: 4 },
236
+ support: [
237
+ { area: '7 / 1 / -1 / 5', surface: 'tint', margin: 3 },
238
+ { area: '7 / 5 / -1 / 9', surface: 'dark', margin: 3 },
239
+ { area: '7 / 9 / -1 / -1', surface: 'color', margin: 3 },
240
+ ],
241
+ },
242
+ },
243
+ {
244
+ content: 'metrics',
245
+ aliases: [{ variant: 'metrics-featured-media' }, { variant: 'metrics', arrangement: 'featured-media' }],
246
+ regions: {
247
+ primary: { area: '2 / 1 / 7 / -1', surface: 'light', margin: 4 },
248
+ media: '7 / 1 / -1 / 7',
249
+ support: [
250
+ { area: '7 / 7 / -1 / 10', surface: 'dark', margin: 3 },
251
+ { area: '7 / 10 / -1 / -1', surface: 'color', tone: 'blue', margin: 3 },
252
+ ],
253
+ },
254
+ },
255
+ {
256
+ content: 'metrics',
257
+ aliases: [{ variant: 'metrics-featured-copy' }, { variant: 'metrics', arrangement: 'featured-copy' }],
258
+ regions: {
259
+ primary: { area: '2 / 1 / 8 / -1', surface: 'light', marginLeft: 4, marginRight: 4 },
260
+ media: '8 / 1 / -1 / 7',
261
+ support: [
262
+ { area: '8 / 7 / -1 / 10', surface: 'dark', margin: 3 },
263
+ { area: '8 / 10 / -1 / -1', surface: 'color', tone: 'blue', margin: 3 },
264
+ ],
265
+ },
266
+ },
267
+ {
268
+ content: 'metrics',
269
+ aliases: [
270
+ { variant: 'metrics-featured-copy-split-media' },
271
+ { variant: 'metrics', arrangement: 'featured-copy-split-media' },
272
+ ],
273
+ regions: {
274
+ primary: { area: '2 / 1 / 8 / -1', surface: 'light', marginLeft: 4, marginRight: 4 },
275
+ media: ['8 / 1 / -1 / 4', '8 / 4 / -1 / 7'],
276
+ support: [
277
+ { area: '8 / 7 / -1 / 10', surface: 'dark', margin: 3 },
278
+ { area: '8 / 10 / -1 / -1', surface: 'color', tone: 'blue', margin: 3 },
279
+ ],
280
+ },
281
+ },
282
+ {
283
+ content: 'timeline',
284
+ aliases: [{ variant: 'timeline' }],
285
+ regions: {
286
+ primary: '1 / 1 / 4 / 7',
287
+ secondary: '1 / 7 / 4 / -1',
288
+ support: '5 / 1 / -1 / -1',
289
+ },
290
+ },
291
+ {
292
+ content: 'facts',
293
+ aliases: [{ variant: 'facts-stacked' }, { variant: 'facts', arrangement: 'stacked' }],
294
+ regions: {
295
+ primary: { area: '1 / 1 / -1 / 7', marginTop: 5, marginLeft: 2, marginRight: 3 },
296
+ support: [
297
+ { area: '1 / 7 / 5 / -1', marginTop: 5 },
298
+ { area: '5 / 7 / 9 / -1', marginTop: 5 },
299
+ { area: '9 / 7 / -1 / -1', marginTop: 2 },
300
+ ],
301
+ },
302
+ },
303
+ {
304
+ content: 'facts',
305
+ aliases: [{ variant: 'facts-trio' }, { variant: 'facts', arrangement: 'trio' }],
306
+ regions: {
307
+ primary: { area: '2 / 1 / 6 / -1' },
308
+ support: [
309
+ { area: '6 / 1 / -1 / 5', surface: 'light', margin: 3 },
310
+ { area: '6 / 5 / -1 / 9', surface: 'light', margin: 3 },
311
+ { area: '6 / 9 / -1 / -1', surface: 'light', margin: 3 },
312
+ ],
313
+ },
314
+ },
315
+ {
316
+ content: 'facts',
317
+ aliases: [{ variant: 'facts-featured' }, { variant: 'facts', arrangement: 'featured' }],
318
+ regions: {
319
+ primary: { area: '2 / 7 / 6 / -1', margin: 4 },
320
+ support: [
321
+ { area: '1 / 1 / -1 / 7', surface: 'color', tone: 'blue', margin: 4 },
322
+ { area: '6 / 7 / -1 / 10', surface: 'light', margin: 4 },
323
+ { area: '6 / 10 / -1 / -1', surface: 'light', margin: 4 },
324
+ ],
325
+ },
326
+ },
327
+ ]),
328
+ ]
329
+
330
+ export function resolveLayoutRecipe(layout: ThemeLayout, variant: string, arrangement = '') {
331
+ const resolvedVariant = variant.trim()
332
+ const resolvedArrangement = arrangement.trim()
333
+ const recipe = LAYOUT_RECIPES.find(candidate =>
334
+ candidate.layout === layout
335
+ && candidate.aliases.some(alias =>
336
+ alias.variant === resolvedVariant
337
+ && (alias.arrangement ?? '') === resolvedArrangement,
338
+ ),
339
+ )
340
+
341
+ if (!recipe) {
342
+ const suffix = resolvedArrangement ? `:${resolvedArrangement}` : ''
343
+ fail('Slide', `Неизвестная layout recipe для "${layout}:${resolvedVariant}${suffix}".`)
344
+ }
345
+
346
+ return recipe
347
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ normalizeRecipeRegions,
3
+ resolveLayoutRecipe,
4
+ } from './layout-recipes'
5
+
6
+ export type {
7
+ ThemeLayout,
8
+ ThemeLayoutHeader,
9
+ ThemeLayoutMarginLevel,
10
+ ThemeLayoutRole,
11
+ ThemeLayoutSlotSpec,
12
+ ThemeLayoutSpec,
13
+ ThemeLayoutSurface,
14
+ } from './layout-recipes'
15
+
16
+ import type { ThemeLayout, ThemeLayoutRole } from './layout-recipes'
17
+
18
+ function fail(label: string, message: string): never {
19
+ throw new Error(`[${label}] ${message}`)
20
+ }
21
+
22
+ export function resolveLayoutVariant(layout: ThemeLayout | '', variant: string, arrangement = '') {
23
+ if (!layout)
24
+ return variant.trim()
25
+
26
+ return resolveLayoutRecipe(layout, variant, arrangement).aliases[0].variant
27
+ }
28
+
29
+ export function resolveLayoutSpec(layout: ThemeLayout, variant: string) {
30
+ return normalizeRecipeRegions(resolveLayoutRecipe(layout, variant))
31
+ }
32
+
33
+ export function resolveLayoutSlotSpec(
34
+ layout: ThemeLayout,
35
+ variant: string,
36
+ role: ThemeLayoutRole,
37
+ index: number,
38
+ ) {
39
+ const layoutSpec = resolveLayoutSpec(layout, variant)
40
+ const roleSlots = layoutSpec.slots[role] ?? []
41
+ const resolved = roleSlots[index]
42
+
43
+ if (!resolved)
44
+ fail('Slide', `Variant "${layout}:${variant}" не поддерживает ${role}[${index}].`)
45
+
46
+ return resolved
47
+ }