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,799 @@
|
|
|
1
|
+
import { computed, reactive, ref, watch } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
DecorImage,
|
|
4
|
+
DecorSize,
|
|
5
|
+
DecorStore,
|
|
6
|
+
DecorVariant,
|
|
7
|
+
ImageFit,
|
|
8
|
+
RatioRange,
|
|
9
|
+
SizeRange,
|
|
10
|
+
} from './decor-store'
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
DecorImage,
|
|
14
|
+
DecorSize,
|
|
15
|
+
DecorStore,
|
|
16
|
+
DecorVariant,
|
|
17
|
+
ImageFit,
|
|
18
|
+
RatioRange,
|
|
19
|
+
SizeRange,
|
|
20
|
+
} from './decor-store'
|
|
21
|
+
|
|
22
|
+
export type DecorSizeDraft = {
|
|
23
|
+
colsFrom: number
|
|
24
|
+
colsTo: number
|
|
25
|
+
rowsFrom: number
|
|
26
|
+
rowsTo: number
|
|
27
|
+
ratioFrom: number | null
|
|
28
|
+
ratioTo: number | null
|
|
29
|
+
}
|
|
30
|
+
export type DecorDraft = {
|
|
31
|
+
cols: number
|
|
32
|
+
rows: number
|
|
33
|
+
sizes: DecorSizeDraft[]
|
|
34
|
+
fit: ImageFit
|
|
35
|
+
positionX: number
|
|
36
|
+
positionY: number
|
|
37
|
+
anchor: string
|
|
38
|
+
offsetX: number
|
|
39
|
+
offsetY: number
|
|
40
|
+
zoom: number
|
|
41
|
+
rotate: number
|
|
42
|
+
opacity: number
|
|
43
|
+
color: string
|
|
44
|
+
backgroundColor: string
|
|
45
|
+
}
|
|
46
|
+
export type DecorPreviewFrame = {
|
|
47
|
+
key: string
|
|
48
|
+
cols: number
|
|
49
|
+
rows: number
|
|
50
|
+
label: string
|
|
51
|
+
detail: string
|
|
52
|
+
sizeIndex: number
|
|
53
|
+
}
|
|
54
|
+
export type DecorSaveToken = {
|
|
55
|
+
selectedId: string
|
|
56
|
+
version: number
|
|
57
|
+
}
|
|
58
|
+
type DecorFrameCandidate = {
|
|
59
|
+
cols: number
|
|
60
|
+
rows: number
|
|
61
|
+
area: number
|
|
62
|
+
ratio: number
|
|
63
|
+
score: number
|
|
64
|
+
}
|
|
65
|
+
type DecorPreviewStyle = {
|
|
66
|
+
gridColumn: string
|
|
67
|
+
gridRow: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const EMPTY_DECOR: DecorVariant = Object.freeze({
|
|
71
|
+
id: '',
|
|
72
|
+
src: '',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export class StaleDecorSaveError extends Error {
|
|
76
|
+
constructor() {
|
|
77
|
+
super('Decor save is stale')
|
|
78
|
+
this.name = 'StaleDecorSaveError'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isStaleDecorSaveError(error: unknown): error is StaleDecorSaveError {
|
|
83
|
+
return error instanceof StaleDecorSaveError
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function clamp(value: number, min: number, max: number) {
|
|
87
|
+
return Math.min(Math.max(value, min), max)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatNumber(value: number, precision = 2) {
|
|
91
|
+
const normalized = Number(value.toFixed(precision))
|
|
92
|
+
return Object.is(normalized, -0) ? 0 : normalized
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeGridValue(value: unknown, fallback: number) {
|
|
96
|
+
const normalized = Number(value)
|
|
97
|
+
if (!Number.isFinite(normalized))
|
|
98
|
+
return fallback
|
|
99
|
+
|
|
100
|
+
return clamp(Math.round(normalized), 1, 12)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizeGridRange(from: unknown, to: unknown, fallback: number): [number, number] {
|
|
104
|
+
const normalizedFrom = normalizeGridValue(from, fallback)
|
|
105
|
+
const normalizedTo = normalizeGridValue(to, normalizedFrom)
|
|
106
|
+
|
|
107
|
+
return normalizedFrom <= normalizedTo
|
|
108
|
+
? [normalizedFrom, normalizedTo]
|
|
109
|
+
: [normalizedTo, normalizedFrom]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeRatioValue(value: unknown) {
|
|
113
|
+
const normalized = Number(value)
|
|
114
|
+
|
|
115
|
+
if (!Number.isFinite(normalized) || normalized <= 0)
|
|
116
|
+
return null
|
|
117
|
+
|
|
118
|
+
return clamp(formatNumber(normalized, 2), 0.08, 12)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function normalizeRatioRange(from: unknown, to: unknown): [number | null, number | null] {
|
|
122
|
+
const normalizedFrom = normalizeRatioValue(from)
|
|
123
|
+
const normalizedTo = normalizeRatioValue(to)
|
|
124
|
+
|
|
125
|
+
if (normalizedFrom === null && normalizedTo === null)
|
|
126
|
+
return [null, null]
|
|
127
|
+
|
|
128
|
+
const fromValue = normalizedFrom ?? normalizedTo ?? 1
|
|
129
|
+
const toValue = normalizedTo ?? normalizedFrom ?? 1
|
|
130
|
+
|
|
131
|
+
return fromValue <= toValue
|
|
132
|
+
? [fromValue, toValue]
|
|
133
|
+
: [toValue, fromValue]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rangeBounds(value: SizeRange | undefined, fallback: number): [number, number] {
|
|
137
|
+
if (typeof value === 'number') {
|
|
138
|
+
const normalized = normalizeGridValue(value, fallback)
|
|
139
|
+
return [normalized, normalized]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!Array.isArray(value))
|
|
143
|
+
return [fallback, fallback]
|
|
144
|
+
|
|
145
|
+
return normalizeGridRange(value[0], value[1], fallback)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ratioBounds(value: RatioRange | undefined): [number | null, number | null] {
|
|
149
|
+
if (typeof value === 'number') {
|
|
150
|
+
const normalized = normalizeRatioValue(value)
|
|
151
|
+
return [normalized, normalized]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!Array.isArray(value))
|
|
155
|
+
return [null, null]
|
|
156
|
+
|
|
157
|
+
return normalizeRatioRange(value[0], value[1])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rangePreviewValue(value: SizeRange | undefined, fallback: number) {
|
|
161
|
+
const [from, to] = rangeBounds(value, fallback)
|
|
162
|
+
return normalizeGridValue((from + to) / 2, fallback)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createSizeDraft(size: DecorSize | undefined, fallbackCols = 4, fallbackRows = 4): DecorSizeDraft {
|
|
166
|
+
const [colsFrom, colsTo] = rangeBounds(size?.cols, fallbackCols)
|
|
167
|
+
const [rowsFrom, rowsTo] = rangeBounds(size?.rows, fallbackRows)
|
|
168
|
+
const [ratioFrom, ratioTo] = ratioBounds(size?.ratio ?? size?.aspectRatio)
|
|
169
|
+
|
|
170
|
+
return { colsFrom, colsTo, rowsFrom, rowsTo, ratioFrom, ratioTo }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function normalizeSizeDraft(size: DecorSizeDraft) {
|
|
174
|
+
const [colsFrom, colsTo] = normalizeGridRange(size.colsFrom, size.colsTo, 4)
|
|
175
|
+
const [rowsFrom, rowsTo] = normalizeGridRange(size.rowsFrom, size.rowsTo, 4)
|
|
176
|
+
const [ratioFrom, ratioTo] = normalizeRatioRange(size.ratioFrom, size.ratioTo)
|
|
177
|
+
|
|
178
|
+
size.colsFrom = colsFrom
|
|
179
|
+
size.colsTo = colsTo
|
|
180
|
+
size.rowsFrom = rowsFrom
|
|
181
|
+
size.rowsTo = rowsTo
|
|
182
|
+
size.ratioFrom = ratioFrom
|
|
183
|
+
size.ratioTo = ratioTo
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function formatSizeRange(from: number, to: number) {
|
|
187
|
+
return from === to ? String(from) : `${from}-${to}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatRatioRange(from: number | null, to: number | null) {
|
|
191
|
+
if (from === null || to === null)
|
|
192
|
+
return ''
|
|
193
|
+
|
|
194
|
+
return from === to ? String(from) : `${from}-${to}`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function formatSizeDraft(size: DecorSizeDraft) {
|
|
198
|
+
const ratio = formatRatioRange(size.ratioFrom, size.ratioTo)
|
|
199
|
+
const dimensions = `${formatSizeRange(size.colsFrom, size.colsTo)}x${formatSizeRange(size.rowsFrom, size.rowsTo)}`
|
|
200
|
+
|
|
201
|
+
return ratio ? `${dimensions} · r ${ratio}` : dimensions
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function rangeToDecorValue(from: number, to: number): SizeRange {
|
|
205
|
+
return from === to ? from : [from, to]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ratioToDecorValue(from: number | null, to: number | null): RatioRange | null {
|
|
209
|
+
if (from === null || to === null)
|
|
210
|
+
return null
|
|
211
|
+
|
|
212
|
+
return from === to ? from : [from, to]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function formatDecorRange(from: number, to: number) {
|
|
216
|
+
const value = rangeToDecorValue(from, to)
|
|
217
|
+
return Array.isArray(value) ? `[${value.join(', ')}]` : String(value)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatDecorRatio(from: number | null, to: number | null) {
|
|
221
|
+
const value = ratioToDecorValue(from, to)
|
|
222
|
+
|
|
223
|
+
if (value === null)
|
|
224
|
+
return ''
|
|
225
|
+
|
|
226
|
+
return Array.isArray(value) ? `[${value.join(', ')}]` : String(value)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatDecorSize(size: DecorSizeDraft) {
|
|
230
|
+
const normalized = normalizeDecorSizeDraft(size)
|
|
231
|
+
|
|
232
|
+
const fields = [
|
|
233
|
+
`cols: ${formatDecorRange(normalized.colsFrom, normalized.colsTo)}`,
|
|
234
|
+
`rows: ${formatDecorRange(normalized.rowsFrom, normalized.rowsTo)}`,
|
|
235
|
+
]
|
|
236
|
+
const ratio = formatDecorRatio(normalized.ratioFrom, normalized.ratioTo)
|
|
237
|
+
|
|
238
|
+
if (ratio)
|
|
239
|
+
fields.push(`ratio: ${ratio}`)
|
|
240
|
+
|
|
241
|
+
return `{ ${fields.join(', ')} }`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function axisTokenToPercent(token: string, axis: 'x' | 'y') {
|
|
245
|
+
const normalized = token.trim().toLowerCase()
|
|
246
|
+
const percent = Number(normalized.replace('%', ''))
|
|
247
|
+
|
|
248
|
+
if (normalized.endsWith('%') && Number.isFinite(percent))
|
|
249
|
+
return clamp(percent, 0, 100)
|
|
250
|
+
|
|
251
|
+
if (normalized === 'center')
|
|
252
|
+
return 50
|
|
253
|
+
|
|
254
|
+
if (axis === 'x') {
|
|
255
|
+
if (normalized === 'left')
|
|
256
|
+
return 0
|
|
257
|
+
if (normalized === 'right')
|
|
258
|
+
return 100
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (normalized === 'top')
|
|
262
|
+
return 0
|
|
263
|
+
if (normalized === 'bottom')
|
|
264
|
+
return 100
|
|
265
|
+
|
|
266
|
+
return 50
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parsePosition(position: unknown): [number, number] {
|
|
270
|
+
if (typeof position !== 'string')
|
|
271
|
+
return [50, 50]
|
|
272
|
+
|
|
273
|
+
const tokens = position.trim().split(/\s+/u).filter(Boolean)
|
|
274
|
+
if (tokens.length === 0)
|
|
275
|
+
return [50, 50]
|
|
276
|
+
|
|
277
|
+
if (tokens.length === 1) {
|
|
278
|
+
const token = tokens[0]
|
|
279
|
+
if (token === 'top' || token === 'bottom')
|
|
280
|
+
return [50, axisTokenToPercent(token, 'y')]
|
|
281
|
+
|
|
282
|
+
return [axisTokenToPercent(token, 'x'), 50]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return [axisTokenToPercent(tokens[0], 'x'), axisTokenToPercent(tokens[1], 'y')]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function parseNumeric(value: unknown, fallback: number) {
|
|
289
|
+
if (value === undefined || value === null || value === '')
|
|
290
|
+
return fallback
|
|
291
|
+
|
|
292
|
+
const normalized = Number(String(value).replace(/deg|px|%/gu, '').trim())
|
|
293
|
+
return Number.isFinite(normalized) ? normalized : fallback
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function cloneSizeDraft(size: DecorSizeDraft): DecorSizeDraft {
|
|
297
|
+
return { ...size }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeDecorSizeDraft(size: DecorSizeDraft): DecorSizeDraft {
|
|
301
|
+
const normalized = cloneSizeDraft(size)
|
|
302
|
+
normalizeSizeDraft(normalized)
|
|
303
|
+
|
|
304
|
+
return normalized
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function matchesRatio(cols: number, rows: number, size: DecorSizeDraft) {
|
|
308
|
+
if (size.ratioFrom === null || size.ratioTo === null)
|
|
309
|
+
return true
|
|
310
|
+
|
|
311
|
+
const ratio = cols / rows
|
|
312
|
+
return ratio >= size.ratioFrom && ratio <= size.ratioTo
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function sizeDraftFrame(size: DecorSizeDraft, fallbackCols: number, fallbackRows: number) {
|
|
316
|
+
normalizeSizeDraft(size)
|
|
317
|
+
|
|
318
|
+
const centerCols = (size.colsFrom + size.colsTo) / 2
|
|
319
|
+
const centerRows = (size.rowsFrom + size.rowsTo) / 2
|
|
320
|
+
const centerRatio = size.ratioFrom !== null && size.ratioTo !== null
|
|
321
|
+
? (size.ratioFrom + size.ratioTo) / 2
|
|
322
|
+
: centerCols / centerRows
|
|
323
|
+
let best = {
|
|
324
|
+
cols: rangePreviewValue([size.colsFrom, size.colsTo], fallbackCols),
|
|
325
|
+
rows: rangePreviewValue([size.rowsFrom, size.rowsTo], fallbackRows),
|
|
326
|
+
score: Number.POSITIVE_INFINITY,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (let cols = size.colsFrom; cols <= size.colsTo; cols += 1) {
|
|
330
|
+
for (let rows = size.rowsFrom; rows <= size.rowsTo; rows += 1) {
|
|
331
|
+
if (!matchesRatio(cols, rows, size))
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
const score = Math.abs(cols - centerCols)
|
|
335
|
+
+ Math.abs(rows - centerRows)
|
|
336
|
+
+ Math.abs(cols / rows - centerRatio)
|
|
337
|
+
|
|
338
|
+
if (score < best.score)
|
|
339
|
+
best = { cols, rows, score }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
cols: best.cols,
|
|
345
|
+
rows: best.rows,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function createSizeFrameCandidates(size: DecorSizeDraft) {
|
|
350
|
+
const normalized = cloneSizeDraft(size)
|
|
351
|
+
normalizeSizeDraft(normalized)
|
|
352
|
+
|
|
353
|
+
const centerCols = (normalized.colsFrom + normalized.colsTo) / 2
|
|
354
|
+
const centerRows = (normalized.rowsFrom + normalized.rowsTo) / 2
|
|
355
|
+
const centerRatio = normalized.ratioFrom !== null && normalized.ratioTo !== null
|
|
356
|
+
? (normalized.ratioFrom + normalized.ratioTo) / 2
|
|
357
|
+
: centerCols / centerRows
|
|
358
|
+
const candidates: DecorFrameCandidate[] = []
|
|
359
|
+
|
|
360
|
+
for (let cols = normalized.colsFrom; cols <= normalized.colsTo; cols += 1) {
|
|
361
|
+
for (let rows = normalized.rowsFrom; rows <= normalized.rowsTo; rows += 1) {
|
|
362
|
+
if (!matchesRatio(cols, rows, normalized))
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
const ratio = cols / rows
|
|
366
|
+
candidates.push({
|
|
367
|
+
cols,
|
|
368
|
+
rows,
|
|
369
|
+
area: cols * rows,
|
|
370
|
+
ratio,
|
|
371
|
+
score: Math.abs(cols - centerCols) + Math.abs(rows - centerRows) + Math.abs(ratio - centerRatio),
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return candidates
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function sizePreviewCandidates(size: DecorSizeDraft) {
|
|
380
|
+
const candidates = createSizeFrameCandidates(size)
|
|
381
|
+
|
|
382
|
+
if (candidates.length === 0) {
|
|
383
|
+
const frame = sizeDraftFrame(cloneSizeDraft(size), 4, 4)
|
|
384
|
+
return [{ ...frame, area: frame.cols * frame.rows, ratio: frame.cols / frame.rows, score: 0 }]
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return candidates.sort((a, b) => a.area - b.area || a.ratio - b.ratio || a.cols - b.cols || a.rows - b.rows)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function firstFrame(decor: DecorVariant) {
|
|
391
|
+
const size = decor.sizes?.[0] ?? {}
|
|
392
|
+
const draft = createSizeDraft(size, 4, 4)
|
|
393
|
+
|
|
394
|
+
return sizeDraftFrame(draft, 4, 4)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function createSizeDrafts(decor: DecorVariant) {
|
|
398
|
+
const frame = firstFrame(decor)
|
|
399
|
+
const sizes = decor.sizes ?? []
|
|
400
|
+
|
|
401
|
+
return sizes.length
|
|
402
|
+
? sizes.map(size => createSizeDraft(size, frame.cols, frame.rows))
|
|
403
|
+
: [createSizeDraft({ cols: frame.cols, rows: frame.rows }, frame.cols, frame.rows)]
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function createDraft(decor: DecorVariant): DecorDraft {
|
|
407
|
+
const image = decor.image ?? {}
|
|
408
|
+
const [positionX, positionY] = parsePosition(image.position)
|
|
409
|
+
const frame = firstFrame(decor)
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
cols: frame.cols,
|
|
413
|
+
rows: frame.rows,
|
|
414
|
+
sizes: createSizeDrafts(decor),
|
|
415
|
+
fit: image.fit ?? 'contain',
|
|
416
|
+
positionX,
|
|
417
|
+
positionY,
|
|
418
|
+
anchor: image.anchor ?? 'center',
|
|
419
|
+
offsetX: parseNumeric(image.x, 0),
|
|
420
|
+
offsetY: parseNumeric(image.y, 0),
|
|
421
|
+
zoom: parseNumeric(image.zoom, 1),
|
|
422
|
+
rotate: parseNumeric(image.rotate, 0),
|
|
423
|
+
opacity: clamp(parseNumeric(image.opacity, 1), 0, 1),
|
|
424
|
+
color: image.color ?? '',
|
|
425
|
+
backgroundColor: image.backgroundColor ?? '',
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function normalizeList(value: unknown) {
|
|
430
|
+
return Array.isArray(value)
|
|
431
|
+
? value.filter(item => typeof item === 'string').map(item => item.trim()).filter(Boolean)
|
|
432
|
+
: []
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeCatalogVariant(variant: DecorVariant) {
|
|
436
|
+
const normalized: DecorVariant = {
|
|
437
|
+
...variant,
|
|
438
|
+
id: variant.id.trim(),
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if ((!Array.isArray(normalized.sizes) || normalized.sizes.length === 0)
|
|
442
|
+
&& normalized.cols !== undefined
|
|
443
|
+
&& normalized.rows !== undefined) {
|
|
444
|
+
normalized.sizes = [{
|
|
445
|
+
cols: normalized.cols,
|
|
446
|
+
rows: normalized.rows,
|
|
447
|
+
ratio: normalized.ratio ?? normalized.aspectRatio,
|
|
448
|
+
}]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (normalized.sizes) {
|
|
452
|
+
normalized.sizes = normalized.sizes.map((size) => {
|
|
453
|
+
const next = { ...size }
|
|
454
|
+
next.ratio ??= next.aspectRatio
|
|
455
|
+
delete next.aspectRatio
|
|
456
|
+
|
|
457
|
+
return next
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (normalizeList(normalized.meanings).length === 0 && typeof normalized.meaning === 'string' && normalized.meaning.trim())
|
|
462
|
+
normalized.meanings = [normalized.meaning.trim()]
|
|
463
|
+
|
|
464
|
+
if (normalizeList(normalized.tones).length === 0 && typeof normalized.tone === 'string' && normalized.tone.trim())
|
|
465
|
+
normalized.tones = [normalized.tone.trim()]
|
|
466
|
+
|
|
467
|
+
const toneTags = normalizeList(normalized.tones)
|
|
468
|
+
const explicitTags = normalizeList(normalized.tags)
|
|
469
|
+
normalized.tags = [...new Set([...explicitTags, ...toneTags])]
|
|
470
|
+
|
|
471
|
+
return normalized
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function mergeDecorVariants(catalog: readonly DecorVariant[], overrides: readonly DecorVariant[]) {
|
|
475
|
+
const variantsById = new Map<string, DecorVariant>()
|
|
476
|
+
|
|
477
|
+
for (const variant of [...catalog, ...overrides]) {
|
|
478
|
+
if (!variant?.id?.trim())
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
variantsById.set(variant.id.trim(), normalizeCatalogVariant(variant))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return [...variantsById.values()]
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getImagePatch(current: DecorDraft): DecorImage {
|
|
488
|
+
return {
|
|
489
|
+
fit: current.fit,
|
|
490
|
+
position: `${formatNumber(current.positionX, 1)}% ${formatNumber(current.positionY, 1)}%`,
|
|
491
|
+
anchor: current.anchor,
|
|
492
|
+
x: `${formatNumber(current.offsetX, 1)}%`,
|
|
493
|
+
y: `${formatNumber(current.offsetY, 1)}%`,
|
|
494
|
+
zoom: formatNumber(current.zoom, 2),
|
|
495
|
+
rotate: `${formatNumber(current.rotate, 1)}deg`,
|
|
496
|
+
color: current.color,
|
|
497
|
+
opacity: formatNumber(current.opacity, 2),
|
|
498
|
+
backgroundColor: current.backgroundColor,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function buildOverrideVariant(decor: DecorVariant, current: DecorDraft): DecorVariant {
|
|
503
|
+
const sizes = current.sizes.map((size) => {
|
|
504
|
+
const normalized = normalizeDecorSizeDraft(size)
|
|
505
|
+
const ratio = ratioToDecorValue(normalized.ratioFrom, normalized.ratioTo)
|
|
506
|
+
const nextSize: DecorSize = {
|
|
507
|
+
cols: rangeToDecorValue(normalized.colsFrom, normalized.colsTo),
|
|
508
|
+
rows: rangeToDecorValue(normalized.rowsFrom, normalized.rowsTo),
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (ratio !== null)
|
|
512
|
+
nextSize.ratio = ratio
|
|
513
|
+
|
|
514
|
+
return nextSize
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
id: decor.id,
|
|
519
|
+
src: decor.src,
|
|
520
|
+
meanings: [...(decor.meanings ?? [])],
|
|
521
|
+
tones: [...(decor.tones ?? [])],
|
|
522
|
+
tags: [...(decor.tags ?? [])],
|
|
523
|
+
sizes,
|
|
524
|
+
image: getImagePatch(current),
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function createPreviewStyle(colsInput: unknown, rowsInput: unknown): DecorPreviewStyle {
|
|
529
|
+
const cols = normalizeGridValue(colsInput, 4)
|
|
530
|
+
const rows = normalizeGridValue(rowsInput, 4)
|
|
531
|
+
const colStart = Math.floor((12 - cols) / 2) + 1
|
|
532
|
+
const rowStart = Math.floor((12 - rows) / 2) + 1
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
gridColumn: `${colStart} / span ${cols}`,
|
|
536
|
+
gridRow: `${rowStart} / span ${rows}`,
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function createDecorWorkbench(input: {
|
|
541
|
+
catalog: readonly DecorVariant[]
|
|
542
|
+
overrides: readonly DecorVariant[]
|
|
543
|
+
store: DecorStore
|
|
544
|
+
}) {
|
|
545
|
+
const overrides = ref<DecorVariant[]>([...input.overrides])
|
|
546
|
+
const selectedId = ref(input.catalog[0]?.id ?? input.overrides[0]?.id ?? '')
|
|
547
|
+
const drafts = reactive<Record<string, DecorDraft>>({})
|
|
548
|
+
const variants = computed(() => mergeDecorVariants(input.catalog, overrides.value))
|
|
549
|
+
const selected = computed(() => {
|
|
550
|
+
return variants.value.find(decor => decor.id === selectedId.value) ?? variants.value[0] ?? EMPTY_DECOR
|
|
551
|
+
})
|
|
552
|
+
const draft = computed(() => {
|
|
553
|
+
const decor = selected.value
|
|
554
|
+
drafts[decor.id] ??= createDraft(decor)
|
|
555
|
+
return drafts[decor.id]
|
|
556
|
+
})
|
|
557
|
+
const draftSignature = computed(() => {
|
|
558
|
+
const current = draft.value
|
|
559
|
+
|
|
560
|
+
return JSON.stringify({
|
|
561
|
+
selectedId: selected.value.id,
|
|
562
|
+
cols: current.cols,
|
|
563
|
+
rows: current.rows,
|
|
564
|
+
sizes: current.sizes.map(size => ({
|
|
565
|
+
colsFrom: size.colsFrom,
|
|
566
|
+
colsTo: size.colsTo,
|
|
567
|
+
rowsFrom: size.rowsFrom,
|
|
568
|
+
rowsTo: size.rowsTo,
|
|
569
|
+
ratioFrom: size.ratioFrom,
|
|
570
|
+
ratioTo: size.ratioTo,
|
|
571
|
+
})),
|
|
572
|
+
fit: current.fit,
|
|
573
|
+
positionX: current.positionX,
|
|
574
|
+
positionY: current.positionY,
|
|
575
|
+
anchor: current.anchor,
|
|
576
|
+
offsetX: current.offsetX,
|
|
577
|
+
offsetY: current.offsetY,
|
|
578
|
+
zoom: current.zoom,
|
|
579
|
+
rotate: current.rotate,
|
|
580
|
+
opacity: current.opacity,
|
|
581
|
+
color: current.color,
|
|
582
|
+
backgroundColor: current.backgroundColor,
|
|
583
|
+
})
|
|
584
|
+
})
|
|
585
|
+
const draftVersion = ref(0)
|
|
586
|
+
let saveTail: Promise<unknown> | null = null
|
|
587
|
+
|
|
588
|
+
watch(draftSignature, () => {
|
|
589
|
+
draftVersion.value += 1
|
|
590
|
+
}, { flush: 'sync' })
|
|
591
|
+
|
|
592
|
+
const previewStyle = computed(() => {
|
|
593
|
+
return createPreviewStyle(draft.value.cols, draft.value.rows)
|
|
594
|
+
})
|
|
595
|
+
const activePreviewFrameKey = computed(() => {
|
|
596
|
+
return `${normalizeGridValue(draft.value.cols, 4)}x${normalizeGridValue(draft.value.rows, 4)}`
|
|
597
|
+
})
|
|
598
|
+
const currentRatio = computed(() => {
|
|
599
|
+
const cols = normalizeGridValue(draft.value.cols, 4)
|
|
600
|
+
const rows = normalizeGridValue(draft.value.rows, 4)
|
|
601
|
+
|
|
602
|
+
return formatNumber(cols / rows, 2)
|
|
603
|
+
})
|
|
604
|
+
const previewFrames = computed(() => {
|
|
605
|
+
const seen = new Set<string>()
|
|
606
|
+
const frames: DecorPreviewFrame[] = []
|
|
607
|
+
|
|
608
|
+
for (const [sizeIndex, size] of draft.value.sizes.entries()) {
|
|
609
|
+
for (const frame of sizePreviewCandidates(size)) {
|
|
610
|
+
const dimensionsKey = `${frame.cols}x${frame.rows}`
|
|
611
|
+
if (seen.has(dimensionsKey))
|
|
612
|
+
continue
|
|
613
|
+
|
|
614
|
+
seen.add(dimensionsKey)
|
|
615
|
+
frames.push({
|
|
616
|
+
key: `${sizeIndex}-${dimensionsKey}`,
|
|
617
|
+
cols: frame.cols,
|
|
618
|
+
rows: frame.rows,
|
|
619
|
+
label: dimensionsKey,
|
|
620
|
+
detail: `r ${formatNumber(frame.ratio, 2)}`,
|
|
621
|
+
sizeIndex,
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return frames
|
|
627
|
+
})
|
|
628
|
+
const selectedMeta = computed(() => {
|
|
629
|
+
return [
|
|
630
|
+
...(selected.value.meanings ?? []),
|
|
631
|
+
...(selected.value.tones ?? []),
|
|
632
|
+
]
|
|
633
|
+
})
|
|
634
|
+
const catalogSnippet = computed(() => {
|
|
635
|
+
const current = draft.value
|
|
636
|
+
const sizes = current.sizes
|
|
637
|
+
.map(size => formatDecorSize(size))
|
|
638
|
+
.join(', ')
|
|
639
|
+
const fields: string[] = [
|
|
640
|
+
`fit: '${current.fit}'`,
|
|
641
|
+
`position: '${formatNumber(current.positionX, 1)}% ${formatNumber(current.positionY, 1)}%'`,
|
|
642
|
+
`zoom: ${formatNumber(current.zoom, 2)}`,
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
if (current.anchor !== 'center')
|
|
646
|
+
fields.push(`anchor: '${current.anchor}'`)
|
|
647
|
+
if (current.offsetX !== 0)
|
|
648
|
+
fields.push(`x: '${formatNumber(current.offsetX, 1)}%'`)
|
|
649
|
+
if (current.offsetY !== 0)
|
|
650
|
+
fields.push(`y: '${formatNumber(current.offsetY, 1)}%'`)
|
|
651
|
+
if (current.rotate !== 0)
|
|
652
|
+
fields.push(`rotate: '${formatNumber(current.rotate, 1)}deg'`)
|
|
653
|
+
if (current.color)
|
|
654
|
+
fields.push(`color: '${current.color}'`)
|
|
655
|
+
if (current.backgroundColor)
|
|
656
|
+
fields.push(`backgroundColor: '${current.backgroundColor}'`)
|
|
657
|
+
if (current.opacity !== 1)
|
|
658
|
+
fields.push(`opacity: ${formatNumber(current.opacity, 2)}`)
|
|
659
|
+
|
|
660
|
+
return `sizes: [${sizes}]\nimage: {\n ${fields.join(',\n ')},\n}`
|
|
661
|
+
})
|
|
662
|
+
const slideSnippet = computed(() => {
|
|
663
|
+
const current = draft.value
|
|
664
|
+
const fields: string[] = [
|
|
665
|
+
`id: '${selected.value.id}'`,
|
|
666
|
+
`position: '${formatNumber(current.positionX, 1)}% ${formatNumber(current.positionY, 1)}%'`,
|
|
667
|
+
`zoom: ${formatNumber(current.zoom, 2)}`,
|
|
668
|
+
]
|
|
669
|
+
|
|
670
|
+
if (current.color)
|
|
671
|
+
fields.push(`color: '${current.color}'`)
|
|
672
|
+
if (current.backgroundColor)
|
|
673
|
+
fields.push(`backgroundColor: '${current.backgroundColor}'`)
|
|
674
|
+
if (current.opacity !== 1)
|
|
675
|
+
fields.push(`opacity: ${formatNumber(current.opacity, 2)}`)
|
|
676
|
+
|
|
677
|
+
return `:decor="{ ${fields.join(', ')} }"`
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
function select(id: string) {
|
|
681
|
+
selectedId.value = id
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function update(patch: Partial<DecorDraft>) {
|
|
685
|
+
Object.assign(draft.value, patch)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function reset() {
|
|
689
|
+
drafts[selected.value.id] = createDraft(selected.value)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function buildOverride() {
|
|
693
|
+
return buildOverrideVariant(selected.value, draft.value)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function exportOverrides(next = buildOverride()) {
|
|
697
|
+
const byId = new Map(overrides.value.map(variant => [variant.id, variant]))
|
|
698
|
+
byId.set(next.id, next)
|
|
699
|
+
|
|
700
|
+
return [...byId.values()]
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function createSaveToken(): DecorSaveToken {
|
|
704
|
+
return {
|
|
705
|
+
selectedId: selected.value.id,
|
|
706
|
+
version: draftVersion.value,
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isCurrentSaveToken(token: DecorSaveToken) {
|
|
711
|
+
return token.selectedId === selected.value.id && token.version === draftVersion.value
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function save(token = createSaveToken()) {
|
|
715
|
+
const next = buildOverride()
|
|
716
|
+
const saveCurrent = async () => {
|
|
717
|
+
const nextOverrides = exportOverrides(next)
|
|
718
|
+
const result = await input.store.save(nextOverrides)
|
|
719
|
+
|
|
720
|
+
overrides.value = exportOverrides(next)
|
|
721
|
+
|
|
722
|
+
if (!isCurrentSaveToken(token))
|
|
723
|
+
throw new StaleDecorSaveError()
|
|
724
|
+
|
|
725
|
+
return result
|
|
726
|
+
}
|
|
727
|
+
const pendingSave = saveTail === null
|
|
728
|
+
? saveCurrent()
|
|
729
|
+
: saveTail.then(saveCurrent)
|
|
730
|
+
const tail = pendingSave.catch(() => {}).finally(() => {
|
|
731
|
+
if (saveTail === tail)
|
|
732
|
+
saveTail = null
|
|
733
|
+
})
|
|
734
|
+
saveTail = tail
|
|
735
|
+
|
|
736
|
+
return pendingSave
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function applyFrame(size: DecorSizeDraft) {
|
|
740
|
+
const frame = sizeDraftFrame(size, draft.value.cols, draft.value.rows)
|
|
741
|
+
|
|
742
|
+
draft.value.cols = frame.cols
|
|
743
|
+
draft.value.rows = frame.rows
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function applyPreviewFrame(frame: DecorPreviewFrame) {
|
|
747
|
+
draft.value.cols = frame.cols
|
|
748
|
+
draft.value.rows = frame.rows
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function previewFrameStyle(frame: DecorPreviewFrame) {
|
|
752
|
+
return createPreviewStyle(frame.cols, frame.rows)
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function addCurrentSize() {
|
|
756
|
+
const ratio = formatNumber(draft.value.cols / draft.value.rows, 2)
|
|
757
|
+
const size = createSizeDraft({ cols: draft.value.cols, rows: draft.value.rows, ratio }, draft.value.cols, draft.value.rows)
|
|
758
|
+
const duplicate = draft.value.sizes.some(item => formatSizeDraft(item) === formatSizeDraft(size))
|
|
759
|
+
|
|
760
|
+
if (!duplicate)
|
|
761
|
+
draft.value.sizes.push(size)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function removeSize(index: number) {
|
|
765
|
+
if (draft.value.sizes.length <= 1)
|
|
766
|
+
return
|
|
767
|
+
|
|
768
|
+
draft.value.sizes.splice(index, 1)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
overrides,
|
|
773
|
+
variants,
|
|
774
|
+
selectedId,
|
|
775
|
+
selected,
|
|
776
|
+
draft,
|
|
777
|
+
draftVersion,
|
|
778
|
+
previewStyle,
|
|
779
|
+
activePreviewFrameKey,
|
|
780
|
+
currentRatio,
|
|
781
|
+
previewFrames,
|
|
782
|
+
selectedMeta,
|
|
783
|
+
catalogSnippet,
|
|
784
|
+
slideSnippet,
|
|
785
|
+
select,
|
|
786
|
+
update,
|
|
787
|
+
reset,
|
|
788
|
+
buildOverride,
|
|
789
|
+
exportOverrides,
|
|
790
|
+
createSaveToken,
|
|
791
|
+
isCurrentSaveToken,
|
|
792
|
+
save,
|
|
793
|
+
applyFrame,
|
|
794
|
+
applyPreviewFrame,
|
|
795
|
+
previewFrameStyle,
|
|
796
|
+
addCurrentSize,
|
|
797
|
+
removeSize,
|
|
798
|
+
}
|
|
799
|
+
}
|