slidev-theme-practicum 0.1.0 → 0.1.2

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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -10
  3. package/components/Slide.vue +81 -26
  4. package/components/Slot.vue +33 -48
  5. package/components/Text.vue +68 -2
  6. package/components/Timeline.vue +2 -23
  7. package/composables/deck-slot-markup.cjs +249 -0
  8. package/composables/decor-catalog.mjs +169 -47
  9. package/composables/layout-authoring.ts +6 -2
  10. package/composables/layout-markdown.mjs +33 -0
  11. package/composables/layout-recipes.ts +51 -5
  12. package/composables/layout-shorthands.ts +251 -81
  13. package/composables/markdown-to-vnodes.cjs +1 -0
  14. package/composables/markdown-to-vnodes.mjs +237 -0
  15. package/composables/slot-placement.ts +1 -1
  16. package/composables/slot-spacing.ts +66 -0
  17. package/composables/text-fit-runtime.ts +458 -76
  18. package/composables/text-fit-scope.ts +13 -0
  19. package/composables/text-slot-markdown.mjs +193 -0
  20. package/composables/text-typography.mjs +2 -0
  21. package/composables/theme-assets.mjs +60 -0
  22. package/composables/timeline-grid.ts +27 -0
  23. package/composables/typescript-require.cjs +29 -0
  24. package/composables/validate-deck-layouts.cjs +277 -0
  25. package/decor-library.md +2 -0
  26. package/example.md +132 -10
  27. package/package.json +25 -11
  28. package/public/photos/photo-1.webp +0 -0
  29. package/public/photos/photo-10.webp +0 -0
  30. package/public/photos/photo-11.webp +0 -0
  31. package/public/photos/photo-12.webp +0 -0
  32. package/public/photos/photo-13.webp +0 -0
  33. package/public/photos/photo-14.webp +0 -0
  34. package/public/photos/photo-15.webp +0 -0
  35. package/public/photos/photo-16.webp +0 -0
  36. package/public/photos/photo-17.webp +0 -0
  37. package/public/photos/photo-18.webp +0 -0
  38. package/public/photos/photo-19.webp +0 -0
  39. package/public/photos/photo-2.webp +0 -0
  40. package/public/photos/photo-20.webp +0 -0
  41. package/public/photos/photo-21.webp +0 -0
  42. package/public/photos/photo-22.webp +0 -0
  43. package/public/photos/photo-23.webp +0 -0
  44. package/public/photos/photo-24.webp +0 -0
  45. package/public/photos/photo-25.webp +0 -0
  46. package/public/photos/photo-26.webp +0 -0
  47. package/public/photos/photo-27.webp +0 -0
  48. package/public/photos/photo-28.webp +0 -0
  49. package/public/photos/photo-29.webp +0 -0
  50. package/public/photos/photo-3.webp +0 -0
  51. package/public/photos/photo-30.webp +0 -0
  52. package/public/photos/photo-4.webp +0 -0
  53. package/public/photos/photo-5.webp +0 -0
  54. package/public/photos/photo-6.webp +0 -0
  55. package/public/photos/photo-7.webp +0 -0
  56. package/public/photos/photo-8.webp +0 -0
  57. package/public/photos/photo-9.webp +0 -0
  58. package/scripts/browser-smoke-runtime.mjs +428 -0
  59. package/scripts/browser-smoke.mjs +424 -0
  60. package/scripts/build-artifact-assets.mjs +55 -0
  61. package/scripts/check-build-artifact.mjs +243 -0
  62. package/scripts/check-package.mjs +132 -0
  63. package/scripts/check-test-architecture.mjs +176 -0
  64. package/scripts/npm-spawn.mjs +31 -0
  65. package/scripts/test-architecture-policy.mjs +245 -0
  66. package/scripts/theme-asset-inventory.mjs +83 -0
  67. package/scripts/validate-deck.cjs +18 -0
  68. package/setup/decor-save-middleware.ts +244 -0
  69. package/setup/vite-plugins.ts +14 -84
  70. package/skills/slidev-practicum/SKILL.md +25 -0
  71. package/skills/slidev-practicum/agents/openai.yaml +4 -0
  72. package/styles/index.css +10 -1
  73. package/composables/layout-registry.ts +0 -47
  74. package/env.d.ts +0 -6
  75. package/public/photos/photo-1.png +0 -0
  76. package/public/photos/photo-10.png +0 -0
  77. package/public/photos/photo-2.png +0 -0
  78. package/public/photos/photo-3.png +0 -0
  79. package/public/photos/photo-4.png +0 -0
  80. package/public/photos/photo-5.png +0 -0
  81. package/public/photos/photo-6.png +0 -0
  82. package/public/photos/photo-7.png +0 -0
  83. package/public/photos/photo-8.png +0 -0
  84. package/public/photos/photo-9.png +0 -0
  85. package/slidev-theme-practicum.png +0 -0
  86. package/slidev-theme-practicum.svg +0 -102
@@ -43,7 +43,7 @@ export type TextFitAdapter = {
43
43
  measure(element: HTMLElement, bounds: FitBounds): TextFitMeasurement
44
44
  observe(elements: readonly HTMLElement[], onChange: () => void): () => void
45
45
  schedule(callback: () => void): () => void
46
- afterFontsReady?(callback: () => void): void
46
+ afterFontsReady?(callback: () => void): () => void
47
47
  }
48
48
 
49
49
  type MaybeRef<T> = T | Ref<T>
@@ -67,6 +67,7 @@ type GroupFitInput = {
67
67
  target: Ref<HTMLElement | null>
68
68
  nodes: () => VNode[]
69
69
  maxItems?: number
70
+ sharedKey?: MaybeRef<string>
70
71
  }
71
72
 
72
73
  type TextGroupFitHandle = {
@@ -96,6 +97,22 @@ type SlotTextFitSnapshot = {
96
97
  sizes: number[]
97
98
  }
98
99
 
100
+ type SharedSlotTextFitItem = {
101
+ target: Ref<HTMLElement | null>
102
+ nodes: () => VNode[]
103
+ maxItems: number
104
+ currentFitGroup: SlotTextFitGroup | null
105
+ pendingInternalSignature: string | null | undefined
106
+ }
107
+
108
+ type SharedSlotTextFitGroup = {
109
+ fitSnapshot: Ref<SlotTextFitSnapshot | null>
110
+ items: Map<symbol, SharedSlotTextFitItem>
111
+ runId: number
112
+ version: number
113
+ }
114
+
115
+ const sharedGroups = new Map<string, SharedSlotTextFitGroup>()
99
116
  const THEME_TEXT_SIZE_SET = new Set<string>(THEME_TEXT_SIZE_LEVELS.map(String))
100
117
  const DEFAULT_MAX_TEXT_SIZE = THEME_TEXT_SIZE_LEVELS[THEME_TEXT_SIZE_LEVELS.length - 1]
101
118
  const FIT_TOLERANCE = 1
@@ -173,6 +190,22 @@ function readBounds(input: ElementFitInput) {
173
190
  return bounds
174
191
  }
175
192
 
193
+ function readFitObserverElements(element: HTMLElement) {
194
+ const elements: HTMLElement[] = [element]
195
+ let parent = element.parentElement
196
+
197
+ while (parent && elements.length < 8) {
198
+ elements.push(parent)
199
+
200
+ if (parent.classList.contains('slidev-page'))
201
+ break
202
+
203
+ parent = parent.parentElement
204
+ }
205
+
206
+ return elements
207
+ }
208
+
176
209
  function createBrowserTextFitAdapter(): TextFitAdapter {
177
210
  return {
178
211
  measure(element, bounds) {
@@ -215,9 +248,34 @@ function createBrowserTextFitAdapter(): TextFitAdapter {
215
248
  return () => globalThis.cancelAnimationFrame(frame)
216
249
  },
217
250
  afterFontsReady(callback) {
251
+ let active = true
252
+ let fallbackTimers: ReturnType<typeof globalThis.setTimeout>[] = []
253
+ const runIfActive = () => {
254
+ if (active)
255
+ callback()
256
+ }
218
257
  const fontReady = globalThis.document?.fonts?.ready
219
- if (fontReady)
220
- void fontReady.then(callback)
258
+
259
+ if (fontReady) {
260
+ void fontReady.then(runIfActive)
261
+ }
262
+ else if (typeof globalThis.setTimeout === 'function') {
263
+ fallbackTimers = [
264
+ globalThis.setTimeout(runIfActive, 250),
265
+ globalThis.setTimeout(runIfActive, 1000),
266
+ ]
267
+ }
268
+
269
+ return () => {
270
+ if (!active)
271
+ return
272
+
273
+ active = false
274
+ for (const timer of fallbackTimers)
275
+ globalThis.clearTimeout(timer)
276
+
277
+ fallbackTimers = []
278
+ }
221
279
  },
222
280
  }
223
281
  }
@@ -275,6 +333,48 @@ function firstTextFitCandidate(items: SlotTextFitItem[]) {
275
333
  return first.value
276
334
  }
277
335
 
336
+ function readSharedGroupKey(value: MaybeRef<string> | undefined) {
337
+ if (value == null)
338
+ return ''
339
+
340
+ return String(readMaybeRef(value)).trim()
341
+ }
342
+
343
+ function createSlotTextFitSignature(items: SlotTextFitItem[]) {
344
+ return items.map(item => `${item.minSize}-${item.maxSize}:${item.priority}`).join('|')
345
+ }
346
+
347
+ function combineSlotTextFitGroups(groups: SlotTextFitGroup[]) {
348
+ if (!groups.length)
349
+ return null
350
+
351
+ const itemCount = groups[0].items.length
352
+
353
+ if (!groups.every(group => group.items.length === itemCount))
354
+ return null
355
+
356
+ const items: SlotTextFitItem[] = groups[0].items.map((firstItem, index) => {
357
+ const groupItems = groups.map(group => group.items[index])
358
+ const minSize = Math.max(...groupItems.map(item => item.minSize)) as ThemeTextSize
359
+ const maxSize = Math.min(...groupItems.map(item => item.maxSize)) as ThemeTextSize
360
+
361
+ return {
362
+ minSize,
363
+ maxSize,
364
+ priority: firstItem.priority,
365
+ }
366
+ })
367
+
368
+ if (items.some(item => item.minSize > item.maxSize))
369
+ throw new Error('[Text] shared size ranges cannot be coordinated because their ranges do not overlap.')
370
+
371
+ return {
372
+ initialSizes: firstTextFitCandidate(items),
373
+ items,
374
+ signature: createSlotTextFitSignature(items),
375
+ }
376
+ }
377
+
278
378
  function readSlotTextFitGroup(nodes: VNode[], maxItems: number): SlotTextFitGroup | null {
279
379
  const directNodes = flattenContentNodes(nodes)
280
380
 
@@ -300,7 +400,7 @@ function readSlotTextFitGroup(nodes: VNode[], maxItems: number): SlotTextFitGrou
300
400
  return {
301
401
  initialSizes: firstTextFitCandidate(items),
302
402
  items,
303
- signature: items.map(item => `${item.minSize}-${item.maxSize}:${item.priority}`).join('|'),
403
+ signature: createSlotTextFitSignature(items),
304
404
  }
305
405
  }
306
406
 
@@ -339,23 +439,255 @@ function sizesEqual(left: number[], right: number[]) {
339
439
 
340
440
  function textChildrenFit(element: HTMLElement, adapter: TextFitAdapter) {
341
441
  return Array.from(element.children).every((child) => {
342
- if (!(child instanceof HTMLElement) || !child.classList.contains('Text'))
442
+ const textElement = child as HTMLElement
443
+
444
+ if (!textElement.classList?.contains('Text'))
343
445
  return true
344
446
 
345
- return adapter.measure(child, {
346
- width: child.clientWidth,
447
+ return adapter.measure(textElement, {
448
+ width: textElement.clientWidth,
347
449
  height: Number.POSITIVE_INFINITY,
348
450
  }).fits
349
451
  })
350
452
  }
351
453
 
454
+ function createTextFitCleanupOwner(isDisposed: () => boolean) {
455
+ let cleanup: (() => void) | null = null
456
+ let generation = 0
457
+
458
+ return {
459
+ replace(register: (release: () => void) => (() => void) | null) {
460
+ const registrationGeneration = ++generation
461
+ const previousCleanup = cleanup
462
+ cleanup = null
463
+ previousCleanup?.()
464
+
465
+ if (isDisposed() || registrationGeneration !== generation)
466
+ return
467
+
468
+ const release = () => {
469
+ if (registrationGeneration !== generation)
470
+ return
471
+
472
+ generation += 1
473
+ cleanup = null
474
+ }
475
+ const nextCleanup = register(release)
476
+
477
+ if (!nextCleanup)
478
+ return
479
+
480
+ if (isDisposed() || registrationGeneration !== generation) {
481
+ nextCleanup()
482
+ return
483
+ }
484
+
485
+ cleanup = nextCleanup
486
+ },
487
+ clear() {
488
+ generation += 1
489
+ const currentCleanup = cleanup
490
+ cleanup = null
491
+ currentCleanup?.()
492
+ },
493
+ }
494
+ }
495
+
496
+ type TextFitLifecycleOwnerInput = {
497
+ adapter: TextFitAdapter
498
+ fit: () => void | Promise<void>
499
+ onDispose: () => void
500
+ target: Ref<HTMLElement | null>
501
+ watchTarget?: boolean
502
+ }
503
+
504
+ function createTextFitLifecycleOwner({
505
+ adapter,
506
+ fit,
507
+ onDispose,
508
+ target,
509
+ watchTarget = false,
510
+ }: TextFitLifecycleOwnerInput) {
511
+ let disposed = false
512
+ const scheduleCleanup = createTextFitCleanupOwner(() => disposed)
513
+ const fontCleanup = createTextFitCleanupOwner(() => disposed)
514
+ const observerCleanup = createTextFitCleanupOwner(() => disposed)
515
+
516
+ function scheduleFit() {
517
+ if (disposed)
518
+ return
519
+
520
+ scheduleCleanup.replace(release => adapter.schedule(() => {
521
+ release()
522
+ if (disposed)
523
+ return
524
+
525
+ void fit()
526
+ }))
527
+ }
528
+
529
+ function reconnectObservers() {
530
+ observerCleanup.replace(() => {
531
+ const element = target.value
532
+ if (!element)
533
+ return null
534
+
535
+ return adapter.observe(readFitObserverElements(element), scheduleFit)
536
+ })
537
+ }
538
+
539
+ onMounted(async () => {
540
+ await nextTick()
541
+ if (disposed)
542
+ return
543
+
544
+ reconnectObservers()
545
+ scheduleFit()
546
+ const afterFontsReady = adapter.afterFontsReady
547
+ fontCleanup.replace(() => afterFontsReady?.(scheduleFit) ?? null)
548
+ })
549
+
550
+ if (watchTarget) {
551
+ watch(target, async () => {
552
+ await nextTick()
553
+ if (disposed)
554
+ return
555
+
556
+ reconnectObservers()
557
+ scheduleFit()
558
+ }, { flush: 'post' })
559
+ }
560
+
561
+ onBeforeUnmount(() => {
562
+ if (disposed)
563
+ return
564
+
565
+ disposed = true
566
+ fontCleanup.clear()
567
+ scheduleCleanup.clear()
568
+ observerCleanup.clear()
569
+ onDispose()
570
+ })
571
+
572
+ return {
573
+ scheduleFit,
574
+ }
575
+ }
576
+
352
577
  export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserTextFitAdapter()): TextFitRuntime {
578
+ function ensureSharedGroup(key: string) {
579
+ let group = sharedGroups.get(key)
580
+
581
+ if (!group) {
582
+ group = {
583
+ fitSnapshot: shallowRef<SlotTextFitSnapshot | null>(null),
584
+ items: new Map(),
585
+ runId: 0,
586
+ version: 0,
587
+ }
588
+ sharedGroups.set(key, group)
589
+ }
590
+
591
+ return group
592
+ }
593
+
594
+ function readSharedFitGroup(group: SharedSlotTextFitGroup) {
595
+ const itemGroups: SlotTextFitGroup[] = []
596
+
597
+ for (const item of group.items.values()) {
598
+ const fitGroup = item.currentFitGroup ?? readSlotTextFitGroup(item.nodes(), item.maxItems)
599
+ item.currentFitGroup = fitGroup
600
+
601
+ if (fitGroup)
602
+ itemGroups.push(fitGroup)
603
+ }
604
+
605
+ return combineSlotTextFitGroups(itemGroups)
606
+ }
607
+
608
+ function setSharedFitSnapshot(group: SharedSlotTextFitGroup, signature: string, sizes: number[]) {
609
+ if (group.fitSnapshot.value?.signature === signature && sizesEqual(group.fitSnapshot.value.sizes, sizes))
610
+ return
611
+
612
+ for (const item of group.items.values())
613
+ item.pendingInternalSignature = signature
614
+
615
+ group.fitSnapshot.value = {
616
+ signature,
617
+ sizes,
618
+ }
619
+ }
620
+
621
+ function clearSharedFitSnapshot(group: SharedSlotTextFitGroup) {
622
+ if (group.fitSnapshot.value) {
623
+ for (const item of group.items.values())
624
+ item.pendingInternalSignature = null
625
+
626
+ group.fitSnapshot.value = null
627
+ }
628
+ }
629
+
630
+ function sharedItemsFit(group: SharedSlotTextFitGroup) {
631
+ for (const item of group.items.values()) {
632
+ const element = item.target.value
633
+
634
+ if (!element)
635
+ return false
636
+
637
+ const ownBounds = {
638
+ width: element.clientWidth,
639
+ height: element.clientHeight,
640
+ }
641
+
642
+ if (!adapter.measure(element, ownBounds).fits || !textChildrenFit(element, adapter))
643
+ return false
644
+ }
645
+
646
+ return true
647
+ }
648
+
649
+ async function fitSharedNow(key: string) {
650
+ const groupState = sharedGroups.get(key)
651
+
652
+ if (!groupState) {
653
+ return
654
+ }
655
+
656
+ const currentRunId = ++groupState.runId
657
+ const group = readSharedFitGroup(groupState)
658
+
659
+ if (!group) {
660
+ clearSharedFitSnapshot(groupState)
661
+ return
662
+ }
663
+
664
+ let fallbackSizes = group.initialSizes
665
+
666
+ for (const sizes of createPriorityTextSizeCandidates(group.items)) {
667
+ fallbackSizes = sizes
668
+ setSharedFitSnapshot(groupState, group.signature, sizes)
669
+
670
+ await nextTick()
671
+
672
+ if (currentRunId !== groupState.runId)
673
+ return
674
+
675
+ const currentGroup = readSharedFitGroup(groupState)
676
+
677
+ if (!currentGroup || currentGroup.signature !== group.signature)
678
+ return
679
+
680
+ if (sharedItemsFit(groupState))
681
+ return
682
+ }
683
+
684
+ setSharedFitSnapshot(groupState, group.signature, fallbackSizes)
685
+ }
686
+
353
687
  return {
354
688
  useElement(input: ElementFitInput) {
355
689
  const size = ref<ThemeTextSize>(readMaybeRef(input.fallbackSize))
356
690
  let runId = 0
357
- let cancelSchedule: (() => void) | null = null
358
- let disconnectObservers: (() => void) | null = null
359
691
 
360
692
  async function fitNow() {
361
693
  const currentRunId = ++runId
@@ -399,39 +731,16 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
399
731
  size.value = resolvedSize
400
732
  }
401
733
 
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)
734
+ const { scheduleFit } = createTextFitLifecycleOwner({
735
+ adapter,
736
+ fit: fitNow,
737
+ onDispose: () => {
738
+ runId += 1
739
+ },
740
+ target: input.target,
741
+ watchTarget: true,
427
742
  })
428
743
 
429
- watch(input.target, async () => {
430
- await nextTick()
431
- reconnectObservers()
432
- scheduleFit()
433
- }, { flush: 'post' })
434
-
435
744
  watch(input.enabled, (isEnabled) => {
436
745
  if (isEnabled)
437
746
  scheduleFit()
@@ -445,12 +754,6 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
445
754
  }, { flush: 'post' })
446
755
  }
447
756
 
448
- onBeforeUnmount(() => {
449
- cancelSchedule?.()
450
- disconnectObservers?.()
451
- runId += 1
452
- })
453
-
454
757
  return {
455
758
  size,
456
759
  refit: scheduleFit,
@@ -458,13 +761,57 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
458
761
  },
459
762
  useGroup(input: GroupFitInput) {
460
763
  const maxItems = input.maxItems ?? 3
764
+ const itemId = Symbol('slot-text-fit-group')
461
765
  const fitSnapshot = shallowRef<SlotTextFitSnapshot | null>(null)
462
766
  let currentFitGroup: SlotTextFitGroup | null = null
767
+ let currentSharedKey = ''
768
+ let appliedSharedSignature: string | null = null
769
+ let appliedSharedVersion = 0
463
770
  let runId = 0
464
- let cancelSchedule: (() => void) | null = null
465
- let disconnectObservers: (() => void) | null = null
466
771
  let pendingInternalSignature: string | null | undefined
467
772
 
773
+ function unregisterSharedItem() {
774
+ if (!currentSharedKey)
775
+ return
776
+
777
+ const group = sharedGroups.get(currentSharedKey)
778
+ group?.items.delete(itemId)
779
+
780
+ if (group && !group.items.size)
781
+ sharedGroups.delete(currentSharedKey)
782
+
783
+ currentSharedKey = ''
784
+ }
785
+
786
+ function registerSharedItem(key: string) {
787
+ if (currentSharedKey && currentSharedKey !== key)
788
+ unregisterSharedItem()
789
+
790
+ currentSharedKey = key
791
+ const group = ensureSharedGroup(key)
792
+ let item = group.items.get(itemId)
793
+ let isNew = false
794
+
795
+ if (!item) {
796
+ isNew = true
797
+ item = {
798
+ target: input.target,
799
+ nodes: input.nodes,
800
+ maxItems,
801
+ currentFitGroup: null,
802
+ pendingInternalSignature: undefined,
803
+ }
804
+ group.items.set(itemId, item)
805
+ group.version += 1
806
+ }
807
+
808
+ item.target = input.target
809
+ item.nodes = input.nodes
810
+ item.maxItems = maxItems
811
+
812
+ return { group, item, isNew }
813
+ }
814
+
468
815
  function setFitSnapshot(signature: string, sizes: number[]) {
469
816
  if (fitSnapshot.value?.signature === signature && sizesEqual(fitSnapshot.value.sizes, sizes))
470
817
  return
@@ -484,6 +831,16 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
484
831
  }
485
832
 
486
833
  async function fitNow() {
834
+ const sharedKey = readSharedGroupKey(input.sharedKey)
835
+
836
+ if (sharedKey) {
837
+ registerSharedItem(sharedKey)
838
+ await fitSharedNow(sharedKey)
839
+ return
840
+ }
841
+
842
+ unregisterSharedItem()
843
+
487
844
  const currentRunId = ++runId
488
845
  const group = currentFitGroup ?? readSlotTextFitGroup(input.nodes(), maxItems)
489
846
  const element = input.target.value
@@ -516,33 +873,34 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
516
873
  setFitSnapshot(group.signature, fallbackSizes)
517
874
  }
518
875
 
519
- function scheduleFit() {
520
- cancelSchedule?.()
521
- cancelSchedule = adapter.schedule(() => {
522
- cancelSchedule = null
523
- void fitNow()
524
- })
525
- }
876
+ const { scheduleFit } = createTextFitLifecycleOwner({
877
+ adapter,
878
+ fit: fitNow,
879
+ onDispose: () => {
880
+ runId += 1
881
+ unregisterSharedItem()
882
+ },
883
+ target: input.target,
884
+ })
526
885
 
527
- function reconnectObservers() {
528
- disconnectObservers?.()
529
- disconnectObservers = null
886
+ onUpdated(() => {
887
+ const sharedKey = readSharedGroupKey(input.sharedKey)
530
888
 
531
- const element = input.target.value
532
- if (!element)
533
- return
889
+ if (sharedKey) {
890
+ const sharedGroup = sharedGroups.get(sharedKey)
891
+ const sharedItem = sharedGroup?.items.get(itemId)
892
+ const internalSignature = sharedItem?.pendingInternalSignature
534
893
 
535
- disconnectObservers = adapter.observe([element], scheduleFit)
536
- }
894
+ if (sharedItem)
895
+ sharedItem.pendingInternalSignature = undefined
537
896
 
538
- onMounted(async () => {
539
- await nextTick()
540
- reconnectObservers()
541
- scheduleFit()
542
- adapter.afterFontsReady?.(scheduleFit)
543
- })
897
+ if (internalSignature !== undefined && internalSignature === (sharedGroup ? readSharedFitGroup(sharedGroup)?.signature ?? null : null))
898
+ return
899
+
900
+ scheduleFit()
901
+ return
902
+ }
544
903
 
545
- onUpdated(() => {
546
904
  const internalSignature = pendingInternalSignature
547
905
  pendingInternalSignature = undefined
548
906
 
@@ -552,15 +910,39 @@ export function createTextFitRuntime(adapter: TextFitAdapter = createBrowserText
552
910
  scheduleFit()
553
911
  })
554
912
 
555
- onBeforeUnmount(() => {
556
- cancelSchedule?.()
557
- disconnectObservers?.()
558
- runId += 1
559
- })
560
-
561
913
  function apply(nodes: VNode[]) {
562
914
  const group = readSlotTextFitGroup(nodes, maxItems)
563
915
  currentFitGroup = group
916
+
917
+ const sharedKey = readSharedGroupKey(input.sharedKey)
918
+
919
+ if (sharedKey) {
920
+ const shared = registerSharedItem(sharedKey)
921
+ const previousSignature = shared.item.currentFitGroup?.signature ?? null
922
+ shared.item.currentFitGroup = group
923
+ const nextSignature = group?.signature ?? null
924
+
925
+ if (
926
+ shared.isNew
927
+ || previousSignature !== nextSignature
928
+ || appliedSharedVersion !== shared.group.version
929
+ || appliedSharedSignature !== nextSignature
930
+ ) {
931
+ appliedSharedVersion = shared.group.version
932
+ appliedSharedSignature = nextSignature
933
+ scheduleFit()
934
+ }
935
+
936
+ const sharedGroup = readSharedFitGroup(shared.group)
937
+ const fittedSizes = group && sharedGroup && shared.group.fitSnapshot.value?.signature === sharedGroup.signature
938
+ ? shared.group.fitSnapshot.value.sizes
939
+ : sharedGroup?.initialSizes ?? group?.initialSizes ?? null
940
+
941
+ return applySlotTextFit(nodes, group, fittedSizes)
942
+ }
943
+
944
+ unregisterSharedItem()
945
+
564
946
  const fittedSizes = group && fitSnapshot.value?.signature === group.signature
565
947
  ? fitSnapshot.value.sizes
566
948
  : group?.initialSizes ?? null
@@ -0,0 +1,13 @@
1
+ import { inject, provide } from 'vue'
2
+
3
+ const textFitScopeKey = Symbol('theme-text-fit-scope')
4
+
5
+ export function provideTextFitScope() {
6
+ const scope = `theme-text-fit-${Math.random().toString(36).slice(2, 10)}`
7
+ provide(textFitScopeKey, scope)
8
+ return scope
9
+ }
10
+
11
+ export function useTextFitScope() {
12
+ return inject(textFitScopeKey, '')
13
+ }