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,419 @@
1
+ <script setup lang="ts">
2
+ import { useSlideContext } from '@slidev/client'
3
+ import {
4
+ computed,
5
+ defineComponent,
6
+ getCurrentInstance,
7
+ h,
8
+ isVNode,
9
+ onBeforeUnmount,
10
+ shallowRef,
11
+ watchEffect,
12
+ type CSSProperties,
13
+ type PropType,
14
+ type VNode,
15
+ } from 'vue'
16
+ import ImageRenderer from './ImageRenderer.vue'
17
+ import { createThemeMedia } from '../composables/theme-media.mjs'
18
+ import type { ThemeLayoutSlotSpec } from '../composables/layout-registry'
19
+ import { useSlotPlacementSession, type ResolvedSlotPlacement } from '../composables/slot-placement'
20
+ import { createTextFitRuntime } from '../composables/text-fit-runtime'
21
+ import { useThemeConfig } from '../composables/use-theme-config'
22
+ import { resolveSlotTheme, themeVars, type ThemeSlotSurface } from '../composables/theme-foundation'
23
+ import { camelToKebab } from '../composables/layout-vnode'
24
+
25
+ type SlotMarginLevel = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
26
+ type SlotMarginInput = SlotMarginLevel | `${SlotMarginLevel}`
27
+ type SlotGap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
28
+ type SlotGapInput = SlotGap | `${SlotGap}`
29
+ type SlotRole = 'primary' | 'secondary' | 'support' | 'media'
30
+ type SlotSurfaceInput = ThemeSlotSurface
31
+ type ImageDataInput = {
32
+ src: string
33
+ fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
34
+ position?: string
35
+ anchor?: string
36
+ x?: number | string
37
+ y?: number | string
38
+ zoom?: number | string
39
+ rotate?: number | string
40
+ color?: string
41
+ opacity?: number | string
42
+ backgroundColor?: string
43
+ } & {
44
+ 'background-color'?: string
45
+ }
46
+ type SlotBackgroundInput = ImageDataInput | ImageDataInput[]
47
+ type SlotDecorInput = Omit<Partial<ImageDataInput>, 'src'> & {
48
+ src?: string
49
+ id?: string
50
+ meaning?: string
51
+ tone?: string
52
+ }
53
+
54
+ const SLOT_MARGIN_LEVELS = new Set<`${SlotMarginLevel}`>(['1', '2', '3', '4', '5', '6', '7', '8'])
55
+ const SLOT_GAP_LEVELS = new Set<`${SlotGap}`>(['0', '1', '2', '3', '4', '5', '6', '7'])
56
+ const layoutOverrideKeys = [
57
+ 'surface',
58
+ 'tone',
59
+ 'margin',
60
+ 'marginTop',
61
+ 'marginRight',
62
+ 'marginBottom',
63
+ 'marginLeft',
64
+ 'centered',
65
+ ] as const satisfies readonly (keyof ThemeLayoutSlotSpec)[]
66
+
67
+ const SlotContent = defineComponent({
68
+ name: 'SlotContent',
69
+ props: {
70
+ contentStyle: {
71
+ type: Object as PropType<CSSProperties>,
72
+ required: true,
73
+ },
74
+ },
75
+ setup(contentProps, { slots }) {
76
+ const contentElement = shallowRef<HTMLElement | null>(null)
77
+ let currentChildren: VNode[] = []
78
+ const textFit = createTextFitRuntime()
79
+ const { apply } = textFit.useGroup({
80
+ target: contentElement,
81
+ nodes: () => currentChildren,
82
+ })
83
+
84
+ return () => {
85
+ const children = (slots.default?.() ?? []).filter(isVNode)
86
+ currentChildren = children
87
+
88
+ return h('div', {
89
+ ref: contentElement,
90
+ class: 'Slot-Content',
91
+ style: contentProps.contentStyle,
92
+ }, apply(children))
93
+ }
94
+ },
95
+ })
96
+
97
+ const props = withDefaults(defineProps<{
98
+ as?: string
99
+ role?: SlotRole
100
+ area?: string
101
+ col?: string
102
+ row?: string
103
+ label?: string
104
+ surface?: SlotSurfaceInput
105
+ tone?: string
106
+ background?: SlotBackgroundInput
107
+ decor?: SlotDecorInput
108
+ margin?: SlotMarginInput
109
+ marginTop?: SlotMarginInput
110
+ marginRight?: SlotMarginInput
111
+ marginBottom?: SlotMarginInput
112
+ marginLeft?: SlotMarginInput
113
+ gap?: SlotGapInput
114
+ centered?: boolean
115
+ }>(), {
116
+ as: 'section',
117
+ role: undefined,
118
+ area: '',
119
+ col: '',
120
+ row: '',
121
+ label: '',
122
+ surface: 'none',
123
+ tone: '',
124
+ background: undefined,
125
+ decor: undefined,
126
+ gap: '0',
127
+ centered: false,
128
+ })
129
+
130
+ const slotId = `theme-slot-${Math.random().toString(36).slice(2, 10)}`
131
+ const instance = getCurrentInstance()
132
+ const placementSession = useSlotPlacementSession()
133
+ const { defaultTone, deckTitle, decors } = useThemeConfig()
134
+ const { $slidev } = useSlideContext()
135
+ const resolvedRect = shallowRef<ResolvedSlotPlacement['rect'] | null>(null)
136
+ const resolvedFootprint = shallowRef<ResolvedSlotPlacement['footprint'] | null>(null)
137
+ const resolvedLayoutSlot = shallowRef<ResolvedSlotPlacement['layoutSlot']>(null)
138
+ const placementStyle = shallowRef<ResolvedSlotPlacement['style']>({
139
+ gridColumn: 'auto',
140
+ gridRow: 'auto',
141
+ })
142
+
143
+ function hasAuthoredProp(key: string) {
144
+ const vnodeProps = instance?.vnode.props ?? {}
145
+ const kebabKey = camelToKebab(key)
146
+ return key in vnodeProps || kebabKey in vnodeProps
147
+ }
148
+
149
+ function readLayoutOverrides() {
150
+ const overrides: Partial<ThemeLayoutSlotSpec> = {}
151
+
152
+ for (const key of layoutOverrideKeys) {
153
+ if (!hasAuthoredProp(key))
154
+ continue
155
+
156
+ const value = props[key]
157
+ if (key === 'tone' && !value)
158
+ continue
159
+
160
+ overrides[key] = value as never
161
+ }
162
+
163
+ return overrides
164
+ }
165
+
166
+ function readPlacementInput() {
167
+ return {
168
+ id: slotId,
169
+ label: props.label || slotId,
170
+ role: props.role,
171
+ area: props.area,
172
+ col: props.col,
173
+ row: props.row,
174
+ overrides: readLayoutOverrides(),
175
+ }
176
+ }
177
+
178
+ function applyPlacement(placement: ResolvedSlotPlacement) {
179
+ resolvedRect.value = placement.rect
180
+ resolvedFootprint.value = placement.footprint
181
+ resolvedLayoutSlot.value = placement.layoutSlot
182
+ placementStyle.value = placement.style
183
+ }
184
+
185
+ const placementHandle = placementSession.register(readPlacementInput(), applyPlacement)
186
+
187
+ watchEffect(() => {
188
+ placementHandle.update(readPlacementInput())
189
+ })
190
+
191
+ onBeforeUnmount(() => {
192
+ placementHandle.dispose()
193
+ })
194
+
195
+ const resolvedSurface = computed(() => resolveSlotTheme({
196
+ surface: resolvedLayoutSlot.value?.surface ?? props.surface ?? 'none',
197
+ tone: resolvedLayoutSlot.value?.tone ?? props.tone ?? '',
198
+ defaultTone: defaultTone.value,
199
+ label: props.label || slotId,
200
+ }))
201
+
202
+ const slotStyle = computed(() => ({
203
+ ...placementStyle.value,
204
+ ...themeVars('slot', resolvedSurface.value),
205
+ }))
206
+
207
+ function readDecorToken(value: unknown) {
208
+ return typeof value === 'string' && value.trim() ? value.trim() : ''
209
+ }
210
+
211
+ const slideIndex = computed(() => {
212
+ const nav = $slidev?.nav as { currentPage?: unknown } | undefined
213
+ const currentPage = Number(nav?.currentPage)
214
+
215
+ return Number.isFinite(currentPage) ? String(currentPage) : ''
216
+ })
217
+
218
+ const decorSlotIdentity = computed(() => {
219
+ if (props.label)
220
+ return props.label
221
+
222
+ if (props.area)
223
+ return props.area
224
+
225
+ if (props.col || props.row)
226
+ return `${props.col}|${props.row}`
227
+
228
+ if (resolvedRect.value) {
229
+ return `${props.role || 'slot'}|${resolvedRect.value.colStart}-${resolvedRect.value.rowStart}-${resolvedRect.value.colEnd}-${resolvedRect.value.rowEnd}`
230
+ }
231
+
232
+ return 'slot'
233
+ })
234
+
235
+ const decorSeed = computed(() => [
236
+ deckTitle.value,
237
+ slideIndex.value,
238
+ decorSlotIdentity.value,
239
+ readDecorToken(props.decor?.id),
240
+ readDecorToken(props.decor?.meaning),
241
+ readDecorToken(props.decor?.tone),
242
+ ].filter(Boolean).join('|'))
243
+
244
+ const media = computed(() => createThemeMedia({
245
+ themeCatalog: decors.value,
246
+ warn: (message: string) => console.warn(message),
247
+ }))
248
+
249
+ const backgroundLayers = computed(() =>
250
+ media.value.resolveSlotLayers({
251
+ background: props.background,
252
+ decor: props.decor,
253
+ }, {
254
+ label: props.label || slotId,
255
+ footprint: resolvedFootprint.value,
256
+ seed: decorSeed.value,
257
+ tone: resolvedSurface.value.tone,
258
+ contrast: resolvedSurface.value.surface === 'dark' || resolvedSurface.value.surface === 'color',
259
+ }),
260
+ )
261
+
262
+ function resolveMarginCssValue(value: SlotMarginInput | undefined) {
263
+ if (value == null)
264
+ return undefined
265
+
266
+ const normalizedValue = String(value) as `${SlotMarginLevel}`
267
+ if (!SLOT_MARGIN_LEVELS.has(normalizedValue)) {
268
+ throw new Error(`[${props.label || slotId}] margin должен быть уровнем 1..8. Получено: "${String(value)}".`)
269
+ }
270
+
271
+ return `var(--theme-slot-margin-${normalizedValue})`
272
+ }
273
+
274
+ function resolveGapCssValue(value: SlotGapInput | undefined) {
275
+ if (value == null)
276
+ return '0'
277
+
278
+ const normalizedValue = String(value) as `${SlotGap}`
279
+ if (normalizedValue === '0')
280
+ return '0'
281
+
282
+ if (!SLOT_GAP_LEVELS.has(normalizedValue)) {
283
+ throw new Error(`[${props.label || slotId}] gap должен быть уровнем 0..7. Получено: "${String(value)}".`)
284
+ }
285
+
286
+ return `var(--theme-slot-margin-${normalizedValue})`
287
+ }
288
+
289
+ const marginStyle = computed(() => {
290
+ const layoutSlot = resolvedLayoutSlot.value
291
+ const margin = layoutSlot?.margin ?? props.margin
292
+ const style = {
293
+ paddingTop: resolveMarginCssValue(layoutSlot?.marginTop ?? props.marginTop ?? margin),
294
+ paddingRight: resolveMarginCssValue(layoutSlot?.marginRight ?? props.marginRight ?? margin),
295
+ paddingBottom: resolveMarginCssValue(layoutSlot?.marginBottom ?? props.marginBottom ?? margin),
296
+ paddingLeft: resolveMarginCssValue(layoutSlot?.marginLeft ?? props.marginLeft ?? margin),
297
+ }
298
+
299
+ if (Object.values(style).every(value => value == null))
300
+ return null
301
+
302
+ return style
303
+ })
304
+
305
+ const contentStyle = computed(() => ({
306
+ ...(marginStyle.value ?? {}),
307
+ '--theme-slot-gap': resolveGapCssValue(props.gap),
308
+ }))
309
+
310
+ const alignmentClass = computed(() => ({
311
+ Slot_centered: resolvedLayoutSlot.value?.centered ?? props.centered,
312
+ }))
313
+
314
+ const surfaceClass = computed(() => {
315
+ if (resolvedSurface.value.surface === 'none')
316
+ return null
317
+
318
+ return `Slot_surface_${resolvedSurface.value.surface}`
319
+ })
320
+ </script>
321
+
322
+ <template>
323
+ <component :is="as"
324
+ class="Slot"
325
+ :class="[
326
+ alignmentClass,
327
+ surfaceClass,
328
+ ]"
329
+ :style="slotStyle">
330
+ <div v-if="backgroundLayers.length" class="Slot-Background" aria-hidden="true">
331
+ <ImageRenderer v-for="backgroundLayer in backgroundLayers"
332
+ :key="backgroundLayer.key"
333
+ class="Slot-BackgroundLayer"
334
+ :layer="backgroundLayer" />
335
+ </div>
336
+ <SlotContent :content-style="contentStyle">
337
+ <slot />
338
+ </SlotContent>
339
+ </component>
340
+ </template>
341
+
342
+ <style scoped>
343
+ .Slot {
344
+ position: relative;
345
+ min-width: 0;
346
+ min-height: 0;
347
+ display: flex;
348
+ flex-direction: column;
349
+ border-radius: var(--theme-panel-radius);
350
+ }
351
+
352
+ .Slot-Background {
353
+ position: absolute;
354
+ inset: 0;
355
+ z-index: 0;
356
+ pointer-events: none;
357
+ overflow: hidden;
358
+ border-radius: inherit;
359
+ }
360
+
361
+ .Slot-BackgroundLayer {
362
+ position: absolute;
363
+ inset: 0;
364
+ }
365
+
366
+ .Slot-Content {
367
+ position: relative;
368
+ z-index: 1;
369
+ display: flex;
370
+ flex: 1;
371
+ min-width: 0;
372
+ min-height: 0;
373
+ flex-direction: column;
374
+ border-radius: inherit;
375
+ gap: var(--theme-slot-gap, 0);
376
+ align-items: stretch;
377
+ }
378
+
379
+ .Slot_surface_none {
380
+ background: transparent;
381
+ }
382
+
383
+ .Slot_surface_light {
384
+ background: var(--theme-surface-light);
385
+ --theme-text: var(--theme-color-dark-0);
386
+ --theme-text-muted: var(--theme-color-dark-2);
387
+ --theme-inline-code-text: var(--theme-text);
388
+ color: var(--theme-text);
389
+ }
390
+
391
+ .Slot_surface_dark {
392
+ background: var(--theme-surface-dark);
393
+ --theme-text: var(--theme-text-on-dark);
394
+ --theme-text-muted: var(--theme-text-muted-on-contrast);
395
+ --theme-inline-code-text: var(--theme-text);
396
+ color: var(--theme-text);
397
+ }
398
+
399
+ .Slot_surface_color {
400
+ background: var(--theme-slot-color, var(--theme-current-color));
401
+ --theme-text: var(--theme-text-on-dark);
402
+ --theme-text-muted: var(--theme-text-muted-on-contrast);
403
+ --theme-inline-code-text: var(--theme-text);
404
+ color: var(--theme-text);
405
+ }
406
+
407
+ .Slot_surface_tint {
408
+ background: var(--theme-slot-tint, var(--theme-surface-light));
409
+ --theme-text: var(--theme-color-dark-0);
410
+ --theme-text-muted: var(--theme-color-dark-2);
411
+ --theme-inline-code-text: var(--theme-text);
412
+ color: var(--theme-text);
413
+ }
414
+
415
+ .Slot_centered .Slot-Content {
416
+ align-items: center;
417
+ text-align: center;
418
+ }
419
+ </style>
@@ -0,0 +1,216 @@
1
+ <script setup lang="ts">
2
+ import { computed, shallowRef } from 'vue'
3
+ import {
4
+ createTextFitRuntime,
5
+ parseThemeTextSizeRange,
6
+ type ThemeTextSize,
7
+ type ThemeTextSizeInput,
8
+ } from '../composables/text-fit-runtime'
9
+
10
+ type ThemeTextPriority = 1 | 2 | 3
11
+ type ThemeTextPriorityInput = ThemeTextPriority | `${ThemeTextPriority}`
12
+ type TextFlowAlign = 'start' | 'middle' | 'end'
13
+ type TextColorToken =
14
+ | 'current'
15
+ | 'text'
16
+ | 'text-muted'
17
+ | 'text-on-dark'
18
+ | 'text-muted-on-contrast'
19
+ | 'link'
20
+ | 'light-0'
21
+ | 'light-1'
22
+ | 'dark-0'
23
+ | 'dark-1'
24
+ | 'dark-2'
25
+ | 'blue-0'
26
+ | 'blue-1'
27
+ | 'orange-0'
28
+ | 'orange-1'
29
+ | 'green-0'
30
+ | 'green-1'
31
+ | 'red-0'
32
+ | 'yellow-0'
33
+
34
+ const TEXT_COLOR_MAP: Record<TextColorToken, string> = {
35
+ 'current': 'currentColor',
36
+ 'text': 'var(--theme-text)',
37
+ 'text-muted': 'var(--theme-text-muted)',
38
+ 'text-on-dark': 'var(--theme-text-on-dark)',
39
+ 'text-muted-on-contrast': 'var(--theme-text-muted-on-contrast)',
40
+ 'link': 'var(--theme-link)',
41
+ 'light-0': 'var(--theme-color-light-0)',
42
+ 'light-1': 'var(--theme-color-light-1)',
43
+ 'dark-0': 'var(--theme-color-dark-0)',
44
+ 'dark-1': 'var(--theme-color-dark-1)',
45
+ 'dark-2': 'var(--theme-color-dark-2)',
46
+ 'blue-0': 'var(--theme-color-blue-0)',
47
+ 'blue-1': 'var(--theme-color-blue-1)',
48
+ 'orange-0': 'var(--theme-color-orange-0)',
49
+ 'orange-1': 'var(--theme-color-orange-1)',
50
+ 'green-0': 'var(--theme-color-green-0)',
51
+ 'green-1': 'var(--theme-color-green-1)',
52
+ 'red-0': 'var(--theme-color-red-0)',
53
+ 'yellow-0': 'var(--theme-color-yellow-0)',
54
+ }
55
+
56
+ const props = withDefaults(defineProps<{
57
+ as?: string
58
+ size?: ThemeTextSizeInput
59
+ priority?: ThemeTextPriorityInput
60
+ align?: TextFlowAlign
61
+ maxSize?: boolean
62
+ muted?: boolean
63
+ color?: TextColorToken
64
+ }>(), {
65
+ as: 'div',
66
+ size: '2',
67
+ priority: '1',
68
+ align: 'start',
69
+ maxSize: false,
70
+ muted: false,
71
+ color: 'current',
72
+ })
73
+
74
+ const textElement = shallowRef<HTMLElement | null>(null)
75
+ const normalizedSizeRange = computed(() => parseThemeTextSizeRange(props.size, 2))
76
+ const fitMax = computed(() => props.maxSize || normalizedSizeRange.value.isRange)
77
+ const fitMinSize = computed<ThemeTextSize>(() => props.maxSize ? 0 : normalizedSizeRange.value.minSize)
78
+ const fitMaxSize = computed<ThemeTextSize>(() => props.maxSize ? 12 : normalizedSizeRange.value.maxSize)
79
+ const fitFallbackSize = computed<ThemeTextSize>(() => normalizedSizeRange.value.minSize)
80
+ const textFit = createTextFitRuntime()
81
+ const { size: fittedSize } = textFit.useElement({
82
+ target: textElement,
83
+ enabled: fitMax,
84
+ fallbackSize: fitFallbackSize,
85
+ minSize: fitMinSize,
86
+ maxSize: fitMaxSize,
87
+ watchSources: [
88
+ () => props.size,
89
+ () => props.maxSize,
90
+ () => props.priority,
91
+ ],
92
+ })
93
+
94
+ const normalizedSize = computed(() => {
95
+ if (fitMax.value)
96
+ return fittedSize.value
97
+
98
+ return normalizedSizeRange.value.minSize
99
+ })
100
+
101
+ const resolvedColor = computed<TextColorToken>(() => props.muted ? 'text-muted' : props.color)
102
+
103
+ const textStyle = computed(() => ({
104
+ color: TEXT_COLOR_MAP[resolvedColor.value],
105
+ }))
106
+ </script>
107
+
108
+ <template>
109
+ <component :is="as"
110
+ ref="textElement"
111
+ class="Text"
112
+ :class="[
113
+ `Text_size_${normalizedSize}`,
114
+ {
115
+ Text_align_start: props.align === 'start',
116
+ Text_align_middle: props.align === 'middle',
117
+ Text_align_end: props.align === 'end',
118
+ Text_fit_max: fitMax,
119
+ Text_muted: props.muted,
120
+ },
121
+ ]"
122
+ :style="textStyle">
123
+ <slot />
124
+ </component>
125
+ </template>
126
+
127
+ <style scoped>
128
+ .Text {
129
+ min-width: 0;
130
+ margin: 0;
131
+ font-weight: 400;
132
+ }
133
+
134
+ .Text_fit_max {
135
+ width: 100%;
136
+ }
137
+
138
+ .Text_align_start {
139
+ margin-top: 0;
140
+ margin-bottom: 0;
141
+ }
142
+
143
+ .Text_align_middle {
144
+ margin-top: auto;
145
+ margin-bottom: auto;
146
+ }
147
+
148
+ .Text_align_end {
149
+ margin-top: auto;
150
+ }
151
+
152
+ .Text_size_0 {
153
+ font-size: var(--theme-text-size-0);
154
+ line-height: var(--theme-text-line-0);
155
+ }
156
+
157
+ .Text_size_1 {
158
+ font-size: var(--theme-text-size-1);
159
+ line-height: var(--theme-text-line-1);
160
+ }
161
+
162
+ .Text_size_2 {
163
+ font-size: var(--theme-text-size-2);
164
+ line-height: var(--theme-text-line-2);
165
+ }
166
+
167
+ .Text_size_3 {
168
+ font-size: var(--theme-text-size-3);
169
+ line-height: var(--theme-text-line-3);
170
+ }
171
+
172
+ .Text_size_4 {
173
+ font-size: var(--theme-text-size-4);
174
+ line-height: var(--theme-text-line-4);
175
+ }
176
+
177
+ .Text_size_5 {
178
+ font-size: var(--theme-text-size-5);
179
+ line-height: var(--theme-text-line-5);
180
+ }
181
+
182
+ .Text_size_6 {
183
+ font-size: var(--theme-text-size-6);
184
+ line-height: var(--theme-text-line-6);
185
+ }
186
+
187
+ .Text_size_7 {
188
+ font-size: var(--theme-text-size-7);
189
+ line-height: var(--theme-text-line-7);
190
+ }
191
+
192
+ .Text_size_8 {
193
+ font-size: var(--theme-text-size-8);
194
+ line-height: var(--theme-text-line-8);
195
+ }
196
+
197
+ .Text_size_9 {
198
+ font-size: var(--theme-text-size-9);
199
+ line-height: var(--theme-text-line-9);
200
+ }
201
+
202
+ .Text_size_10 {
203
+ font-size: var(--theme-text-size-10);
204
+ line-height: var(--theme-text-line-10);
205
+ }
206
+
207
+ .Text_size_11 {
208
+ font-size: var(--theme-text-size-11);
209
+ line-height: var(--theme-text-line-11);
210
+ }
211
+
212
+ .Text_size_12 {
213
+ font-size: var(--theme-text-size-12);
214
+ line-height: var(--theme-text-line-12);
215
+ }
216
+ </style>