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,1117 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { DEFAULT_DECORS } from '../composables/decor-catalog.mjs'
4
+ import { DECOR_TUNING_OVERRIDES } from '../composables/decor-tuning-overrides.mjs'
5
+ import { createHttpDecorStore } from '../composables/decor-http-store'
6
+ import {
7
+ createDecorWorkbench,
8
+ formatNumber,
9
+ formatRatioRange,
10
+ formatSizeDraft,
11
+ formatSizeRange,
12
+ isStaleDecorSaveError,
13
+ normalizeSizeDraft,
14
+ type DecorPreviewFrame,
15
+ type DecorSizeDraft,
16
+ type DecorVariant,
17
+ type ImageFit,
18
+ } from '../composables/decor-workbench'
19
+ import { createThemeMedia } from '../composables/theme-media.mjs'
20
+ import ImageRenderer from './ImageRenderer.vue'
21
+
22
+ type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
23
+ type ColorOption = {
24
+ label: string
25
+ value: string
26
+ swatch: string
27
+ }
28
+
29
+ const COLOR_OPTIONS: readonly ColorOption[] = Object.freeze([
30
+ { label: 'none', value: '', swatch: 'transparent' },
31
+ { label: 'light-0', value: 'var(--theme-color-light-0)', swatch: '#ffffff' },
32
+ { label: 'light-1', value: 'var(--theme-color-light-1)', swatch: '#f0f0f0' },
33
+ { label: 'dark-0', value: 'var(--theme-color-dark-0)', swatch: '#1e1e1e' },
34
+ { label: 'dark-1', value: 'var(--theme-color-dark-1)', swatch: '#3c3c3c' },
35
+ { label: 'blue-0', value: 'var(--theme-color-blue-0)', swatch: '#027ef2' },
36
+ { label: 'blue-1', value: 'var(--theme-color-blue-1)', swatch: '#98d2fe' },
37
+ { label: 'orange-0', value: 'var(--theme-color-orange-0)', swatch: '#ff6c26' },
38
+ { label: 'orange-1', value: 'var(--theme-color-orange-1)', swatch: '#ffd2bd' },
39
+ { label: 'green-0', value: 'var(--theme-color-green-0)', swatch: '#07ab4b' },
40
+ { label: 'green-1', value: 'var(--theme-color-green-1)', swatch: '#9cddb7' },
41
+ { label: 'red-0', value: 'var(--theme-color-red-0)', swatch: '#e84033' },
42
+ { label: 'yellow-0', value: 'var(--theme-color-yellow-0)', swatch: '#ffd20a' },
43
+ ])
44
+
45
+ const FIT_OPTIONS: readonly ImageFit[] = Object.freeze(['contain', 'cover', 'fill', 'none', 'scale-down'])
46
+ const ANCHOR_OPTIONS = Object.freeze([
47
+ 'center',
48
+ 'left top',
49
+ 'left center',
50
+ 'left bottom',
51
+ 'center top',
52
+ 'center bottom',
53
+ 'right top',
54
+ 'right center',
55
+ 'right bottom',
56
+ ])
57
+
58
+ const workbench = createDecorWorkbench({
59
+ catalog: DEFAULT_DECORS as readonly DecorVariant[],
60
+ overrides: DECOR_TUNING_OVERRIDES as readonly DecorVariant[],
61
+ store: createHttpDecorStore(),
62
+ })
63
+ const media = createThemeMedia({ baseCatalog: [] })
64
+ const query = ref('')
65
+ const copied = ref(false)
66
+ const saveStatus = ref<SaveStatus>('idle')
67
+ const saveError = ref('')
68
+
69
+ const decorVariants = workbench.variants
70
+ const selectedDecor = workbench.selected
71
+ const draft = workbench.draft
72
+ const previewStyle = workbench.previewStyle
73
+ const activePreviewFrameKey = workbench.activePreviewFrameKey
74
+ const currentRatio = workbench.currentRatio
75
+ const previewFrames = workbench.previewFrames
76
+ const selectedMeta = workbench.selectedMeta
77
+ const catalogSnippet = workbench.catalogSnippet
78
+ const slideSnippet = workbench.slideSnippet
79
+
80
+ watch(workbench.draftVersion, () => {
81
+ if (saveStatus.value !== 'saved' && saveStatus.value !== 'error')
82
+ return
83
+
84
+ saveStatus.value = 'idle'
85
+ saveError.value = ''
86
+ })
87
+
88
+ function includesQuery(decor: DecorVariant, value: string) {
89
+ const normalized = value.trim().toLowerCase()
90
+ if (!normalized)
91
+ return true
92
+
93
+ return [
94
+ decor.id,
95
+ decor.src,
96
+ ...(decor.meanings ?? []),
97
+ ...(decor.tones ?? []),
98
+ ...(decor.tags ?? []),
99
+ ].some(item => item.toLowerCase().includes(normalized))
100
+ }
101
+
102
+ const filteredDecors = computed(() => {
103
+ return decorVariants.value.filter(decor => includesQuery(decor, query.value))
104
+ })
105
+
106
+ const filteredDecorItems = computed(() => {
107
+ return filteredDecors.value.map(decor => ({
108
+ decor,
109
+ layer: media.resolveImage({ src: decor.src, ...(decor.image ?? {}) }, decor.id),
110
+ }))
111
+ })
112
+
113
+ const selectedLayer = computed(() => {
114
+ const current = draft.value
115
+
116
+ return media.resolveImage({
117
+ src: selectedDecor.value.src,
118
+ fit: current.fit,
119
+ position: formatNumber(current.positionX, 1) + '% ' + formatNumber(current.positionY, 1) + '%',
120
+ anchor: current.anchor,
121
+ x: formatNumber(current.offsetX, 1) + '%',
122
+ y: formatNumber(current.offsetY, 1) + '%',
123
+ zoom: current.zoom,
124
+ rotate: formatNumber(current.rotate, 1) + 'deg',
125
+ color: current.color,
126
+ opacity: current.opacity,
127
+ backgroundColor: current.backgroundColor,
128
+ }, selectedDecor.value.id)
129
+ })
130
+
131
+ const selectedLayerKind = computed(() => selectedLayer.value?.render.kind ?? 'img')
132
+
133
+ function selectDecor(id: string) {
134
+ workbench.select(id)
135
+ copied.value = false
136
+ saveStatus.value = 'idle'
137
+ }
138
+
139
+ function resetSelected() {
140
+ workbench.reset()
141
+ copied.value = false
142
+ saveStatus.value = 'idle'
143
+ }
144
+
145
+ function applyFrame(size: DecorSizeDraft) {
146
+ workbench.applyFrame(size)
147
+ }
148
+
149
+ function applyPreviewFrame(frame: DecorPreviewFrame) {
150
+ workbench.applyPreviewFrame(frame)
151
+ }
152
+
153
+ function previewFrameStyle(frame: DecorPreviewFrame) {
154
+ return workbench.previewFrameStyle(frame)
155
+ }
156
+
157
+ function addCurrentSize() {
158
+ workbench.addCurrentSize()
159
+ saveStatus.value = 'idle'
160
+ }
161
+
162
+ function removeSize(index: number) {
163
+ workbench.removeSize(index)
164
+ saveStatus.value = 'idle'
165
+ }
166
+
167
+ function setColor(field: 'color' | 'backgroundColor', value: string) {
168
+ draft.value[field] = value
169
+ saveStatus.value = 'idle'
170
+ }
171
+
172
+ async function copySnippet() {
173
+ copied.value = false
174
+
175
+ if (!navigator?.clipboard)
176
+ return
177
+
178
+ await navigator.clipboard.writeText(catalogSnippet.value + '\n\n' + slideSnippet.value)
179
+ copied.value = true
180
+ }
181
+
182
+ async function saveCurrentDecor() {
183
+ const token = workbench.createSaveToken()
184
+
185
+ saveStatus.value = 'saving'
186
+ saveError.value = ''
187
+
188
+ try {
189
+ await workbench.save(token)
190
+ if (!workbench.isCurrentSaveToken(token))
191
+ return
192
+
193
+ saveStatus.value = 'saved'
194
+ }
195
+ catch (error) {
196
+ if (isStaleDecorSaveError(error) || !workbench.isCurrentSaveToken(token)) {
197
+ if (saveStatus.value === 'saving')
198
+ saveStatus.value = 'idle'
199
+ return
200
+ }
201
+
202
+ saveStatus.value = 'error'
203
+ saveError.value = error instanceof Error ? error.message : 'Не удалось сохранить'
204
+ }
205
+ }
206
+ </script>
207
+
208
+ <template>
209
+ <div class="DecorLibrary">
210
+ <aside class="DecorLibrary-Browser">
211
+ <div class="DecorLibrary-Header">
212
+ <div>
213
+ <p class="DecorLibrary-Kicker">decor catalog</p>
214
+ <h1>Библиотека декора</h1>
215
+ </div>
216
+ <span>{{ filteredDecors.length }}/{{ decorVariants.length }}</span>
217
+ </div>
218
+
219
+ <label class="DecorLibrary-Search">
220
+ <span>Поиск</span>
221
+ <input v-model="query"
222
+ type="search"
223
+ placeholder="id, meaning, tone" />
224
+ </label>
225
+
226
+ <div class="DecorLibrary-List">
227
+ <button v-for="item in filteredDecorItems"
228
+ :key="item.decor.id"
229
+ type="button"
230
+ class="DecorLibrary-Item"
231
+ :class="{ 'is-active': item.decor.id === selectedDecor.id }"
232
+ @click="selectDecor(item.decor.id)">
233
+ <span class="DecorLibrary-Thumb">
234
+ <ImageRenderer v-if="item.layer" :layer="item.layer" />
235
+ </span>
236
+ <span class="DecorLibrary-ItemText">
237
+ <strong>{{ item.decor.id }}</strong>
238
+ <small>{{ [...(item.decor.meanings ?? []), ...(item.decor.tones ?? [])].join(' · ') }}</small>
239
+ </span>
240
+ </button>
241
+ </div>
242
+ </aside>
243
+
244
+ <main class="DecorLibrary-Stage">
245
+ <div class="DecorLibrary-StageTop">
246
+ <div>
247
+ <p class="DecorLibrary-Kicker">{{ selectedLayerKind === 'mask' ? 'svg mask / recolor' : 'raster / photo' }}</p>
248
+ <h2>{{ selectedDecor.id }}</h2>
249
+ </div>
250
+ <button type="button"
251
+ class="DecorLibrary-Reset"
252
+ @click="resetSelected">
253
+ Сброс
254
+ </button>
255
+ </div>
256
+
257
+ <div class="DecorLibrary-PreviewGrid">
258
+ <div class="DecorLibrary-Preview" :style="previewStyle">
259
+ <ImageRenderer v-if="selectedLayer" :layer="selectedLayer" />
260
+ </div>
261
+ </div>
262
+
263
+ <div v-if="previewFrames.length > 1" class="DecorLibrary-Variants">
264
+ <div class="DecorLibrary-VariantsHeader">
265
+ <span>Размерные превью</span>
266
+ <span>{{ previewFrames.length }}</span>
267
+ </div>
268
+
269
+ <div class="DecorLibrary-VariantList">
270
+ <button v-for="frame in previewFrames"
271
+ :key="frame.key"
272
+ type="button"
273
+ class="DecorLibrary-Variant"
274
+ :class="{ 'is-active': `${frame.cols}x${frame.rows}` === activePreviewFrameKey }"
275
+ @click="applyPreviewFrame(frame)">
276
+ <span class="DecorLibrary-VariantGrid">
277
+ <span class="DecorLibrary-VariantPreview" :style="previewFrameStyle(frame)">
278
+ <ImageRenderer v-if="selectedLayer" :layer="selectedLayer" />
279
+ </span>
280
+ </span>
281
+ <span class="DecorLibrary-VariantText">
282
+ <strong>{{ frame.label }}</strong>
283
+ <small>{{ frame.detail }}</small>
284
+ </span>
285
+ </button>
286
+ </div>
287
+ </div>
288
+
289
+ <div class="DecorLibrary-Meta">
290
+ <span v-for="item in selectedMeta" :key="item">{{ item }}</span>
291
+ </div>
292
+ </main>
293
+
294
+ <aside class="DecorLibrary-Controls">
295
+ <section>
296
+ <h3>Кроп</h3>
297
+
298
+ <label>
299
+ <span>fit</span>
300
+ <select v-model="draft.fit">
301
+ <option v-for="fit in FIT_OPTIONS" :key="fit" :value="fit">{{ fit }}</option>
302
+ </select>
303
+ </label>
304
+
305
+ <div class="DecorLibrary-ControlRow">
306
+ <label>
307
+ <span>cols</span>
308
+ <input v-model.number="draft.cols"
309
+ type="number"
310
+ min="1"
311
+ max="12"
312
+ step="1" />
313
+ </label>
314
+ <label>
315
+ <span>rows</span>
316
+ <input v-model.number="draft.rows"
317
+ type="number"
318
+ min="1"
319
+ max="12"
320
+ step="1" />
321
+ </label>
322
+ <label>
323
+ <span>ratio</span>
324
+ <input :value="currentRatio"
325
+ type="text"
326
+ readonly />
327
+ </label>
328
+ </div>
329
+
330
+ <div class="DecorLibrary-Sizes">
331
+ <div class="DecorLibrary-SizeHeader">
332
+ <span>Варианты размеров</span>
333
+ <button type="button" @click="addCurrentSize">Добавить текущий</button>
334
+ </div>
335
+
336
+ <div v-for="(size, index) in draft.sizes"
337
+ :key="index"
338
+ class="DecorLibrary-SizeRow">
339
+ <div class="DecorLibrary-SizeActions">
340
+ <button type="button"
341
+ class="DecorLibrary-SizeChip"
342
+ @click="applyFrame(size)">
343
+ {{ formatSizeDraft(size) }}
344
+ </button>
345
+
346
+ <button type="button"
347
+ class="DecorLibrary-RemoveSize"
348
+ :disabled="draft.sizes.length <= 1"
349
+ @click="removeSize(index)">
350
+ Удалить
351
+ </button>
352
+ </div>
353
+
354
+ <div class="DecorLibrary-SizeInputs">
355
+ <div class="DecorLibrary-SizeAxis">
356
+ <div class="DecorLibrary-SizeAxisHeader">
357
+ <span>cols</span>
358
+ <strong>{{ formatSizeRange(size.colsFrom, size.colsTo) }}</strong>
359
+ </div>
360
+ <div class="DecorLibrary-SizePair">
361
+ <label>
362
+ <span>от</span>
363
+ <input v-model.number="size.colsFrom"
364
+ type="number"
365
+ min="1"
366
+ max="12"
367
+ step="1"
368
+ @change="normalizeSizeDraft(size)" />
369
+ </label>
370
+ <label>
371
+ <span>до</span>
372
+ <input v-model.number="size.colsTo"
373
+ type="number"
374
+ min="1"
375
+ max="12"
376
+ step="1"
377
+ @change="normalizeSizeDraft(size)" />
378
+ </label>
379
+ </div>
380
+ </div>
381
+
382
+ <div class="DecorLibrary-SizeAxis">
383
+ <div class="DecorLibrary-SizeAxisHeader">
384
+ <span>rows</span>
385
+ <strong>{{ formatSizeRange(size.rowsFrom, size.rowsTo) }}</strong>
386
+ </div>
387
+ <div class="DecorLibrary-SizePair">
388
+ <label>
389
+ <span>от</span>
390
+ <input v-model.number="size.rowsFrom"
391
+ type="number"
392
+ min="1"
393
+ max="12"
394
+ step="1"
395
+ @change="normalizeSizeDraft(size)" />
396
+ </label>
397
+ <label>
398
+ <span>до</span>
399
+ <input v-model.number="size.rowsTo"
400
+ type="number"
401
+ min="1"
402
+ max="12"
403
+ step="1"
404
+ @change="normalizeSizeDraft(size)" />
405
+ </label>
406
+ </div>
407
+ </div>
408
+
409
+ <div class="DecorLibrary-SizeAxis">
410
+ <div class="DecorLibrary-SizeAxisHeader">
411
+ <span>ratio</span>
412
+ <strong>{{ formatRatioRange(size.ratioFrom, size.ratioTo) || 'auto' }}</strong>
413
+ </div>
414
+ <div class="DecorLibrary-SizePair">
415
+ <label>
416
+ <span>от</span>
417
+ <input v-model.number="size.ratioFrom"
418
+ type="number"
419
+ min="0.08"
420
+ max="12"
421
+ step="0.01"
422
+ placeholder="auto"
423
+ @change="normalizeSizeDraft(size)" />
424
+ </label>
425
+ <label>
426
+ <span>до</span>
427
+ <input v-model.number="size.ratioTo"
428
+ type="number"
429
+ min="0.08"
430
+ max="12"
431
+ step="0.01"
432
+ placeholder="auto"
433
+ @change="normalizeSizeDraft(size)" />
434
+ </label>
435
+ </div>
436
+ </div>
437
+ </div>
438
+ </div>
439
+ </div>
440
+
441
+ <label>
442
+ <span>position x: {{ formatNumber(draft.positionX, 1) }}%</span>
443
+ <input v-model.number="draft.positionX"
444
+ type="range"
445
+ min="0"
446
+ max="100"
447
+ step="0.5" />
448
+ </label>
449
+
450
+ <label>
451
+ <span>position y: {{ formatNumber(draft.positionY, 1) }}%</span>
452
+ <input v-model.number="draft.positionY"
453
+ type="range"
454
+ min="0"
455
+ max="100"
456
+ step="0.5" />
457
+ </label>
458
+
459
+ <label>
460
+ <span>zoom: {{ formatNumber(draft.zoom, 2) }}</span>
461
+ <input v-model.number="draft.zoom"
462
+ type="range"
463
+ min="0.2"
464
+ max="6"
465
+ step="0.01" />
466
+ </label>
467
+ </section>
468
+
469
+ <section>
470
+ <h3>Трансформация</h3>
471
+
472
+ <label>
473
+ <span>anchor</span>
474
+ <select v-model="draft.anchor">
475
+ <option v-for="anchor in ANCHOR_OPTIONS" :key="anchor" :value="anchor">{{ anchor }}</option>
476
+ </select>
477
+ </label>
478
+
479
+ <label>
480
+ <span>x: {{ formatNumber(draft.offsetX, 1) }}%</span>
481
+ <input v-model.number="draft.offsetX"
482
+ type="range"
483
+ min="-120"
484
+ max="120"
485
+ step="0.5" />
486
+ </label>
487
+
488
+ <label>
489
+ <span>y: {{ formatNumber(draft.offsetY, 1) }}%</span>
490
+ <input v-model.number="draft.offsetY"
491
+ type="range"
492
+ min="-120"
493
+ max="120"
494
+ step="0.5" />
495
+ </label>
496
+
497
+ <label>
498
+ <span>rotate: {{ formatNumber(draft.rotate, 1) }}deg</span>
499
+ <input v-model.number="draft.rotate"
500
+ type="range"
501
+ min="-180"
502
+ max="180"
503
+ step="0.5" />
504
+ </label>
505
+
506
+ <label>
507
+ <span>opacity: {{ formatNumber(draft.opacity, 2) }}</span>
508
+ <input v-model.number="draft.opacity"
509
+ type="range"
510
+ min="0"
511
+ max="1"
512
+ step="0.01" />
513
+ </label>
514
+ </section>
515
+
516
+ <section>
517
+ <h3>Цвета</h3>
518
+
519
+ <div class="DecorLibrary-Palette">
520
+ <button v-for="option in COLOR_OPTIONS"
521
+ :key="`color-${option.label}`"
522
+ type="button"
523
+ :class="{ 'is-active': draft.color === option.value }"
524
+ :title="`color: ${option.label}`"
525
+ @click="setColor('color', option.value)">
526
+ <span :style="{ background: option.swatch }" />
527
+ </button>
528
+ </div>
529
+
530
+ <label>
531
+ <span>color</span>
532
+ <input v-model="draft.color" type="text" placeholder="var(--theme-color-blue-0)" />
533
+ </label>
534
+
535
+ <div class="DecorLibrary-Palette">
536
+ <button v-for="option in COLOR_OPTIONS"
537
+ :key="`bg-${option.label}`"
538
+ type="button"
539
+ :class="{ 'is-active': draft.backgroundColor === option.value }"
540
+ :title="`background: ${option.label}`"
541
+ @click="setColor('backgroundColor', option.value)">
542
+ <span :style="{ background: option.swatch }" />
543
+ </button>
544
+ </div>
545
+
546
+ <label>
547
+ <span>backgroundColor</span>
548
+ <input v-model="draft.backgroundColor" type="text" placeholder="var(--theme-color-orange-1)" />
549
+ </label>
550
+ </section>
551
+
552
+ <section>
553
+ <h3>Вывод</h3>
554
+ <textarea :value="`${catalogSnippet}\n\n${slideSnippet}`" readonly />
555
+ <button type="button"
556
+ class="DecorLibrary-Save"
557
+ :disabled="saveStatus === 'saving'"
558
+ @click="saveCurrentDecor">
559
+ {{ saveStatus === 'saving' ? 'Сохраняю' : 'Сохранить в overrides' }}
560
+ </button>
561
+ <button type="button"
562
+ class="DecorLibrary-Copy"
563
+ @click="copySnippet">
564
+ {{ copied ? 'Скопировано' : 'Копировать' }}
565
+ </button>
566
+ <p v-if="saveStatus === 'saved'" class="DecorLibrary-SaveStatus">
567
+ Сохранено в composables/decor-tuning-overrides.mjs
568
+ </p>
569
+ <p v-else-if="saveStatus === 'error'" class="DecorLibrary-SaveStatus DecorLibrary-SaveStatus_error">
570
+ {{ saveError }}
571
+ </p>
572
+ </section>
573
+ </aside>
574
+ </div>
575
+ </template>
576
+
577
+ <style scoped>
578
+ .DecorLibrary {
579
+ --decor-space-1: var(--theme-grid-module);
580
+ --decor-space-2: calc(var(--theme-grid-module) * 2);
581
+ --decor-space-3: calc(var(--theme-grid-module) * 3);
582
+ --decor-space-4: calc(var(--theme-grid-module) * 4);
583
+ --decor-space-5: calc(var(--theme-grid-module) * 5);
584
+ --decor-browser-width: calc(var(--theme-grid-module) * 52);
585
+ --decor-control-width: calc(var(--theme-grid-module) * 58);
586
+ --decor-stage-title-width: calc(var(--theme-grid-module) * 90);
587
+ --decor-thumb-size: calc(var(--theme-grid-module) * 10);
588
+ --decor-grid-size: calc(var(--theme-grid-module) * 8);
589
+ --decor-variant-grid-size: calc(var(--theme-grid-module) * 13);
590
+ --decor-pill-radius: calc(var(--theme-grid-module) * 10);
591
+ --decor-swatch-button: calc(var(--theme-grid-module) * 4);
592
+ --decor-swatch: calc(var(--theme-grid-module) * 2);
593
+
594
+ grid-column: 1 / -1;
595
+ grid-row: 1 / -1;
596
+ display: grid;
597
+ grid-template-columns: var(--decor-browser-width) minmax(0, 1fr) var(--decor-control-width);
598
+ gap: var(--decor-space-3);
599
+ width: 100%;
600
+ height: 100%;
601
+ padding: var(--decor-space-5);
602
+ background: var(--theme-color-light-1);
603
+ color: var(--theme-text);
604
+ font-size: var(--theme-text-size-0);
605
+ line-height: var(--theme-text-line-0);
606
+ }
607
+
608
+ .DecorLibrary,
609
+ .DecorLibrary * {
610
+ box-sizing: border-box;
611
+ }
612
+
613
+ .DecorLibrary-Browser,
614
+ .DecorLibrary-Stage,
615
+ .DecorLibrary-Controls {
616
+ min-height: 0;
617
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
618
+ border-radius: var(--theme-grid-module);
619
+ background: var(--theme-color-light-0);
620
+ }
621
+
622
+ .DecorLibrary-Browser,
623
+ .DecorLibrary-Controls {
624
+ display: flex;
625
+ flex-direction: column;
626
+ }
627
+
628
+ .DecorLibrary-Header,
629
+ .DecorLibrary-StageTop {
630
+ display: flex;
631
+ align-items: flex-start;
632
+ justify-content: space-between;
633
+ gap: var(--decor-space-2);
634
+ padding: var(--decor-space-3);
635
+ border-bottom: var(--theme-stroke-thin) solid var(--theme-border-subtle);
636
+ }
637
+
638
+ .DecorLibrary-Header h1,
639
+ .DecorLibrary-StageTop h2,
640
+ .DecorLibrary-Controls h3 {
641
+ margin: 0;
642
+ color: var(--theme-text);
643
+ font-size: var(--theme-text-size-2);
644
+ line-height: var(--theme-text-line-2);
645
+ font-weight: 400;
646
+ }
647
+
648
+ .DecorLibrary-StageTop h2 {
649
+ max-width: var(--decor-stage-title-width);
650
+ overflow-wrap: anywhere;
651
+ font-size: var(--theme-text-size-3);
652
+ }
653
+
654
+ .DecorLibrary-Controls h3 {
655
+ font-size: var(--theme-text-size-1);
656
+ }
657
+
658
+ .DecorLibrary-Kicker {
659
+ margin: 0 0 var(--decor-space-1);
660
+ color: var(--theme-text-muted);
661
+ font-size: var(--theme-text-size-0);
662
+ text-transform: uppercase;
663
+ }
664
+
665
+ .DecorLibrary-Header > span {
666
+ padding: var(--decor-space-1) var(--decor-space-2);
667
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
668
+ border-radius: var(--decor-pill-radius);
669
+ color: var(--theme-text-muted);
670
+ font-size: var(--theme-text-size-0);
671
+ }
672
+
673
+ .DecorLibrary-Search {
674
+ display: grid;
675
+ gap: var(--decor-space-1);
676
+ padding: var(--decor-space-2) var(--decor-space-3);
677
+ border-bottom: var(--theme-stroke-thin) solid var(--theme-border-subtle);
678
+ }
679
+
680
+ .DecorLibrary-Search span,
681
+ .DecorLibrary-Controls label span {
682
+ color: var(--theme-text-muted);
683
+ font-size: var(--theme-text-size-0);
684
+ }
685
+
686
+ .DecorLibrary input,
687
+ .DecorLibrary select,
688
+ .DecorLibrary textarea {
689
+ width: 100%;
690
+ min-width: 0;
691
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
692
+ border-radius: var(--theme-grid-module);
693
+ background: var(--theme-color-light-0);
694
+ color: var(--theme-text);
695
+ font: inherit;
696
+ }
697
+
698
+ .DecorLibrary input,
699
+ .DecorLibrary select {
700
+ height: var(--decor-space-5);
701
+ padding: 0 var(--decor-space-1);
702
+ }
703
+
704
+ .DecorLibrary input[type='range'] {
705
+ padding: 0;
706
+ border: 0;
707
+ }
708
+
709
+ .DecorLibrary textarea {
710
+ min-height: calc(var(--theme-grid-module) * 20);
711
+ padding: var(--decor-space-2);
712
+ resize: none;
713
+ font-family: var(--theme-font-mono);
714
+ font-size: var(--theme-text-size-0);
715
+ line-height: var(--theme-text-line-0);
716
+ }
717
+
718
+ .DecorLibrary button {
719
+ border: 0;
720
+ font: inherit;
721
+ cursor: pointer;
722
+ }
723
+
724
+ .DecorLibrary button:disabled {
725
+ cursor: default;
726
+ opacity: 0.45;
727
+ }
728
+
729
+ .DecorLibrary-List {
730
+ display: grid;
731
+ gap: var(--decor-space-1);
732
+ min-height: 0;
733
+ padding: var(--decor-space-2);
734
+ overflow: auto;
735
+ }
736
+
737
+ .DecorLibrary-Item {
738
+ display: grid;
739
+ grid-template-columns: var(--decor-thumb-size) minmax(0, 1fr);
740
+ gap: var(--decor-space-2);
741
+ align-items: center;
742
+ padding: var(--decor-space-1);
743
+ border: var(--theme-stroke-thin) solid transparent;
744
+ border-radius: var(--theme-grid-module);
745
+ background: transparent;
746
+ color: inherit;
747
+ text-align: left;
748
+ }
749
+
750
+ .DecorLibrary-Item.is-active {
751
+ border-color: var(--theme-color-blue-0);
752
+ background: color-mix(in srgb, var(--theme-color-blue-1) 22%, transparent);
753
+ }
754
+
755
+ .DecorLibrary-Thumb {
756
+ display: block;
757
+ width: var(--decor-thumb-size);
758
+ aspect-ratio: 1;
759
+ overflow: hidden;
760
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
761
+ border-radius: var(--theme-grid-module);
762
+ background:
763
+ linear-gradient(45deg, color-mix(in srgb, var(--theme-color-dark-2) 15%, transparent) 25%, transparent 25%),
764
+ linear-gradient(-45deg, color-mix(in srgb, var(--theme-color-dark-2) 15%, transparent) 25%, transparent 25%),
765
+ var(--theme-color-light-1);
766
+ background-position: 0 0, 0 var(--decor-space-2);
767
+ background-size: var(--decor-space-4) var(--decor-space-4);
768
+ }
769
+
770
+ .DecorLibrary-ItemText {
771
+ display: grid;
772
+ min-width: 0;
773
+ gap: var(--decor-space-1);
774
+ }
775
+
776
+ .DecorLibrary-ItemText strong,
777
+ .DecorLibrary-ItemText small {
778
+ display: block;
779
+ overflow: hidden;
780
+ text-overflow: ellipsis;
781
+ white-space: nowrap;
782
+ }
783
+
784
+ .DecorLibrary-ItemText strong {
785
+ font-weight: 400;
786
+ }
787
+
788
+ .DecorLibrary-ItemText small {
789
+ color: var(--theme-text-muted);
790
+ font-size: var(--theme-text-size-0);
791
+ }
792
+
793
+ .DecorLibrary-Stage {
794
+ display: grid;
795
+ grid-template-rows: auto minmax(0, 1fr) auto auto;
796
+ min-width: 0;
797
+ }
798
+
799
+ .DecorLibrary-Reset,
800
+ .DecorLibrary-Copy,
801
+ .DecorLibrary-Save {
802
+ height: var(--decor-space-5);
803
+ padding: 0 var(--decor-space-2);
804
+ border-radius: var(--theme-grid-module);
805
+ background: var(--theme-color-dark-0);
806
+ color: var(--theme-color-light-0);
807
+ }
808
+
809
+ .DecorLibrary-Copy {
810
+ background: var(--theme-color-light-1);
811
+ color: var(--theme-text);
812
+ }
813
+
814
+ .DecorLibrary-PreviewGrid {
815
+ display: grid;
816
+ grid-template-columns: repeat(12, minmax(0, 1fr));
817
+ grid-template-rows: repeat(12, minmax(0, 1fr));
818
+ gap: var(--theme-grid-gap);
819
+ min-height: 0;
820
+ padding: var(--decor-space-5);
821
+ background:
822
+ linear-gradient(var(--theme-border-subtle) var(--theme-stroke-thin), transparent var(--theme-stroke-thin)),
823
+ linear-gradient(90deg, var(--theme-border-subtle) var(--theme-stroke-thin), transparent var(--theme-stroke-thin)),
824
+ var(--theme-color-light-0);
825
+ background-size: var(--decor-grid-size) var(--decor-grid-size);
826
+ }
827
+
828
+ .DecorLibrary-Preview {
829
+ width: 100%;
830
+ height: 100%;
831
+ overflow: hidden;
832
+ border: var(--theme-stroke-thin) solid var(--theme-color-dark-0);
833
+ border-radius: var(--theme-grid-module);
834
+ background: var(--theme-color-light-1);
835
+ }
836
+
837
+ .DecorLibrary-Variants {
838
+ display: grid;
839
+ gap: var(--decor-space-2);
840
+ padding: var(--decor-space-2) var(--decor-space-3);
841
+ border-top: var(--theme-stroke-thin) solid var(--theme-border-subtle);
842
+ }
843
+
844
+ .DecorLibrary-VariantsHeader {
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: space-between;
848
+ gap: var(--decor-space-2);
849
+ color: var(--theme-text-muted);
850
+ font-size: var(--theme-text-size-0);
851
+ }
852
+
853
+ .DecorLibrary-VariantList {
854
+ display: grid;
855
+ grid-template-columns: repeat(auto-fit, minmax(calc(var(--theme-grid-module) * 24), 1fr));
856
+ gap: var(--decor-space-1);
857
+ max-height: calc(var(--theme-grid-module) * 24);
858
+ overflow: auto;
859
+ }
860
+
861
+ .DecorLibrary-Variant {
862
+ display: grid;
863
+ grid-template-columns: var(--decor-variant-grid-size) minmax(0, 1fr);
864
+ gap: var(--decor-space-2);
865
+ align-items: center;
866
+ min-width: 0;
867
+ padding: var(--decor-space-1);
868
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
869
+ border-radius: var(--theme-grid-module);
870
+ background: var(--theme-color-light-0);
871
+ color: inherit;
872
+ text-align: left;
873
+ }
874
+
875
+ .DecorLibrary-Variant.is-active {
876
+ border-color: var(--theme-color-blue-0);
877
+ background: color-mix(in srgb, var(--theme-color-blue-1) 18%, transparent);
878
+ }
879
+
880
+ .DecorLibrary-VariantGrid {
881
+ display: grid;
882
+ grid-template-columns: repeat(12, minmax(0, 1fr));
883
+ grid-template-rows: repeat(12, minmax(0, 1fr));
884
+ gap: var(--theme-stroke-thin);
885
+ width: var(--decor-variant-grid-size);
886
+ aspect-ratio: 1;
887
+ padding: var(--decor-space-1);
888
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
889
+ border-radius: var(--theme-grid-module);
890
+ background:
891
+ linear-gradient(var(--theme-border-subtle) var(--theme-stroke-thin), transparent var(--theme-stroke-thin)),
892
+ linear-gradient(90deg, var(--theme-border-subtle) var(--theme-stroke-thin), transparent var(--theme-stroke-thin)),
893
+ var(--theme-color-light-1);
894
+ background-size: var(--decor-space-2) var(--decor-space-2);
895
+ }
896
+
897
+ .DecorLibrary-VariantPreview {
898
+ display: block;
899
+ min-width: 0;
900
+ min-height: 0;
901
+ overflow: hidden;
902
+ border: var(--theme-stroke-thin) solid var(--theme-color-dark-0);
903
+ border-radius: var(--theme-grid-module);
904
+ background: var(--theme-color-light-0);
905
+ }
906
+
907
+ .DecorLibrary-VariantText {
908
+ display: grid;
909
+ min-width: 0;
910
+ gap: var(--decor-space-1);
911
+ }
912
+
913
+ .DecorLibrary-VariantText strong,
914
+ .DecorLibrary-VariantText small {
915
+ overflow: hidden;
916
+ text-overflow: ellipsis;
917
+ white-space: nowrap;
918
+ }
919
+
920
+ .DecorLibrary-VariantText strong {
921
+ font-weight: 400;
922
+ }
923
+
924
+ .DecorLibrary-VariantText small {
925
+ color: var(--theme-text-muted);
926
+ font-size: var(--theme-text-size-0);
927
+ }
928
+
929
+ .DecorLibrary-Meta {
930
+ display: flex;
931
+ flex-wrap: wrap;
932
+ gap: var(--decor-space-1);
933
+ padding: var(--decor-space-2) var(--decor-space-3);
934
+ border-top: var(--theme-stroke-thin) solid var(--theme-border-subtle);
935
+ }
936
+
937
+ .DecorLibrary-Meta span,
938
+ .DecorLibrary-SizeChip,
939
+ .DecorLibrary-SizeHeader button,
940
+ .DecorLibrary-RemoveSize {
941
+ padding: var(--decor-space-1) var(--decor-space-2);
942
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
943
+ border-radius: var(--decor-pill-radius);
944
+ background: transparent;
945
+ color: var(--theme-text-muted);
946
+ font-size: var(--theme-text-size-0);
947
+ }
948
+
949
+ .DecorLibrary-Controls {
950
+ gap: var(--decor-space-2);
951
+ padding: var(--decor-space-3);
952
+ overflow: auto;
953
+ }
954
+
955
+ .DecorLibrary-Controls section {
956
+ display: grid;
957
+ gap: var(--decor-space-2);
958
+ padding-bottom: var(--decor-space-2);
959
+ border-bottom: var(--theme-stroke-thin) solid var(--theme-border-subtle);
960
+ }
961
+
962
+ .DecorLibrary-Controls section:last-child {
963
+ border-bottom: 0;
964
+ }
965
+
966
+ .DecorLibrary-Controls label {
967
+ display: grid;
968
+ gap: var(--decor-space-1);
969
+ }
970
+
971
+ .DecorLibrary-ControlRow {
972
+ display: grid;
973
+ grid-template-columns: repeat(3, minmax(0, 1fr));
974
+ gap: var(--decor-space-1);
975
+ }
976
+
977
+ .DecorLibrary-ControlRow input[readonly] {
978
+ background: var(--theme-color-light-1);
979
+ color: var(--theme-text-muted);
980
+ }
981
+
982
+ .DecorLibrary-Sizes,
983
+ .DecorLibrary-Palette {
984
+ display: flex;
985
+ flex-wrap: wrap;
986
+ gap: var(--decor-space-1);
987
+ }
988
+
989
+ .DecorLibrary-Sizes {
990
+ display: grid;
991
+ }
992
+
993
+ .DecorLibrary-SizeHeader,
994
+ .DecorLibrary-SizeRow {
995
+ display: grid;
996
+ gap: var(--decor-space-1);
997
+ }
998
+
999
+ .DecorLibrary-SizeHeader {
1000
+ grid-template-columns: minmax(0, 1fr) auto;
1001
+ align-items: center;
1002
+ }
1003
+
1004
+ .DecorLibrary-SizeHeader span {
1005
+ color: var(--theme-text-muted);
1006
+ }
1007
+
1008
+ .DecorLibrary-SizeRow {
1009
+ padding: var(--decor-space-2);
1010
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
1011
+ border-radius: var(--theme-grid-module);
1012
+ background: var(--theme-color-light-1);
1013
+ }
1014
+
1015
+ .DecorLibrary-SizeActions {
1016
+ display: grid;
1017
+ grid-template-columns: minmax(0, 1fr) auto;
1018
+ gap: var(--decor-space-1);
1019
+ align-items: center;
1020
+ }
1021
+
1022
+ .DecorLibrary-SizeInputs {
1023
+ display: grid;
1024
+ grid-template-columns: minmax(0, 1fr);
1025
+ gap: var(--decor-space-1);
1026
+ }
1027
+
1028
+ .DecorLibrary-SizeAxis {
1029
+ display: grid;
1030
+ grid-template-columns: calc(var(--theme-grid-module) * 12) minmax(0, 1fr);
1031
+ gap: var(--decor-space-1);
1032
+ align-items: end;
1033
+ min-width: 0;
1034
+ padding: var(--decor-space-1);
1035
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
1036
+ border-radius: var(--theme-grid-module);
1037
+ background: var(--theme-color-light-0);
1038
+ }
1039
+
1040
+ .DecorLibrary-SizeAxisHeader {
1041
+ display: grid;
1042
+ gap: var(--decor-space-1);
1043
+ }
1044
+
1045
+ .DecorLibrary-SizeAxisHeader span {
1046
+ color: var(--theme-text-muted);
1047
+ font-size: var(--theme-text-size-0);
1048
+ }
1049
+
1050
+ .DecorLibrary-SizeAxisHeader strong {
1051
+ color: var(--theme-text);
1052
+ font-family: var(--theme-font-mono);
1053
+ font-size: var(--theme-text-size-0);
1054
+ font-weight: 400;
1055
+ font-variant-numeric: tabular-nums;
1056
+ }
1057
+
1058
+ .DecorLibrary-SizePair {
1059
+ display: grid;
1060
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1061
+ gap: var(--decor-space-1);
1062
+ }
1063
+
1064
+ .DecorLibrary-SizePair label {
1065
+ min-width: 0;
1066
+ }
1067
+
1068
+ .DecorLibrary-SizePair input {
1069
+ padding: 0 var(--decor-space-2);
1070
+ font-family: var(--theme-font-mono);
1071
+ font-variant-numeric: tabular-nums;
1072
+ text-align: right;
1073
+ }
1074
+
1075
+ .DecorLibrary-SizePair input::-webkit-inner-spin-button,
1076
+ .DecorLibrary-SizePair input::-webkit-outer-spin-button {
1077
+ margin: 0;
1078
+ }
1079
+
1080
+ .DecorLibrary-SizeChip,
1081
+ .DecorLibrary-SizeHeader button,
1082
+ .DecorLibrary-RemoveSize {
1083
+ background: transparent;
1084
+ }
1085
+
1086
+ .DecorLibrary-SaveStatus {
1087
+ margin: 0;
1088
+ color: var(--theme-text-muted);
1089
+ font-size: var(--theme-text-size-0);
1090
+ }
1091
+
1092
+ .DecorLibrary-SaveStatus_error {
1093
+ color: var(--theme-color-red-0);
1094
+ }
1095
+
1096
+ .DecorLibrary-Palette button {
1097
+ display: grid;
1098
+ place-items: center;
1099
+ width: var(--decor-swatch-button);
1100
+ height: var(--decor-swatch-button);
1101
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
1102
+ border-radius: var(--theme-grid-module);
1103
+ background: var(--theme-color-light-0);
1104
+ }
1105
+
1106
+ .DecorLibrary-Palette button.is-active {
1107
+ border-color: var(--theme-color-blue-0);
1108
+ box-shadow: inset 0 0 0 calc(var(--theme-stroke-thin) * 2) var(--theme-color-blue-0);
1109
+ }
1110
+
1111
+ .DecorLibrary-Palette span {
1112
+ width: var(--decor-swatch);
1113
+ height: var(--decor-swatch);
1114
+ border: var(--theme-stroke-thin) solid var(--theme-border-subtle);
1115
+ border-radius: var(--decor-pill-radius);
1116
+ }
1117
+ </style>