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.
- package/LICENSE +21 -0
- package/README.md +72 -10
- package/components/Slide.vue +81 -26
- package/components/Slot.vue +33 -48
- package/components/Text.vue +68 -2
- package/components/Timeline.vue +2 -23
- package/composables/deck-slot-markup.cjs +249 -0
- package/composables/decor-catalog.mjs +169 -47
- package/composables/layout-authoring.ts +6 -2
- package/composables/layout-markdown.mjs +33 -0
- package/composables/layout-recipes.ts +51 -5
- package/composables/layout-shorthands.ts +251 -81
- package/composables/markdown-to-vnodes.cjs +1 -0
- package/composables/markdown-to-vnodes.mjs +237 -0
- package/composables/slot-placement.ts +1 -1
- package/composables/slot-spacing.ts +66 -0
- package/composables/text-fit-runtime.ts +458 -76
- package/composables/text-fit-scope.ts +13 -0
- package/composables/text-slot-markdown.mjs +193 -0
- package/composables/text-typography.mjs +2 -0
- package/composables/theme-assets.mjs +60 -0
- package/composables/timeline-grid.ts +27 -0
- package/composables/typescript-require.cjs +29 -0
- package/composables/validate-deck-layouts.cjs +277 -0
- package/decor-library.md +2 -0
- package/example.md +132 -10
- package/package.json +25 -11
- package/public/photos/photo-1.webp +0 -0
- package/public/photos/photo-10.webp +0 -0
- package/public/photos/photo-11.webp +0 -0
- package/public/photos/photo-12.webp +0 -0
- package/public/photos/photo-13.webp +0 -0
- package/public/photos/photo-14.webp +0 -0
- package/public/photos/photo-15.webp +0 -0
- package/public/photos/photo-16.webp +0 -0
- package/public/photos/photo-17.webp +0 -0
- package/public/photos/photo-18.webp +0 -0
- package/public/photos/photo-19.webp +0 -0
- package/public/photos/photo-2.webp +0 -0
- package/public/photos/photo-20.webp +0 -0
- package/public/photos/photo-21.webp +0 -0
- package/public/photos/photo-22.webp +0 -0
- package/public/photos/photo-23.webp +0 -0
- package/public/photos/photo-24.webp +0 -0
- package/public/photos/photo-25.webp +0 -0
- package/public/photos/photo-26.webp +0 -0
- package/public/photos/photo-27.webp +0 -0
- package/public/photos/photo-28.webp +0 -0
- package/public/photos/photo-29.webp +0 -0
- package/public/photos/photo-3.webp +0 -0
- package/public/photos/photo-30.webp +0 -0
- package/public/photos/photo-4.webp +0 -0
- package/public/photos/photo-5.webp +0 -0
- package/public/photos/photo-6.webp +0 -0
- package/public/photos/photo-7.webp +0 -0
- package/public/photos/photo-8.webp +0 -0
- package/public/photos/photo-9.webp +0 -0
- package/scripts/browser-smoke-runtime.mjs +428 -0
- package/scripts/browser-smoke.mjs +424 -0
- package/scripts/build-artifact-assets.mjs +55 -0
- package/scripts/check-build-artifact.mjs +243 -0
- package/scripts/check-package.mjs +132 -0
- package/scripts/check-test-architecture.mjs +176 -0
- package/scripts/npm-spawn.mjs +31 -0
- package/scripts/test-architecture-policy.mjs +245 -0
- package/scripts/theme-asset-inventory.mjs +83 -0
- package/scripts/validate-deck.cjs +18 -0
- package/setup/decor-save-middleware.ts +244 -0
- package/setup/vite-plugins.ts +14 -84
- package/skills/slidev-practicum/SKILL.md +25 -0
- package/skills/slidev-practicum/agents/openai.yaml +4 -0
- package/styles/index.css +10 -1
- package/composables/layout-registry.ts +0 -47
- package/env.d.ts +0 -6
- package/public/photos/photo-1.png +0 -0
- package/public/photos/photo-10.png +0 -0
- package/public/photos/photo-2.png +0 -0
- package/public/photos/photo-3.png +0 -0
- package/public/photos/photo-4.png +0 -0
- package/public/photos/photo-5.png +0 -0
- package/public/photos/photo-6.png +0 -0
- package/public/photos/photo-7.png +0 -0
- package/public/photos/photo-8.png +0 -0
- package/public/photos/photo-9.png +0 -0
- package/slidev-theme-practicum.png +0 -0
- 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
|
-
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
442
|
+
const textElement = child as HTMLElement
|
|
443
|
+
|
|
444
|
+
if (!textElement.classList?.contains('Text'))
|
|
343
445
|
return true
|
|
344
446
|
|
|
345
|
-
return adapter.measure(
|
|
346
|
-
width:
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
disconnectObservers = null
|
|
886
|
+
onUpdated(() => {
|
|
887
|
+
const sharedKey = readSharedGroupKey(input.sharedKey)
|
|
530
888
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
889
|
+
if (sharedKey) {
|
|
890
|
+
const sharedGroup = sharedGroups.get(sharedKey)
|
|
891
|
+
const sharedItem = sharedGroup?.items.get(itemId)
|
|
892
|
+
const internalSignature = sharedItem?.pendingInternalSignature
|
|
534
893
|
|
|
535
|
-
|
|
536
|
-
|
|
894
|
+
if (sharedItem)
|
|
895
|
+
sharedItem.pendingInternalSignature = undefined
|
|
537
896
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
}
|