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.
- package/README.md +270 -0
- package/components/DebugGrid.vue +50 -0
- package/components/DecorLibrary.vue +1117 -0
- package/components/Header.vue +124 -0
- package/components/Image.vue +82 -0
- package/components/ImageRenderer.vue +126 -0
- package/components/Logo.vue +142 -0
- package/components/Person.vue +89 -0
- package/components/Slide.vue +324 -0
- package/components/Slot.vue +419 -0
- package/components/Text.vue +216 -0
- package/components/Timeline.vue +202 -0
- package/composables/decor-catalog.mjs +339 -0
- package/composables/decor-config.mjs +488 -0
- package/composables/decor-file-store.ts +192 -0
- package/composables/decor-http-store.ts +25 -0
- package/composables/decor-store.ts +39 -0
- package/composables/decor-tuning-overrides.mjs +2 -0
- package/composables/decor-workbench.ts +799 -0
- package/composables/grid-placement.ts +396 -0
- package/composables/image-config.mjs +484 -0
- package/composables/layout-authoring.ts +372 -0
- package/composables/layout-markdown.mjs +56 -0
- package/composables/layout-recipes.ts +347 -0
- package/composables/layout-registry.ts +47 -0
- package/composables/layout-shorthands.ts +891 -0
- package/composables/layout-vnode.ts +43 -0
- package/composables/logo-mark.mjs +5 -0
- package/composables/slide-layout.ts +43 -0
- package/composables/slot-placement.ts +195 -0
- package/composables/slot-rules.ts +147 -0
- package/composables/text-fit-runtime.ts +577 -0
- package/composables/text-priority-fit.mjs +166 -0
- package/composables/text-typography.mjs +138 -0
- package/composables/theme-foundation.ts +192 -0
- package/composables/theme-media.mjs +202 -0
- package/composables/use-theme-config.ts +71 -0
- package/decor-library.md +10 -0
- package/env.d.ts +6 -0
- package/example.md +786 -0
- package/layouts/collection.vue +5 -0
- package/layouts/cover.vue +5 -0
- package/layouts/default.vue +5 -0
- package/layouts/explainer.vue +5 -0
- package/layouts/message.vue +5 -0
- package/layouts/none.vue +5 -0
- package/layouts/section.vue +5 -0
- package/layouts/two-cols.vue +11 -0
- package/package.json +84 -0
- package/public/decor/decor-1.svg +83 -0
- package/public/decor/decor-10.svg +40 -0
- package/public/decor/decor-11.svg +162 -0
- package/public/decor/decor-12.svg +83 -0
- package/public/decor/decor-13.svg +5 -0
- package/public/decor/decor-14.svg +7 -0
- package/public/decor/decor-15.svg +10 -0
- package/public/decor/decor-16.svg +9 -0
- package/public/decor/decor-17.png +0 -0
- package/public/decor/decor-18.png +0 -0
- package/public/decor/decor-19.svg +5 -0
- package/public/decor/decor-2.png +0 -0
- package/public/decor/decor-3.svg +9 -0
- package/public/decor/decor-4.svg +31 -0
- package/public/decor/decor-5.png +0 -0
- package/public/decor/decor-6.png +0 -0
- package/public/decor/decor-7.png +0 -0
- package/public/decor/decor-8.svg +21 -0
- package/public/decor/decor-9.svg +38 -0
- package/public/favicon.svg +11 -0
- 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/setup/main.ts +3 -0
- package/setup/shiki.ts +10 -0
- package/setup/vite-plugins.ts +106 -0
- package/slidev-theme-practicum.png +0 -0
- package/slidev-theme-practicum.svg +102 -0
- package/styles/example.css +0 -0
- package/styles/index.css +192 -0
- package/styles/vars.css +119 -0
- 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
|
+
}
|