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.
- package/README.md +270 -0
- package/components/DebugGrid.vue +50 -0
- package/components/DecorLibrary.vue +1117 -0
- package/components/Header.vue +124 -0
- package/components/Image.vue +82 -0
- package/components/ImageRenderer.vue +126 -0
- package/components/Logo.vue +142 -0
- package/components/Person.vue +89 -0
- package/components/Slide.vue +324 -0
- package/components/Slot.vue +419 -0
- package/components/Text.vue +216 -0
- package/components/Timeline.vue +202 -0
- package/composables/decor-catalog.mjs +339 -0
- package/composables/decor-config.mjs +488 -0
- package/composables/decor-file-store.ts +192 -0
- package/composables/decor-http-store.ts +25 -0
- package/composables/decor-store.ts +39 -0
- package/composables/decor-tuning-overrides.mjs +2 -0
- package/composables/decor-workbench.ts +799 -0
- package/composables/grid-placement.ts +396 -0
- package/composables/image-config.mjs +484 -0
- package/composables/layout-authoring.ts +372 -0
- package/composables/layout-markdown.mjs +56 -0
- package/composables/layout-recipes.ts +347 -0
- package/composables/layout-registry.ts +47 -0
- package/composables/layout-shorthands.ts +891 -0
- package/composables/layout-vnode.ts +43 -0
- package/composables/logo-mark.mjs +5 -0
- package/composables/slide-layout.ts +43 -0
- package/composables/slot-placement.ts +195 -0
- package/composables/slot-rules.ts +147 -0
- package/composables/text-fit-runtime.ts +577 -0
- package/composables/text-priority-fit.mjs +166 -0
- package/composables/text-typography.mjs +138 -0
- package/composables/theme-foundation.ts +192 -0
- package/composables/theme-media.mjs +202 -0
- package/composables/use-theme-config.ts +71 -0
- package/decor-library.md +10 -0
- package/env.d.ts +6 -0
- package/example.md +786 -0
- package/layouts/collection.vue +5 -0
- package/layouts/cover.vue +5 -0
- package/layouts/default.vue +5 -0
- package/layouts/explainer.vue +5 -0
- package/layouts/message.vue +5 -0
- package/layouts/none.vue +5 -0
- package/layouts/section.vue +5 -0
- package/layouts/two-cols.vue +11 -0
- package/package.json +84 -0
- package/public/decor/decor-1.svg +83 -0
- package/public/decor/decor-10.svg +40 -0
- package/public/decor/decor-11.svg +162 -0
- package/public/decor/decor-12.svg +83 -0
- package/public/decor/decor-13.svg +5 -0
- package/public/decor/decor-14.svg +7 -0
- package/public/decor/decor-15.svg +10 -0
- package/public/decor/decor-16.svg +9 -0
- package/public/decor/decor-17.png +0 -0
- package/public/decor/decor-18.png +0 -0
- package/public/decor/decor-19.svg +5 -0
- package/public/decor/decor-2.png +0 -0
- package/public/decor/decor-3.svg +9 -0
- package/public/decor/decor-4.svg +31 -0
- package/public/decor/decor-5.png +0 -0
- package/public/decor/decor-6.png +0 -0
- package/public/decor/decor-7.png +0 -0
- package/public/decor/decor-8.svg +21 -0
- package/public/decor/decor-9.svg +38 -0
- package/public/favicon.svg +11 -0
- 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/setup/main.ts +3 -0
- package/setup/shiki.ts +10 -0
- package/setup/vite-plugins.ts +106 -0
- package/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +102 -0
- package/styles/example.css +0 -0
- package/styles/index.css +192 -0
- package/styles/vars.css +119 -0
- 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
|
+
}
|