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,577 @@
1
+ import {
2
+ Comment,
3
+ Fragment,
4
+ Text as VueText,
5
+ cloneVNode,
6
+ h,
7
+ nextTick,
8
+ onBeforeUnmount,
9
+ onMounted,
10
+ onUpdated,
11
+ ref,
12
+ shallowRef,
13
+ watch,
14
+ type Ref,
15
+ type VNode,
16
+ type WatchSource,
17
+ } from 'vue'
18
+ import { createPriorityTextSizeCandidates } from './text-priority-fit.mjs'
19
+
20
+ export const THEME_TEXT_SIZE_LEVELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const
21
+
22
+ export type ThemeTextSize = typeof THEME_TEXT_SIZE_LEVELS[number]
23
+ export type ThemeTextSizeTokenInput = ThemeTextSize | `${ThemeTextSize}`
24
+ export type ThemeTextSizeRangeInput = `${ThemeTextSize}-${ThemeTextSize}`
25
+ export type ThemeTextSizeInput = ThemeTextSizeTokenInput | ThemeTextSizeRangeInput
26
+
27
+ export type ThemeTextSizeRange = {
28
+ minSize: ThemeTextSize
29
+ maxSize: ThemeTextSize
30
+ isRange: boolean
31
+ }
32
+
33
+ export type FitBounds = {
34
+ width: number
35
+ height: number
36
+ }
37
+
38
+ export type TextFitMeasurement = {
39
+ fits: boolean
40
+ }
41
+
42
+ export type TextFitAdapter = {
43
+ measure(element: HTMLElement, bounds: FitBounds): TextFitMeasurement
44
+ observe(elements: readonly HTMLElement[], onChange: () => void): () => void
45
+ schedule(callback: () => void): () => void
46
+ afterFontsReady?(callback: () => void): void
47
+ }
48
+
49
+ type MaybeRef<T> = T | Ref<T>
50
+
51
+ type ElementFitInput = {
52
+ target: Ref<HTMLElement | null>
53
+ enabled: Ref<boolean>
54
+ fallbackSize: MaybeRef<ThemeTextSize>
55
+ maxSize?: MaybeRef<ThemeTextSize>
56
+ minSize?: MaybeRef<ThemeTextSize>
57
+ resolveBounds?: () => FitBounds | null
58
+ watchSources?: WatchSource<unknown>[]
59
+ }
60
+
61
+ type TextFitHandle = {
62
+ size: Ref<ThemeTextSize>
63
+ refit: () => void
64
+ }
65
+
66
+ type GroupFitInput = {
67
+ target: Ref<HTMLElement | null>
68
+ nodes: () => VNode[]
69
+ maxItems?: number
70
+ }
71
+
72
+ type TextGroupFitHandle = {
73
+ apply(nodes: VNode[]): VNode[]
74
+ refit: () => void
75
+ }
76
+
77
+ export type TextFitRuntime = {
78
+ useElement(input: ElementFitInput): TextFitHandle
79
+ useGroup(input: GroupFitInput): TextGroupFitHandle
80
+ }
81
+
82
+ type SlotTextFitItem = {
83
+ minSize: ThemeTextSize
84
+ maxSize: ThemeTextSize
85
+ priority: number
86
+ }
87
+
88
+ type SlotTextFitGroup = {
89
+ initialSizes: number[]
90
+ items: SlotTextFitItem[]
91
+ signature: string
92
+ }
93
+
94
+ type SlotTextFitSnapshot = {
95
+ signature: string
96
+ sizes: number[]
97
+ }
98
+
99
+ const THEME_TEXT_SIZE_SET = new Set<string>(THEME_TEXT_SIZE_LEVELS.map(String))
100
+ const DEFAULT_MAX_TEXT_SIZE = THEME_TEXT_SIZE_LEVELS[THEME_TEXT_SIZE_LEVELS.length - 1]
101
+ const FIT_TOLERANCE = 1
102
+
103
+ function readMaybeRef<T>(value: MaybeRef<T>) {
104
+ if (typeof value === 'object' && value !== null && 'value' in value)
105
+ return value.value
106
+
107
+ return value
108
+ }
109
+
110
+ export function normalizeThemeTextSize(value: unknown, fallbackSize: ThemeTextSize): ThemeTextSize {
111
+ const normalizedValue = String(value)
112
+
113
+ if (!THEME_TEXT_SIZE_SET.has(normalizedValue))
114
+ return fallbackSize
115
+
116
+ return Number(normalizedValue) as ThemeTextSize
117
+ }
118
+
119
+ export function parseThemeTextSizeRange(value: unknown, fallbackSize: ThemeTextSize): ThemeTextSizeRange {
120
+ const normalizedValue = String(value)
121
+ const rangeMatch = normalizedValue.match(/^(\d+)-(\d+)$/)
122
+
123
+ if (!rangeMatch) {
124
+ const size = normalizeThemeTextSize(value, fallbackSize)
125
+
126
+ return {
127
+ minSize: size,
128
+ maxSize: size,
129
+ isRange: false,
130
+ }
131
+ }
132
+
133
+ const minSize = normalizeThemeTextSize(rangeMatch[1], fallbackSize)
134
+ const maxSize = normalizeThemeTextSize(rangeMatch[2], fallbackSize)
135
+
136
+ if (String(minSize) !== rangeMatch[1] || String(maxSize) !== rangeMatch[2] || minSize > maxSize) {
137
+ return {
138
+ minSize: fallbackSize,
139
+ maxSize: fallbackSize,
140
+ isRange: false,
141
+ }
142
+ }
143
+
144
+ return {
145
+ minSize,
146
+ maxSize,
147
+ isRange: true,
148
+ }
149
+ }
150
+
151
+ function defaultBounds(target: Ref<HTMLElement | null>) {
152
+ const parent = target.value?.parentElement
153
+
154
+ if (!parent)
155
+ return null
156
+
157
+ const parentStyle = getComputedStyle(parent)
158
+ const horizontalPadding = Number.parseFloat(parentStyle.paddingLeft) + Number.parseFloat(parentStyle.paddingRight)
159
+ const verticalPadding = Number.parseFloat(parentStyle.paddingTop) + Number.parseFloat(parentStyle.paddingBottom)
160
+
161
+ return {
162
+ width: parent.clientWidth - horizontalPadding,
163
+ height: parent.clientHeight - verticalPadding,
164
+ }
165
+ }
166
+
167
+ function readBounds(input: ElementFitInput) {
168
+ const bounds = input.resolveBounds?.() ?? defaultBounds(input.target)
169
+
170
+ if (!bounds || bounds.width <= 0 || bounds.height <= 0)
171
+ return null
172
+
173
+ return bounds
174
+ }
175
+
176
+ function createBrowserTextFitAdapter(): TextFitAdapter {
177
+ return {
178
+ measure(element, bounds) {
179
+ return {
180
+ fits: element.scrollWidth <= Math.ceil(bounds.width) + FIT_TOLERANCE
181
+ && element.scrollHeight <= Math.ceil(bounds.height) + FIT_TOLERANCE,
182
+ }
183
+ },
184
+ observe(elements, onChange) {
185
+ const resizeObserver = typeof globalThis.ResizeObserver === 'function'
186
+ ? new globalThis.ResizeObserver(onChange)
187
+ : null
188
+ const mutationObserver = typeof globalThis.MutationObserver === 'function'
189
+ ? new globalThis.MutationObserver(onChange)
190
+ : null
191
+
192
+ for (const element of elements)
193
+ resizeObserver?.observe(element)
194
+
195
+ if (elements[0]) {
196
+ mutationObserver?.observe(elements[0], {
197
+ characterData: true,
198
+ childList: true,
199
+ subtree: true,
200
+ })
201
+ }
202
+
203
+ return () => {
204
+ resizeObserver?.disconnect()
205
+ mutationObserver?.disconnect()
206
+ }
207
+ },
208
+ schedule(callback) {
209
+ if (typeof globalThis.requestAnimationFrame !== 'function') {
210
+ callback()
211
+ return () => {}
212
+ }
213
+
214
+ const frame = globalThis.requestAnimationFrame(callback)
215
+ return () => globalThis.cancelAnimationFrame(frame)
216
+ },
217
+ afterFontsReady(callback) {
218
+ const fontReady = globalThis.document?.fonts?.ready
219
+ if (fontReady)
220
+ void fontReady.then(callback)
221
+ },
222
+ }
223
+ }
224
+
225
+ function isBlankSlotNode(node: VNode) {
226
+ if (node.type === Comment)
227
+ return true
228
+
229
+ return node.type === VueText
230
+ && typeof node.children === 'string'
231
+ && !node.children.trim()
232
+ }
233
+
234
+ function isFragmentSlotNode(node: VNode) {
235
+ return node.type === Fragment && Array.isArray(node.children)
236
+ }
237
+
238
+ function readVNodeProp(node: VNode, key: string) {
239
+ const props = (node.props ?? {}) as Record<string, unknown>
240
+ return props[key]
241
+ }
242
+
243
+ function isTextSlotNode(node: VNode) {
244
+ if (typeof node.type !== 'object' || node.type == null)
245
+ return false
246
+
247
+ const component = node.type as { name?: string, __name?: string }
248
+ return component.name === 'Text' || component.__name === 'Text'
249
+ }
250
+
251
+ function flattenContentNodes(nodes: VNode[]): VNode[] {
252
+ return nodes.flatMap((node) => {
253
+ if (isFragmentSlotNode(node))
254
+ return flattenContentNodes(node.children as VNode[])
255
+
256
+ return isBlankSlotNode(node) ? [] : [node]
257
+ })
258
+ }
259
+
260
+ function normalizeTextPriority(value: unknown) {
261
+ const priority = Number(value ?? 1)
262
+
263
+ if (!Number.isInteger(priority) || priority < 1)
264
+ return 1
265
+
266
+ return priority
267
+ }
268
+
269
+ function firstTextFitCandidate(items: SlotTextFitItem[]) {
270
+ const first = createPriorityTextSizeCandidates(items).next()
271
+
272
+ if (first.done || !first.value)
273
+ throw new Error('[Text] size ranges cannot satisfy priority hierarchy.')
274
+
275
+ return first.value
276
+ }
277
+
278
+ function readSlotTextFitGroup(nodes: VNode[], maxItems: number): SlotTextFitGroup | null {
279
+ const directNodes = flattenContentNodes(nodes)
280
+
281
+ if (!directNodes.length || !directNodes.every(isTextSlotNode))
282
+ return null
283
+
284
+ const items: SlotTextFitItem[] = directNodes.map((node) => {
285
+ const sizeRange = parseThemeTextSizeRange(readVNodeProp(node, 'size') ?? '2', 2)
286
+
287
+ return {
288
+ minSize: sizeRange.minSize,
289
+ maxSize: sizeRange.maxSize,
290
+ priority: normalizeTextPriority(readVNodeProp(node, 'priority')),
291
+ }
292
+ })
293
+
294
+ if (!items.some(item => item.minSize !== item.maxSize))
295
+ return null
296
+
297
+ if (items.length > maxItems)
298
+ throw new Error(`[Slot] coordinated text fitting supports up to ${maxItems} direct Text children.`)
299
+
300
+ return {
301
+ initialSizes: firstTextFitCandidate(items),
302
+ items,
303
+ signature: items.map(item => `${item.minSize}-${item.maxSize}:${item.priority}`).join('|'),
304
+ }
305
+ }
306
+
307
+ function applySlotTextFit(nodes: VNode[], group: SlotTextFitGroup | null, sizes: number[] | null) {
308
+ if (!group || !sizes)
309
+ return nodes
310
+
311
+ const resolvedSizes = sizes
312
+ let childNumber = 0
313
+
314
+ function applyNode(node: VNode): VNode {
315
+ if (isFragmentSlotNode(node))
316
+ return h(Fragment, null, (node.children as VNode[]).map(applyNode))
317
+
318
+ if (isBlankSlotNode(node))
319
+ return node
320
+
321
+ childNumber += 1
322
+
323
+ if (!isTextSlotNode(node))
324
+ return node
325
+
326
+ const size = resolvedSizes[childNumber - 1]
327
+
328
+ return cloneVNode(node, {
329
+ size: String(size),
330
+ })
331
+ }
332
+
333
+ return nodes.map(applyNode)
334
+ }
335
+
336
+ function sizesEqual(left: number[], right: number[]) {
337
+ return left.length === right.length && left.every((size, index) => size === right[index])
338
+ }
339
+
340
+ function textChildrenFit(element: HTMLElement, adapter: TextFitAdapter) {
341
+ return Array.from(element.children).every((child) => {
342
+ if (!(child instanceof HTMLElement) || !child.classList.contains('Text'))
343
+ return true
344
+
345
+ return adapter.measure(child, {
346
+ width: child.clientWidth,
347
+ height: Number.POSITIVE_INFINITY,
348
+ }).fits
349
+ })
350
+ }
351
+
352
+ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserTextFitAdapter()): TextFitRuntime {
353
+ return {
354
+ useElement(input: ElementFitInput) {
355
+ const size = ref<ThemeTextSize>(readMaybeRef(input.fallbackSize))
356
+ let runId = 0
357
+ let cancelSchedule: (() => void) | null = null
358
+ let disconnectObservers: (() => void) | null = null
359
+
360
+ async function fitNow() {
361
+ const currentRunId = ++runId
362
+ const element = input.target.value
363
+
364
+ if (!input.enabled.value) {
365
+ size.value = readMaybeRef(input.fallbackSize)
366
+ return
367
+ }
368
+
369
+ if (!element)
370
+ return
371
+
372
+ const resolvedMinSize = readMaybeRef(input.minSize ?? 0)
373
+ const resolvedMaxSize = input.maxSize == null ? DEFAULT_MAX_TEXT_SIZE : readMaybeRef(input.maxSize)
374
+ const candidateSizes = THEME_TEXT_SIZE_LEVELS
375
+ .filter(candidateSize => candidateSize >= resolvedMinSize && candidateSize <= resolvedMaxSize)
376
+ .slice()
377
+ .reverse()
378
+
379
+ let resolvedSize = candidateSizes[candidateSizes.length - 1] ?? readMaybeRef(input.fallbackSize)
380
+
381
+ for (const candidateSize of candidateSizes) {
382
+ size.value = candidateSize
383
+ await nextTick()
384
+
385
+ if (currentRunId !== runId || !input.enabled.value)
386
+ return
387
+
388
+ const bounds = readBounds(input)
389
+
390
+ if (!bounds)
391
+ return
392
+
393
+ if (adapter.measure(element, bounds).fits) {
394
+ resolvedSize = candidateSize
395
+ break
396
+ }
397
+ }
398
+
399
+ size.value = resolvedSize
400
+ }
401
+
402
+ function scheduleFit() {
403
+ cancelSchedule?.()
404
+ cancelSchedule = adapter.schedule(() => {
405
+ cancelSchedule = null
406
+ void fitNow()
407
+ })
408
+ }
409
+
410
+ function reconnectObservers() {
411
+ disconnectObservers?.()
412
+ disconnectObservers = null
413
+
414
+ const element = input.target.value
415
+ if (!element)
416
+ return
417
+
418
+ const observedElements = element.parentElement ? [element, element.parentElement] : [element]
419
+ disconnectObservers = adapter.observe(observedElements, scheduleFit)
420
+ }
421
+
422
+ onMounted(async () => {
423
+ await nextTick()
424
+ reconnectObservers()
425
+ scheduleFit()
426
+ adapter.afterFontsReady?.(scheduleFit)
427
+ })
428
+
429
+ watch(input.target, async () => {
430
+ await nextTick()
431
+ reconnectObservers()
432
+ scheduleFit()
433
+ }, { flush: 'post' })
434
+
435
+ watch(input.enabled, (isEnabled) => {
436
+ if (isEnabled)
437
+ scheduleFit()
438
+ else
439
+ size.value = readMaybeRef(input.fallbackSize)
440
+ }, { flush: 'post' })
441
+
442
+ if (input.watchSources && input.watchSources.length > 0) {
443
+ watch(input.watchSources, () => {
444
+ scheduleFit()
445
+ }, { flush: 'post' })
446
+ }
447
+
448
+ onBeforeUnmount(() => {
449
+ cancelSchedule?.()
450
+ disconnectObservers?.()
451
+ runId += 1
452
+ })
453
+
454
+ return {
455
+ size,
456
+ refit: scheduleFit,
457
+ }
458
+ },
459
+ useGroup(input: GroupFitInput) {
460
+ const maxItems = input.maxItems ?? 3
461
+ const fitSnapshot = shallowRef<SlotTextFitSnapshot | null>(null)
462
+ let currentFitGroup: SlotTextFitGroup | null = null
463
+ let runId = 0
464
+ let cancelSchedule: (() => void) | null = null
465
+ let disconnectObservers: (() => void) | null = null
466
+ let pendingInternalSignature: string | null | undefined
467
+
468
+ function setFitSnapshot(signature: string, sizes: number[]) {
469
+ if (fitSnapshot.value?.signature === signature && sizesEqual(fitSnapshot.value.sizes, sizes))
470
+ return
471
+
472
+ pendingInternalSignature = signature
473
+ fitSnapshot.value = {
474
+ signature,
475
+ sizes,
476
+ }
477
+ }
478
+
479
+ function clearFitSnapshot() {
480
+ if (fitSnapshot.value) {
481
+ pendingInternalSignature = null
482
+ fitSnapshot.value = null
483
+ }
484
+ }
485
+
486
+ async function fitNow() {
487
+ const currentRunId = ++runId
488
+ const group = currentFitGroup ?? readSlotTextFitGroup(input.nodes(), maxItems)
489
+ const element = input.target.value
490
+
491
+ if (!group || !element) {
492
+ clearFitSnapshot()
493
+ return
494
+ }
495
+
496
+ let fallbackSizes = group.initialSizes
497
+
498
+ for (const sizes of createPriorityTextSizeCandidates(group.items)) {
499
+ fallbackSizes = sizes
500
+ setFitSnapshot(group.signature, sizes)
501
+
502
+ await nextTick()
503
+
504
+ if (currentRunId !== runId || currentFitGroup?.signature !== group.signature)
505
+ return
506
+
507
+ const ownBounds = {
508
+ width: element.clientWidth,
509
+ height: element.clientHeight,
510
+ }
511
+
512
+ if (adapter.measure(element, ownBounds).fits && textChildrenFit(element, adapter))
513
+ return
514
+ }
515
+
516
+ setFitSnapshot(group.signature, fallbackSizes)
517
+ }
518
+
519
+ function scheduleFit() {
520
+ cancelSchedule?.()
521
+ cancelSchedule = adapter.schedule(() => {
522
+ cancelSchedule = null
523
+ void fitNow()
524
+ })
525
+ }
526
+
527
+ function reconnectObservers() {
528
+ disconnectObservers?.()
529
+ disconnectObservers = null
530
+
531
+ const element = input.target.value
532
+ if (!element)
533
+ return
534
+
535
+ disconnectObservers = adapter.observe([element], scheduleFit)
536
+ }
537
+
538
+ onMounted(async () => {
539
+ await nextTick()
540
+ reconnectObservers()
541
+ scheduleFit()
542
+ adapter.afterFontsReady?.(scheduleFit)
543
+ })
544
+
545
+ onUpdated(() => {
546
+ const internalSignature = pendingInternalSignature
547
+ pendingInternalSignature = undefined
548
+
549
+ if (internalSignature !== undefined && internalSignature === (currentFitGroup?.signature ?? null))
550
+ return
551
+
552
+ scheduleFit()
553
+ })
554
+
555
+ onBeforeUnmount(() => {
556
+ cancelSchedule?.()
557
+ disconnectObservers?.()
558
+ runId += 1
559
+ })
560
+
561
+ function apply(nodes: VNode[]) {
562
+ const group = readSlotTextFitGroup(nodes, maxItems)
563
+ currentFitGroup = group
564
+ const fittedSizes = group && fitSnapshot.value?.signature === group.signature
565
+ ? fitSnapshot.value.sizes
566
+ : group?.initialSizes ?? null
567
+
568
+ return applySlotTextFit(nodes, group, fittedSizes)
569
+ }
570
+
571
+ return {
572
+ apply,
573
+ refit: scheduleFit,
574
+ }
575
+ },
576
+ }
577
+ }