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,396 @@
1
+ export const THEME_GRID_COLUMNS = 12
2
+ export const THEME_GRID_ROWS = 12
3
+
4
+ export type GridPlacementInput = {
5
+ area?: string
6
+ col?: string
7
+ row?: string
8
+ label?: string
9
+ }
10
+
11
+ export type GridRect = {
12
+ colStart: number
13
+ colEnd: number
14
+ rowStart: number
15
+ rowEnd: number
16
+ }
17
+
18
+ export type GridPlacementStyle = {
19
+ gridColumn: string
20
+ gridRow: string
21
+ }
22
+
23
+ export type GridPlacedItem = GridRect & {
24
+ id: string
25
+ label: string
26
+ }
27
+
28
+ export type GridCursor = {
29
+ row: number
30
+ col: number
31
+ }
32
+
33
+ export type GridPlacementResult = {
34
+ rect: GridRect
35
+ style: GridPlacementStyle
36
+ nextCursor: GridCursor
37
+ }
38
+
39
+ type ParsedGridLine =
40
+ | { kind: 'auto' }
41
+ | { kind: 'line'; value: number }
42
+ | { kind: 'span'; value: number }
43
+
44
+ type ParsedAxisPlacement = {
45
+ raw: string
46
+ start: ParsedGridLine
47
+ end: ParsedGridLine
48
+ }
49
+
50
+ type ParsedGridPlacement = {
51
+ col: ParsedAxisPlacement
52
+ row: ParsedAxisPlacement
53
+ }
54
+
55
+ type AxisResolution = {
56
+ explicit: boolean
57
+ start?: number
58
+ end?: number
59
+ span: number
60
+ }
61
+
62
+ const INTEGER_PATTERN = /^-?\d+$/
63
+ const SPAN_PATTERN = /^span\s+(\d+)$/i
64
+
65
+ function fail(label: string, message: string): never {
66
+ throw new Error(`[${label}] ${message}`)
67
+ }
68
+
69
+ function parseGridLine(rawValue: string, label: string, axis: 'row' | 'col'): ParsedGridLine {
70
+ const raw = rawValue.trim()
71
+
72
+ if (!raw)
73
+ fail(label, `Пустое значение ${axis}.`)
74
+
75
+ if (raw === 'auto')
76
+ return { kind: 'auto' }
77
+
78
+ const spanMatch = raw.match(SPAN_PATTERN)
79
+ if (spanMatch) {
80
+ const value = Number(spanMatch[1])
81
+ if (!Number.isInteger(value) || value <= 0)
82
+ fail(label, `Неверный span в ${axis}: "${raw}".`)
83
+
84
+ return { kind: 'span', value }
85
+ }
86
+
87
+ if (INTEGER_PATTERN.test(raw)) {
88
+ const value = Number(raw)
89
+ if (!Number.isInteger(value) || value === 0)
90
+ fail(label, `Линия грида в ${axis} должна быть целым числом, кроме 0: "${raw}".`)
91
+
92
+ return { kind: 'line', value }
93
+ }
94
+
95
+ fail(
96
+ label,
97
+ `Публичный low-level API пока не поддерживает named lines. Используйте числа, отрицательные линии, auto и span: "${raw}".`,
98
+ )
99
+ }
100
+
101
+ function parseAxisPlacement(rawValue: string, label: string, axis: 'row' | 'col'): ParsedAxisPlacement {
102
+ const parts = rawValue
103
+ .split('/')
104
+ .map(part => part.trim())
105
+
106
+ if (parts.some(part => !part))
107
+ fail(label, `${axis} содержит пустую часть: "${rawValue}".`)
108
+
109
+ if (parts.length === 0 || parts.length > 2)
110
+ fail(label, `${axis} должен содержать одну или две части, разделённые "/": "${rawValue}".`)
111
+
112
+ const start = parseGridLine(parts[0], label, axis)
113
+ const end = parseGridLine(parts[1] ?? 'auto', label, axis)
114
+
115
+ if (start.kind === 'span' && end.kind === 'span')
116
+ fail(label, `${axis} не может одновременно использовать span в start и end: "${rawValue}".`)
117
+
118
+ return {
119
+ raw: rawValue.trim(),
120
+ start,
121
+ end,
122
+ }
123
+ }
124
+
125
+ function parseAreaPlacement(rawValue: string, label: string): ParsedGridPlacement {
126
+ const parts = rawValue
127
+ .split('/')
128
+ .map(part => part.trim())
129
+
130
+ if (parts.some(part => !part))
131
+ fail(label, `area содержит пустую часть: "${rawValue}".`)
132
+
133
+ if (parts.length !== 4) {
134
+ fail(
135
+ label,
136
+ `area должен содержать ровно четыре части в формате "row-start / col-start / row-end / col-end": "${rawValue}".`,
137
+ )
138
+ }
139
+
140
+ const [rowStart, colStart, rowEnd, colEnd] = parts
141
+
142
+ return {
143
+ row: parseAxisPlacement(`${rowStart} / ${rowEnd}`, label, 'row'),
144
+ col: parseAxisPlacement(`${colStart} / ${colEnd}`, label, 'col'),
145
+ }
146
+ }
147
+
148
+ function parseGridPlacement(input: GridPlacementInput, label: string): ParsedGridPlacement {
149
+ const area = input.area?.trim()
150
+ const col = input.col?.trim()
151
+ const row = input.row?.trim()
152
+
153
+ if (area && (col || row))
154
+ fail(label, 'Используйте либо area, либо col/row, но не оба варианта сразу.')
155
+
156
+ if (area)
157
+ return parseAreaPlacement(area, label)
158
+
159
+ return {
160
+ col: parseAxisPlacement(col || 'auto', label, 'col'),
161
+ row: parseAxisPlacement(row || 'auto', label, 'row'),
162
+ }
163
+ }
164
+
165
+ function normalizeLine(value: number, trackCount: number, label: string, axis: 'row' | 'col') {
166
+ const lineCount = trackCount + 1
167
+
168
+ if (value > 0) {
169
+ if (value < 1 || value > lineCount)
170
+ fail(label, `${axis} использует линию ${value}, но доступен диапазон 1..${lineCount}.`)
171
+
172
+ return value
173
+ }
174
+
175
+ const resolved = lineCount + 1 + value
176
+ if (resolved < 1 || resolved > lineCount)
177
+ fail(label, `${axis} использует отрицательную линию ${value}, которая выходит за диапазон 1..${lineCount}.`)
178
+
179
+ return resolved
180
+ }
181
+
182
+ function resolveAxis(
183
+ placement: ParsedAxisPlacement,
184
+ trackCount: number,
185
+ axis: 'row' | 'col',
186
+ label: string,
187
+ ): AxisResolution {
188
+ const startLine = placement.start.kind === 'line'
189
+ ? normalizeLine(placement.start.value, trackCount, label, axis)
190
+ : undefined
191
+
192
+ const endLine = placement.end.kind === 'line'
193
+ ? normalizeLine(placement.end.value, trackCount, label, axis)
194
+ : undefined
195
+
196
+ const span = placement.start.kind === 'span'
197
+ ? placement.start.value
198
+ : placement.end.kind === 'span'
199
+ ? placement.end.value
200
+ : 1
201
+
202
+ if (span > trackCount)
203
+ fail(label, `${axis} использует span ${span}, но в сетке только ${trackCount} треков.`)
204
+
205
+ if (startLine !== undefined && endLine !== undefined) {
206
+ if (endLine <= startLine)
207
+ fail(label, `${axis} должен заканчиваться после start: "${placement.raw}".`)
208
+
209
+ return {
210
+ explicit: true,
211
+ start: startLine,
212
+ end: endLine,
213
+ span: endLine - startLine,
214
+ }
215
+ }
216
+
217
+ if (startLine !== undefined) {
218
+ const end = placement.end.kind === 'span' ? startLine + placement.end.value : startLine + 1
219
+ if (end > trackCount + 1)
220
+ fail(label, `${axis} выходит за пределы сетки: "${placement.raw}".`)
221
+
222
+ return {
223
+ explicit: true,
224
+ start: startLine,
225
+ end,
226
+ span: end - startLine,
227
+ }
228
+ }
229
+
230
+ if (endLine !== undefined) {
231
+ const start = placement.start.kind === 'span' ? endLine - placement.start.value : endLine - 1
232
+ if (start < 1)
233
+ fail(label, `${axis} выходит за пределы сетки: "${placement.raw}".`)
234
+
235
+ return {
236
+ explicit: true,
237
+ start,
238
+ end: endLine,
239
+ span: endLine - start,
240
+ }
241
+ }
242
+
243
+ return {
244
+ explicit: false,
245
+ span,
246
+ }
247
+ }
248
+
249
+ function intersects(a: GridRect, b: GridRect) {
250
+ return a.colStart < b.colEnd
251
+ && a.colEnd > b.colStart
252
+ && a.rowStart < b.rowEnd
253
+ && a.rowEnd > b.rowStart
254
+ }
255
+
256
+ function ensureNoOverlap(rect: GridRect, occupied: GridPlacedItem[], label: string) {
257
+ for (const other of occupied) {
258
+ if (!intersects(rect, other))
259
+ continue
260
+
261
+ fail(
262
+ label,
263
+ `Слот пересекается с "${other.label}" в зоне col ${other.colStart}/${other.colEnd}, row ${other.rowStart}/${other.rowEnd}. На первом этапе overlap запрещён.`,
264
+ )
265
+ }
266
+ }
267
+
268
+ function fits(occupied: GridPlacedItem[], rect: GridRect) {
269
+ if (rect.colStart < 1 || rect.colEnd > THEME_GRID_COLUMNS + 1)
270
+ return false
271
+
272
+ if (rect.rowStart < 1 || rect.rowEnd > THEME_GRID_ROWS + 1)
273
+ return false
274
+
275
+ return !occupied.some(other => intersects(rect, other))
276
+ }
277
+
278
+ function findRowMajorPlacement(
279
+ occupied: GridPlacedItem[],
280
+ colSpan: number,
281
+ rowSpan: number,
282
+ cursor: GridCursor,
283
+ ) {
284
+ for (let row = cursor.row; row <= THEME_GRID_ROWS - rowSpan + 1; row++) {
285
+ const startCol = row === cursor.row ? cursor.col : 1
286
+ for (let col = startCol; col <= THEME_GRID_COLUMNS - colSpan + 1; col++) {
287
+ const rect = {
288
+ colStart: col,
289
+ colEnd: col + colSpan,
290
+ rowStart: row,
291
+ rowEnd: row + rowSpan,
292
+ }
293
+
294
+ if (fits(occupied, rect))
295
+ return rect
296
+ }
297
+ }
298
+
299
+ for (let row = 1; row <= THEME_GRID_ROWS - rowSpan + 1; row++) {
300
+ for (let col = 1; col <= THEME_GRID_COLUMNS - colSpan + 1; col++) {
301
+ const rect = {
302
+ colStart: col,
303
+ colEnd: col + colSpan,
304
+ rowStart: row,
305
+ rowEnd: row + rowSpan,
306
+ }
307
+
308
+ if (fits(occupied, rect))
309
+ return rect
310
+ }
311
+ }
312
+
313
+ return null
314
+ }
315
+
316
+ function advanceCursor(rect: GridRect): GridCursor {
317
+ const col = rect.colEnd > THEME_GRID_COLUMNS ? 1 : rect.colEnd
318
+ const row = rect.colEnd > THEME_GRID_COLUMNS ? rect.rowStart + 1 : rect.rowStart
319
+
320
+ return { row, col }
321
+ }
322
+
323
+ function placementStyle(rect: GridRect): GridPlacementStyle {
324
+ return {
325
+ gridColumn: `${rect.colStart} / ${rect.colEnd}`,
326
+ gridRow: `${rect.rowStart} / ${rect.rowEnd}`,
327
+ }
328
+ }
329
+
330
+ export function resolveGridPlacement(input: GridPlacementInput, options: {
331
+ occupied: GridPlacedItem[]
332
+ cursor?: GridCursor
333
+ label?: string
334
+ }): GridPlacementResult {
335
+ const label = options.label?.trim() || input.label?.trim() || 'Slot'
336
+ const placement = parseGridPlacement(input, label)
337
+ const cursor = options.cursor ?? { row: 1, col: 1 }
338
+
339
+ const col = resolveAxis(placement.col, THEME_GRID_COLUMNS, 'col', label)
340
+ const row = resolveAxis(placement.row, THEME_GRID_ROWS, 'row', label)
341
+
342
+ let rect: GridRect | null = null
343
+
344
+ if (col.explicit && row.explicit) {
345
+ rect = {
346
+ colStart: col.start!,
347
+ colEnd: col.end!,
348
+ rowStart: row.start!,
349
+ rowEnd: row.end!,
350
+ }
351
+ }
352
+ else if (col.explicit) {
353
+ for (let rowStart = 1; rowStart <= THEME_GRID_ROWS - row.span + 1; rowStart++) {
354
+ const candidate = {
355
+ colStart: col.start!,
356
+ colEnd: col.end!,
357
+ rowStart,
358
+ rowEnd: rowStart + row.span,
359
+ }
360
+
361
+ if (fits(options.occupied, candidate)) {
362
+ rect = candidate
363
+ break
364
+ }
365
+ }
366
+ }
367
+ else if (row.explicit) {
368
+ for (let colStart = 1; colStart <= THEME_GRID_COLUMNS - col.span + 1; colStart++) {
369
+ const candidate = {
370
+ colStart,
371
+ colEnd: colStart + col.span,
372
+ rowStart: row.start!,
373
+ rowEnd: row.end!,
374
+ }
375
+
376
+ if (fits(options.occupied, candidate)) {
377
+ rect = candidate
378
+ break
379
+ }
380
+ }
381
+ }
382
+ else {
383
+ rect = findRowMajorPlacement(options.occupied, col.span, row.span, cursor)
384
+ }
385
+
386
+ if (!rect)
387
+ fail(label, 'Не удалось разместить слот внутри сетки без пересечений.')
388
+
389
+ ensureNoOverlap(rect, options.occupied, label)
390
+
391
+ return {
392
+ rect,
393
+ style: placementStyle(rect),
394
+ nextCursor: advanceCursor(rect),
395
+ }
396
+ }