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,484 @@
1
+ /**
2
+ * @typedef {'cover' | 'contain' | 'fill' | 'none' | 'scale-down'} ImageFit
3
+ */
4
+
5
+ /**
6
+ * @typedef {{
7
+ * src: string
8
+ * fit?: ImageFit
9
+ * position?: string
10
+ * anchor?: string
11
+ * x?: number | string
12
+ * y?: number | string
13
+ * zoom?: number | string
14
+ * rotate?: number | string
15
+ * color?: string
16
+ * opacity?: number | string
17
+ * backgroundColor?: string
18
+ * 'background-color'?: string
19
+ * }} ImageDataInput
20
+ */
21
+
22
+ /**
23
+ * @typedef {{
24
+ * src: string
25
+ * fit: ImageFit
26
+ * position: string
27
+ * anchor: string
28
+ * x: string
29
+ * y: string
30
+ * zoom: number
31
+ * rotate: string
32
+ * color: string
33
+ * opacity: number
34
+ * backgroundColor: string
35
+ * }} NormalizedImageData
36
+ */
37
+
38
+ /**
39
+ * @typedef {{
40
+ * src: string
41
+ * alt?: string
42
+ * fit?: ImageFit
43
+ * position?: string
44
+ * anchor?: string
45
+ * x?: number | string
46
+ * y?: number | string
47
+ * zoom?: number | string
48
+ * rotate?: number | string
49
+ * color?: string
50
+ * opacity?: number | string
51
+ * backgroundColor?: string
52
+ * 'background-color'?: string
53
+ * }} ImageConfigInput
54
+ */
55
+
56
+ /**
57
+ * @typedef {NormalizedImageData & { alt: string }} NormalizedImageConfig
58
+ */
59
+
60
+ const IMAGE_FITS = new Set(['cover', 'contain', 'fill', 'none', 'scale-down'])
61
+ const IMAGE_MASK_SIZE_BY_FIT = Object.freeze({
62
+ 'cover': 'cover',
63
+ 'contain': 'contain',
64
+ 'fill': '100% 100%',
65
+ 'none': 'auto',
66
+ 'scale-down': 'contain',
67
+ })
68
+
69
+ const DEFAULT_IMAGE_CONFIG = Object.freeze({
70
+ alt: '',
71
+ fit: 'cover',
72
+ position: 'center',
73
+ })
74
+
75
+ const DEFAULT_IMAGE_DATA = Object.freeze({
76
+ fit: 'cover',
77
+ position: 'center',
78
+ anchor: '',
79
+ x: 0,
80
+ y: 0,
81
+ zoom: 1,
82
+ rotate: 0,
83
+ color: '',
84
+ opacity: 1,
85
+ backgroundColor: '',
86
+ })
87
+
88
+ export const IMAGE_DATA_FIELDS = Object.freeze([
89
+ 'fit',
90
+ 'position',
91
+ 'anchor',
92
+ 'x',
93
+ 'y',
94
+ 'zoom',
95
+ 'rotate',
96
+ 'color',
97
+ 'opacity',
98
+ 'backgroundColor',
99
+ ])
100
+
101
+ export const IMAGE_SOURCE_FIELDS = Object.freeze([
102
+ 'src',
103
+ ...IMAGE_DATA_FIELDS,
104
+ ])
105
+
106
+ /**
107
+ * @param {unknown} value
108
+ * @returns {value is Record<string, unknown>}
109
+ */
110
+ function isPlainObject(value) {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
112
+ }
113
+
114
+ /**
115
+ * @param {unknown} input
116
+ * @returns {Partial<ImageDataInput>}
117
+ */
118
+ export function pickImageDataFields(input) {
119
+ if (!isPlainObject(input))
120
+ return {}
121
+
122
+ const fields = new Set(IMAGE_DATA_FIELDS)
123
+ return Object.fromEntries(
124
+ Object.entries(input)
125
+ .flatMap(([key, value]) => {
126
+ if (value === undefined)
127
+ return []
128
+
129
+ if (fields.has(key))
130
+ return [[key, value]]
131
+
132
+ if (key === 'background-color')
133
+ return [['backgroundColor', value]]
134
+
135
+ return []
136
+ }),
137
+ )
138
+ }
139
+
140
+ /**
141
+ * @param {unknown} value
142
+ * @returns {value is ImageFit}
143
+ */
144
+ export function isImageFit(value) {
145
+ return typeof value === 'string' && IMAGE_FITS.has(value)
146
+ }
147
+
148
+ /**
149
+ * @param {unknown} value
150
+ * @returns {string}
151
+ */
152
+ export function normalizeImagePosition(value) {
153
+ if (typeof value !== 'string')
154
+ return DEFAULT_IMAGE_CONFIG.position
155
+
156
+ const normalized = value.trim().replace(/\s+/g, ' ')
157
+
158
+ return normalized || DEFAULT_IMAGE_CONFIG.position
159
+ }
160
+
161
+ /**
162
+ * @param {string} position
163
+ * @returns {string}
164
+ */
165
+ export function normalizeImageTransformOrigin(position) {
166
+ const normalized = normalizeImagePosition(position)
167
+ const tokens = normalized.split(' ')
168
+
169
+ if (tokens.length === 1) {
170
+ if (tokens[0] === 'top' || tokens[0] === 'bottom')
171
+ return `center ${tokens[0]}`
172
+
173
+ return `${tokens[0]} center`
174
+ }
175
+
176
+ if (tokens.length === 2)
177
+ return normalized
178
+
179
+ return 'center'
180
+ }
181
+
182
+ /**
183
+ * @param {unknown} value
184
+ * @returns {boolean}
185
+ */
186
+ export function isSvgImageSource(value) {
187
+ if (typeof value !== 'string')
188
+ return false
189
+
190
+ const normalized = value.trim().split(/[?#]/u, 1)[0]?.toLowerCase() ?? ''
191
+ return normalized.endsWith('.svg')
192
+ }
193
+
194
+ /**
195
+ * @param {unknown} value
196
+ * @param {string} label
197
+ * @returns {ImageFit}
198
+ */
199
+ function normalizeImageFit(value, label) {
200
+ const normalized = typeof value === 'string' && value.trim()
201
+ ? value.trim()
202
+ : DEFAULT_IMAGE_CONFIG.fit
203
+
204
+ if (!isImageFit(normalized))
205
+ throw new Error(`[${label}] image fit "${String(value)}" is not supported.`)
206
+
207
+ return normalized
208
+ }
209
+
210
+ /**
211
+ * @param {unknown} value
212
+ * @returns {string}
213
+ */
214
+ export function resolveImageMaskSize(value) {
215
+ const fit = normalizeImageFit(value, 'image.fit')
216
+ return IMAGE_MASK_SIZE_BY_FIT[fit]
217
+ }
218
+
219
+ /**
220
+ * @param {unknown} value
221
+ * @param {string} label
222
+ */
223
+ function normalizeImageZoom(value, label) {
224
+ if (value === undefined || value === null || value === '')
225
+ return DEFAULT_IMAGE_DATA.zoom
226
+
227
+ const normalized = Number(value)
228
+
229
+ if (!Number.isFinite(normalized) || normalized <= 0)
230
+ throw new Error(`[${label}] image zoom must be a positive number.`)
231
+
232
+ return normalized
233
+ }
234
+
235
+ /**
236
+ * @param {unknown} value
237
+ * @param {string} label
238
+ */
239
+ function normalizeImageRotate(value, label) {
240
+ if (value === undefined || value === null || value === '')
241
+ return '0deg'
242
+
243
+ if (typeof value === 'number') {
244
+ if (!Number.isFinite(value))
245
+ throw new Error(`[${label}] image rotate must be a finite number or CSS angle.`)
246
+
247
+ return `${value}deg`
248
+ }
249
+
250
+ if (typeof value !== 'string')
251
+ throw new Error(`[${label}] image rotate must be a finite number or CSS angle.`)
252
+
253
+ const normalized = value.trim()
254
+ if (!normalized)
255
+ return '0deg'
256
+
257
+ return /^[-+]?(?:\d+|\d*\.\d+)$/.test(normalized) ? `${normalized}deg` : normalized
258
+ }
259
+
260
+ /**
261
+ * @param {unknown} value
262
+ * @param {string} label
263
+ */
264
+ function normalizeImageOffset(value, label) {
265
+ if (value === undefined || value === null || value === '')
266
+ return '0px'
267
+
268
+ if (typeof value === 'number') {
269
+ if (!Number.isFinite(value))
270
+ throw new Error(`[${label}] image offset must be a finite number or CSS length.`)
271
+
272
+ return `${value}px`
273
+ }
274
+
275
+ if (typeof value !== 'string')
276
+ throw new Error(`[${label}] image offset must be a finite number or CSS length.`)
277
+
278
+ const normalized = value.trim()
279
+ return normalized || '0px'
280
+ }
281
+
282
+ /**
283
+ * @param {unknown} value
284
+ * @param {string} position
285
+ */
286
+ function normalizeImageAnchor(value, position) {
287
+ if (typeof value !== 'string')
288
+ return normalizeImageTransformOrigin(position)
289
+
290
+ const normalized = value.trim().replace(/\s+/g, ' ')
291
+ return normalized || normalizeImageTransformOrigin(position)
292
+ }
293
+
294
+ /**
295
+ * @param {unknown} value
296
+ */
297
+ function normalizeImageColor(value) {
298
+ if (typeof value !== 'string')
299
+ return DEFAULT_IMAGE_DATA.color
300
+
301
+ return value.trim()
302
+ }
303
+
304
+ /**
305
+ * @param {unknown} value
306
+ */
307
+ function normalizeImageBackgroundColor(value) {
308
+ if (typeof value !== 'string')
309
+ return DEFAULT_IMAGE_DATA.backgroundColor
310
+
311
+ return value.trim()
312
+ }
313
+
314
+ /**
315
+ * @param {unknown} value
316
+ * @param {string} label
317
+ */
318
+ function normalizeImageOpacity(value, label) {
319
+ if (value === undefined || value === null || value === '')
320
+ return DEFAULT_IMAGE_DATA.opacity
321
+
322
+ const normalized = Number(value)
323
+
324
+ if (!Number.isFinite(normalized) || normalized < 0 || normalized > 1)
325
+ throw new Error(`[${label}] image opacity must be a number between 0 and 1.`)
326
+
327
+ return normalized
328
+ }
329
+
330
+ /**
331
+ * @param {unknown} value
332
+ * @param {string} label
333
+ */
334
+ function normalizeImageSource(value, label) {
335
+ if (typeof value !== 'string' || !value.trim())
336
+ throw new Error(`[${label}] image src must be a non-empty string.`)
337
+
338
+ return value.trim()
339
+ }
340
+
341
+ /**
342
+ * @param {unknown} value
343
+ * @param {string} label
344
+ */
345
+ function assertNoLegacyScale(value, label) {
346
+ if (!isPlainObject(value))
347
+ return
348
+
349
+ if (Object.prototype.hasOwnProperty.call(value, 'scale'))
350
+ throw new Error(`[${label}] image scale is no longer supported; use zoom instead.`)
351
+ }
352
+
353
+ /**
354
+ * @param {string} input
355
+ * @param {string} label
356
+ * @returns {{ src: string } | null}
357
+ */
358
+ function parseImageString(input, label) {
359
+ const normalized = input.trim()
360
+
361
+ if (!normalized)
362
+ return null
363
+
364
+ const tokens = normalized.split(/\s+/)
365
+
366
+ if (tokens.length > 1 && isImageFit(tokens[1])) {
367
+ throw new Error(
368
+ `[${label}] image shorthand is no longer supported. Use object syntax with src, fit, position, anchor, x, y, zoom, rotate, and backgroundColor.`,
369
+ )
370
+ }
371
+
372
+ return { src: normalized }
373
+ }
374
+
375
+ /**
376
+ * @param {ImageDataInput} input
377
+ * @param {Partial<NormalizedImageData>} [defaults]
378
+ * @param {string} [label]
379
+ * @returns {NormalizedImageData}
380
+ */
381
+ export function normalizeImageData(input, defaults = {}, label = 'image') {
382
+ if (!isPlainObject(input))
383
+ throw new Error(`[${label}] image data must be a plain object.`)
384
+
385
+ assertNoLegacyScale(defaults, label)
386
+ assertNoLegacyScale(input, label)
387
+
388
+ const merged = {
389
+ ...DEFAULT_IMAGE_DATA,
390
+ ...defaults,
391
+ ...input,
392
+ }
393
+
394
+ const position = normalizeImagePosition(merged.position)
395
+ const zoom = normalizeImageZoom(merged.zoom, label)
396
+ const rotate = normalizeImageRotate(merged.rotate, label)
397
+ const backgroundColor = normalizeImageBackgroundColor(
398
+ Object.prototype.hasOwnProperty.call(merged, 'background-color')
399
+ ? merged['background-color']
400
+ : merged.backgroundColor,
401
+ )
402
+
403
+ return {
404
+ src: normalizeImageSource(merged.src, label),
405
+ fit: normalizeImageFit(merged.fit, label),
406
+ position,
407
+ anchor: normalizeImageAnchor(merged.anchor, position),
408
+ x: normalizeImageOffset(merged.x, label),
409
+ y: normalizeImageOffset(merged.y, label),
410
+ zoom,
411
+ rotate,
412
+ color: normalizeImageColor(merged.color),
413
+ opacity: normalizeImageOpacity(merged.opacity, label),
414
+ backgroundColor,
415
+ }
416
+ }
417
+
418
+ /**
419
+ * @param {string | ImageDataInput | Array<string | ImageDataInput> | null | undefined} input
420
+ * @param {Partial<NormalizedImageData>} [defaults]
421
+ * @param {string} [label]
422
+ * @returns {NormalizedImageData[]}
423
+ */
424
+ export function parseImageBackgrounds(input, defaults = {}, label = 'image') {
425
+ if (input === undefined || input === null || input === '')
426
+ return []
427
+
428
+ const items = Array.isArray(input) ? input : [input]
429
+
430
+ return items.flatMap((item, index) => {
431
+ if (item === undefined || item === null || item === '')
432
+ return []
433
+
434
+ let raw
435
+
436
+ if (typeof item === 'string')
437
+ raw = parseImageString(item, `${label}[${index}]`)
438
+ else if (isPlainObject(item))
439
+ raw = item
440
+ else
441
+ throw new Error(`[${label}[${index}]] image background must be a string or plain object.`)
442
+
443
+ if (!raw)
444
+ return []
445
+
446
+ return [normalizeImageData(raw, defaults, `${label}[${index}]`)]
447
+ })
448
+ }
449
+
450
+ /**
451
+ * @param {string | ImageConfigInput | null | undefined} input
452
+ * @param {Partial<NormalizedImageConfig>} [defaults]
453
+ * @param {string} [label]
454
+ * @returns {NormalizedImageConfig | null}
455
+ */
456
+ export function parseImageConfig(input, defaults = {}, label = 'image') {
457
+ if (input === undefined || input === null || input === '')
458
+ return null
459
+
460
+ let raw
461
+
462
+ if (typeof input === 'string')
463
+ raw = parseImageString(input, label)
464
+ else if (isPlainObject(input))
465
+ raw = input
466
+ else
467
+ throw new Error(`[${label}] image config must be a string or plain object.`)
468
+
469
+ if (!raw)
470
+ return null
471
+
472
+ const imageData = normalizeImageData(raw, defaults, label)
473
+ const merged = {
474
+ ...DEFAULT_IMAGE_CONFIG,
475
+ ...defaults,
476
+ ...raw,
477
+ }
478
+
479
+ return {
480
+ ...imageData,
481
+ src: imageData.src,
482
+ alt: typeof merged.alt === 'string' ? merged.alt : DEFAULT_IMAGE_CONFIG.alt,
483
+ }
484
+ }