@meistrari/tela-build 1.25.1 → 1.25.3

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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { useEventListener, useScroll } from '@vueuse/core'
2
+ import { useThrottleFn, useEventListener, useScroll } from '@vueuse/core'
3
3
  import { cn } from '@/lib/utils'
4
4
  import type { PreviewFile, PreviewContentLabels, PdfDocumentHandle } from './types'
5
5
 
@@ -24,6 +24,8 @@ const props = withDefaults(
24
24
  highlightExact?: boolean
25
25
  /** Override the initial zoom scale. Defaults to 1 for minimal, 0.75 for default. */
26
26
  initialScale?: number
27
+ /** When true, shows the loading skeleton regardless of internal loading state. Use when the file content is being fetched externally (e.g. lazy download). */
28
+ loading?: boolean
27
29
  }>(),
28
30
  {
29
31
  variant: 'default',
@@ -32,6 +34,7 @@ const props = withDefaults(
32
34
  highlightPage: undefined,
33
35
  highlightExact: false,
34
36
  initialScale: undefined,
37
+ loading: false,
35
38
  },
36
39
  )
37
40
 
@@ -94,16 +97,33 @@ const pdfLoadError = ref<string | null>(null)
94
97
  const pageRefs = ref<Map<number, HTMLElement>>(new Map())
95
98
  const textLayerRefs = ref<Map<number, HTMLDivElement>>(new Map())
96
99
  const renderedPages = ref<Set<number>>(new Set())
100
+ const pageDimensions = ref<Map<number, { width: number, height: number }>>(new Map())
101
+ const defaultPageDimensions = ref<{ width: number, height: number } | null>(null)
102
+ const visiblePages = ref<Set<number>>(new Set())
103
+ const pdfObserver = ref<IntersectionObserver | null>(null)
104
+ const BUFFER_PAGES = 1
105
+ const INITIAL_DIMENSION_PAGES = 3
106
+ const DIMENSION_BATCH_SIZE = 8
107
+ const canvasRenderTokens = new WeakMap<HTMLCanvasElement, number>()
108
+ let canvasRenderTokenCounter = 0
109
+ let pageRenderGeneration = 0
110
+ const inFlightPageRenders = new Map<number, {
111
+ generation: number
112
+ scale: number
113
+ promise: Promise<void>
114
+ }>()
97
115
  let isRendering = false
98
116
  let pendingReRender = false
99
117
  let pendingScrollPage: number | null = null
100
118
 
119
+ const throttledReRender = useThrottleFn(reRenderAllPdfPages, 150, true)
120
+
101
121
  function zoomIn() {
102
122
  if (scale.value < 3) {
103
123
  scale.value = Math.min(scale.value + 0.25, 3)
104
124
 
105
125
  if (props.file.fileType === 'application/pdf') {
106
- reRenderAllPdfPages()
126
+ throttledReRender()
107
127
  }
108
128
  }
109
129
  }
@@ -113,10 +133,33 @@ function zoomOut() {
113
133
  scale.value = Math.max(scale.value - 0.25, 0.5)
114
134
 
115
135
  if (props.file.fileType === 'application/pdf') {
116
- reRenderAllPdfPages()
136
+ throttledReRender()
117
137
  }
118
138
  }
119
139
  }
140
+ function clearCanvasElement(canvas: HTMLCanvasElement | null | undefined) {
141
+ if (!canvas)
142
+ return
143
+
144
+ canvasRenderTokens.delete(canvas)
145
+
146
+ const ctx = canvas.getContext('2d')
147
+ if (ctx) {
148
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
149
+ }
150
+
151
+ canvas.width = 0
152
+ canvas.height = 0
153
+ }
154
+
155
+ function clearTextLayerElement(textLayer: HTMLDivElement | null | undefined) {
156
+ if (!textLayer)
157
+ return
158
+
159
+ textLayer.innerHTML = ''
160
+ textLayer.style.width = ''
161
+ textLayer.style.height = ''
162
+ }
120
163
 
121
164
  function handleMouseDown(e: MouseEvent) {
122
165
  if (!scrollContainerRef.value || !dragEnabled.value)
@@ -195,8 +238,13 @@ watch(scrollY, () => {
195
238
  function setPageRef(pageNum: number, el: HTMLElement | null) {
196
239
  if (el) {
197
240
  pageRefs.value.set(pageNum, el)
241
+ el.setAttribute('data-page-num', String(pageNum))
242
+ pdfObserver.value?.observe(el)
198
243
  }
199
244
  else {
245
+ const existing = pageRefs.value.get(pageNum)
246
+ if (existing)
247
+ pdfObserver.value?.unobserve(existing)
200
248
  pageRefs.value.delete(pageNum)
201
249
  }
202
250
  }
@@ -211,6 +259,34 @@ function setTextLayerRef(pageNum: number, el: HTMLDivElement | null) {
211
259
  }
212
260
 
213
261
  async function renderPdfPage(pageNum: number) {
262
+ const generation = pageRenderGeneration
263
+ const renderScale = scale.value
264
+ const existingRender = inFlightPageRenders.get(pageNum)
265
+
266
+ if (existingRender && existingRender.generation === generation && existingRender.scale === renderScale) {
267
+ await existingRender.promise
268
+ return
269
+ }
270
+
271
+ const renderPromise = renderPdfPageOnce(pageNum, generation, renderScale)
272
+ inFlightPageRenders.set(pageNum, {
273
+ generation,
274
+ scale: renderScale,
275
+ promise: renderPromise,
276
+ })
277
+
278
+ try {
279
+ await renderPromise
280
+ }
281
+ finally {
282
+ const activeRender = inFlightPageRenders.get(pageNum)
283
+ if (activeRender?.promise === renderPromise) {
284
+ inFlightPageRenders.delete(pageNum)
285
+ }
286
+ }
287
+ }
288
+
289
+ async function renderPdfPageOnce(pageNum: number, generation: number, renderScale: number) {
214
290
  const handle = pdfDocHandle.value
215
291
 
216
292
  if (!handle)
@@ -221,50 +297,347 @@ async function renderPdfPage(pageNum: number) {
221
297
  if (!pageContainer)
222
298
  return
223
299
 
224
- const canvas = pageContainer.querySelector('canvas')
300
+ const canvas = pageContainer.querySelector('canvas') as HTMLCanvasElement | null
225
301
 
226
302
  if (!canvas)
227
303
  return
304
+ const renderToken = ++canvasRenderTokenCounter
305
+ canvasRenderTokens.set(canvas, renderToken)
228
306
 
229
307
  const textLayer = textLayerRefs.value.get(pageNum) ?? null
230
308
 
231
309
  await handle.renderPage({
232
310
  pageNum,
233
- canvas: canvas as HTMLCanvasElement,
234
- scale: scale.value,
311
+ canvas,
312
+ scale: renderScale,
235
313
  textLayer,
236
314
  highlight: props.highlightText,
237
315
  highlightPage: props.highlightPage,
238
316
  highlightExact: props.highlightExact,
239
317
  })
240
318
 
319
+ const currentRenderToken = canvasRenderTokens.get(canvas)
320
+ if (
321
+ currentRenderToken !== renderToken
322
+ || handle !== pdfDocHandle.value
323
+ || generation !== pageRenderGeneration
324
+ || renderScale !== scale.value
325
+ ) {
326
+ if (currentRenderToken === undefined) {
327
+ clearCanvasElement(canvas)
328
+ }
329
+ return
330
+ }
331
+
241
332
  renderedPages.value.add(pageNum)
242
333
  }
243
334
 
244
- async function renderAllPdfPages() {
245
- const handle = pdfDocHandle.value
335
+ function clearPageCanvas(pageNum: number) {
336
+ const container = pageRefs.value.get(pageNum)
337
+ if (!container)
338
+ return
339
+ const canvas = container.querySelector('canvas') as HTMLCanvasElement | null
340
+ const textLayer = textLayerRefs.value.get(pageNum)
341
+ clearCanvasElement(canvas)
342
+ clearTextLayerElement(textLayer)
343
+ renderedPages.value.delete(pageNum)
344
+ }
345
+
346
+ function clearAllPageCanvases() {
347
+ for (const [pageNum, container] of pageRefs.value.entries()) {
348
+ const canvas = container.querySelector('canvas') as HTMLCanvasElement | null
349
+ const textLayer = textLayerRefs.value.get(pageNum)
350
+ clearCanvasElement(canvas)
351
+ clearTextLayerElement(textLayer)
352
+ }
353
+ }
354
+
355
+ function getBufferedPageNumbers(pageNums: Iterable<number>, total: number): number[] {
356
+ const bufferedPages = new Set<number>()
357
+
358
+ for (const pageNum of pageNums) {
359
+ for (let i = pageNum - BUFFER_PAGES; i <= pageNum + BUFFER_PAGES; i++) {
360
+ if (i >= 1 && i <= total) {
361
+ bufferedPages.add(i)
362
+ }
363
+ }
364
+ }
365
+
366
+ return [...bufferedPages].sort((a, b) => a - b)
367
+ }
368
+
369
+ function getPagesToRender(total: number): number[] {
370
+ if (visiblePages.value.size === 0) {
371
+ return getBufferedPageNumbers([1], total)
372
+ }
373
+
374
+ return getBufferedPageNumbers(visiblePages.value, total)
375
+ }
376
+
377
+ function getInitialDimensionPages(total: number): number[] {
378
+ return Array.from({ length: Math.min(total, INITIAL_DIMENSION_PAGES) }, (_, index) => index + 1)
379
+ }
380
+
381
+ function yieldToBrowser(): Promise<void> {
382
+ if (typeof requestAnimationFrame === 'undefined') {
383
+ return new Promise(resolve => setTimeout(resolve, 0))
384
+ }
246
385
 
386
+ return new Promise(resolve => requestAnimationFrame(() => resolve()))
387
+ }
388
+
389
+ let dimensionGeneration = 0
390
+
391
+ async function measurePageDimension(
392
+ handle: PdfDocumentHandle,
393
+ pageNum: number,
394
+ generation: number,
395
+ ): Promise<{ width: number, height: number } | null> {
396
+ if (!handle.getPageDimensions)
397
+ return null
398
+
399
+ const existing = pageDimensions.value.get(pageNum)
400
+ if (existing)
401
+ return existing
402
+
403
+ const dims = await handle.getPageDimensions({ pageNum, scale: scale.value })
404
+ if (!dims)
405
+ return null
406
+
407
+ if (generation !== dimensionGeneration || handle !== pdfDocHandle.value)
408
+ return null
409
+
410
+ pageDimensions.value.set(pageNum, dims)
411
+
412
+ if (pageNum === 1 || !defaultPageDimensions.value) {
413
+ defaultPageDimensions.value = dims
414
+ }
415
+
416
+ return dims
417
+ }
418
+
419
+ async function primePageDimensions(
420
+ handle: PdfDocumentHandle,
421
+ priorityPages: number[],
422
+ generation: number,
423
+ ): Promise<void> {
424
+ if (!handle.getPageDimensions)
425
+ return
426
+
427
+ for (const pageNum of priorityPages) {
428
+ await measurePageDimension(handle, pageNum, generation)
429
+ }
430
+ }
431
+
432
+ async function warmRemainingPageDimensions(
433
+ handle: PdfDocumentHandle,
434
+ priorityPages: number[],
435
+ generation: number,
436
+ ): Promise<void> {
437
+ if (!handle.getPageDimensions)
438
+ return
439
+
440
+ const priorityPageSet = new Set(priorityPages)
441
+ const deferredPages: number[] = []
442
+
443
+ for (let pageNum = 1; pageNum <= handle.numPages; pageNum++) {
444
+ if (!priorityPageSet.has(pageNum)) {
445
+ deferredPages.push(pageNum)
446
+ }
447
+ }
448
+
449
+ for (let i = 0; i < deferredPages.length; i += DIMENSION_BATCH_SIZE) {
450
+ const batch = deferredPages.slice(i, i + DIMENSION_BATCH_SIZE)
451
+ await Promise.all(batch.map(pageNum => measurePageDimension(handle, pageNum, generation)))
452
+
453
+ if (generation !== dimensionGeneration || handle !== pdfDocHandle.value)
454
+ return
455
+
456
+ await yieldToBrowser()
457
+ }
458
+ }
459
+
460
+ let isRenderingVisiblePages = false
461
+ let shouldRenderVisiblePagesAgain = false
462
+
463
+ async function renderVisiblePagesOnce() {
464
+ const handle = pdfDocHandle.value
247
465
  if (!handle)
248
466
  return
249
467
 
468
+ const pagesToRender = new Set<number>(getPagesToRender(handle.numPages))
469
+
470
+ for (const pageNum of pagesToRender) {
471
+ if (!renderedPages.value.has(pageNum)) {
472
+ await renderPdfPage(pageNum)
473
+ }
474
+ }
475
+
476
+ for (const pageNum of [...renderedPages.value]) {
477
+ if (!pagesToRender.has(pageNum)) {
478
+ clearPageCanvas(pageNum)
479
+ }
480
+ }
481
+ }
482
+
483
+ async function renderVisiblePages() {
484
+ if (isRenderingVisiblePages) {
485
+ shouldRenderVisiblePagesAgain = true
486
+ return
487
+ }
488
+
489
+ isRenderingVisiblePages = true
490
+
491
+ try {
492
+ do {
493
+ shouldRenderVisiblePagesAgain = false
494
+ await renderVisiblePagesOnce()
495
+ }
496
+ while (shouldRenderVisiblePagesAgain)
497
+ }
498
+ finally {
499
+ isRenderingVisiblePages = false
500
+ }
501
+ }
502
+
503
+ let renderRafId: number | null = null
504
+
505
+ function scheduleRenderVisiblePages() {
506
+ if (renderRafId !== null)
507
+ return
508
+ renderRafId = requestAnimationFrame(() => {
509
+ renderRafId = null
510
+ void renderVisiblePages()
511
+ })
512
+ }
513
+
514
+ function setupPdfObserver() {
515
+ pdfObserver.value?.disconnect()
516
+
517
+ if (typeof IntersectionObserver === 'undefined') {
518
+ renderFallbackAllPages()
519
+ return
520
+ }
521
+
522
+ pdfObserver.value = new IntersectionObserver(
523
+ (entries) => {
524
+ for (const entry of entries) {
525
+ const pageNum = Number(entry.target.getAttribute('data-page-num'))
526
+ if (Number.isNaN(pageNum))
527
+ continue
528
+
529
+ if (entry.isIntersecting) {
530
+ visiblePages.value.add(pageNum)
531
+ }
532
+ else {
533
+ visiblePages.value.delete(pageNum)
534
+ }
535
+ }
536
+ scheduleRenderVisiblePages()
537
+ },
538
+ {
539
+ root: scrollContainerRef.value,
540
+ rootMargin: '300px 0px',
541
+ },
542
+ )
543
+
544
+ for (const [pageNum, el] of pageRefs.value) {
545
+ el.setAttribute('data-page-num', String(pageNum))
546
+ pdfObserver.value.observe(el)
547
+ }
548
+
549
+ // Render first pages immediately — don't wait for async observer callback
550
+ visiblePages.value.add(1)
551
+ void renderVisiblePages()
552
+ }
553
+
554
+ async function renderFallbackAllPages() {
555
+ const handle = pdfDocHandle.value
556
+ if (!handle)
557
+ return
250
558
  for (let pageNum = 1; pageNum <= handle.numPages; pageNum++) {
251
559
  await renderPdfPage(pageNum)
252
560
  }
253
561
  }
254
562
 
563
+ async function precomputePageDimensions() {
564
+ const handle = pdfDocHandle.value
565
+ if (!handle)
566
+ return
567
+
568
+ const priorityPages = getInitialDimensionPages(handle.numPages)
569
+ const generation = ++dimensionGeneration
570
+
571
+ await primePageDimensions(handle, priorityPages, generation)
572
+ void warmRemainingPageDimensions(handle, priorityPages, generation)
573
+ }
574
+
575
+ function getPagePlaceholderStyle(pageNum: number) {
576
+ const dims = pageDimensions.value.get(pageNum) ?? defaultPageDimensions.value
577
+ if (!dims || renderedPages.value.has(pageNum))
578
+ return undefined
579
+ return {
580
+ minWidth: `${dims.width}px`,
581
+ minHeight: `${dims.height}px`,
582
+ }
583
+ }
584
+
585
+ function resetPdfRenderState() {
586
+ dimensionGeneration++
587
+ pageRenderGeneration++
588
+ inFlightPageRenders.clear()
589
+
590
+ if (renderRafId !== null) {
591
+ cancelAnimationFrame(renderRafId)
592
+ renderRafId = null
593
+ }
594
+
595
+ pdfObserver.value?.disconnect()
596
+ pdfObserver.value = null
597
+ visiblePages.value.clear()
598
+ pageDimensions.value.clear()
599
+ defaultPageDimensions.value = null
600
+
601
+ clearAllPageCanvases()
602
+ renderedPages.value.clear()
603
+ currentDocumentPage.value = 1
604
+ totalPages.value = 0
605
+ }
606
+
255
607
  async function reRenderAllPdfPages() {
608
+ const handle = pdfDocHandle.value
609
+ if (!handle)
610
+ return
611
+
256
612
  if (isRendering) {
257
613
  pendingReRender = true
258
614
  return
259
615
  }
616
+
260
617
  isRendering = true
618
+
261
619
  try {
620
+ pageRenderGeneration++
621
+ inFlightPageRenders.clear()
622
+ clearAllPageCanvases()
262
623
  renderedPages.value.clear()
624
+ pageDimensions.value.clear()
625
+ defaultPageDimensions.value = null
626
+
627
+ const priorityPages = getPagesToRender(handle.numPages)
628
+ const generation = ++dimensionGeneration
629
+
630
+ await primePageDimensions(handle, priorityPages, generation)
631
+ void warmRemainingPageDimensions(handle, priorityPages, generation)
263
632
  await nextTick()
264
- await renderAllPdfPages()
633
+ if (visiblePages.value.size === 0) {
634
+ visiblePages.value.add(1)
635
+ }
636
+ await renderVisiblePages()
265
637
  }
266
638
  finally {
267
639
  isRendering = false
640
+
268
641
  if (pendingReRender) {
269
642
  pendingReRender = false
270
643
  void reRenderAllPdfPages().then(() => {
@@ -274,6 +647,7 @@ async function reRenderAllPdfPages() {
274
647
  }
275
648
  })
276
649
  }
650
+
277
651
  else if (pendingScrollPage !== null) {
278
652
  scrollToPage(pendingScrollPage)
279
653
  pendingScrollPage = null
@@ -281,7 +655,17 @@ async function reRenderAllPdfPages() {
281
655
  }
282
656
  }
283
657
 
284
- watch(() => props.file, async (newFile) => {
658
+ watch(() => props.file, async (newFile, oldFile) => {
659
+ const isSamePdfRecompute = newFile?.fileType === 'application/pdf'
660
+ && pdfDocHandle.value
661
+ && newFile?.fileName === oldFile?.fileName
662
+ && newFile?.fileUrl === oldFile?.fileUrl
663
+
664
+ if (isSamePdfRecompute) {
665
+ return
666
+ }
667
+
668
+ resetPdfRenderState()
285
669
  pdfDocHandle.value?.destroy()
286
670
  pdfDocHandle.value = null
287
671
  pdfLoadError.value = null
@@ -327,8 +711,11 @@ watch(() => props.file, async (newFile) => {
327
711
  pdfDocHandle.value = doc
328
712
  totalPages.value = doc.numPages
329
713
 
714
+ await precomputePageDimensions()
330
715
  await nextTick()
331
- await renderAllPdfPages()
716
+ if (scrollContainerRef.value) {
717
+ setupPdfObserver()
718
+ }
332
719
 
333
720
  const scrollTarget = (props.highlightPage && props.highlightPage > 0)
334
721
  ? props.highlightPage
@@ -346,6 +733,7 @@ watch(() => props.file, async (newFile) => {
346
733
  }, { immediate: true, deep: true })
347
734
 
348
735
  onBeforeUnmount(() => {
736
+ resetPdfRenderState()
349
737
  pdfDocHandle.value?.destroy()
350
738
  })
351
739
 
@@ -484,61 +872,111 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
484
872
  @mouseenter="handleContainerMouseEnter"
485
873
  @mouseleave="handleContainerMouseLeave"
486
874
  >
487
- <div
488
- v-if="isLoading"
489
- :class="cn({
490
- 'pt-56px': !props.segmentTab && props.file.fileType === 'application/pdf',
491
- 'pt-88px': props.segmentTab && props.file.fileType === 'application/pdf',
492
- 'px-46px': variant === 'default',
493
- 'px-32px': variant === 'minimal',
494
- })"
495
- >
875
+ <template v-if="isLoading || props.loading">
876
+ <!-- PDF skeleton: mimics the real PDF page stack -->
496
877
  <div
497
- bg-white rounded-12px
498
- flex="~ col" justify-between mx-auto relative
499
- :class="cn({
500
- 'w-446px min-h-631px px-32px py-28px': variant === 'default',
501
- 'w-256px min-h-363px px-16px py-14px': variant === 'minimal',
878
+ v-if="props.file.fileType === 'application/pdf'"
879
+ :class="cn('flex flex-col items-center gap-8px pb-80px', {
880
+ 'pt-56px': !props.segmentTab,
881
+ 'pt-88px': props.segmentTab,
882
+ 'px-28px': true,
502
883
  })"
503
884
  >
504
- <div flex="~ col" gap-16px>
505
- <div
506
- v-for="(width, index) in [100, 88, 72, 64]" :key="index"
507
- :h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
508
- :style="{ width: `${width}%` }"
509
- >
510
- <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
885
+ <div
886
+ v-for="pageNum in 2" :key="pageNum"
887
+ relative bg-white rounded-12px overflow-hidden mx-auto
888
+ :class="cn({
889
+ 'w-446px min-h-631px': variant === 'default',
890
+ 'w-256px min-h-363px': variant === 'minimal',
891
+ })"
892
+ >
893
+ <!-- Page badge -->
894
+ <div absolute top-8px left-8px flex items-center w-fit h-16px>
895
+ <div
896
+ overflow-hidden rounded-4px
897
+ :class="cn({
898
+ 'w-48px h-16px': variant === 'default',
899
+ 'w-36px h-14px': variant === 'minimal',
900
+ })"
901
+ >
902
+ <TelaSkeleton w-full h-full bg-gray-100 />
903
+ </div>
511
904
  </div>
512
- </div>
513
- <div flex="~ col" gap-16px>
905
+ <!-- Content lines -->
514
906
  <div
515
- v-for="(width, index) in [72, 92, 64, 48]" :key="index"
516
- :h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
517
- :style="{ width: `${width}%` }"
907
+ flex="~ col"
908
+ :class="cn({
909
+ 'gap-14px px-32px pt-44px pb-28px': variant === 'default',
910
+ 'gap-10px px-16px pt-32px pb-14px': variant === 'minimal',
911
+ })"
518
912
  >
519
- <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
913
+ <div
914
+ v-for="(width, index) in [100, 88, 72, 64, 78, 92, 60, 44, 85, 70, 56, 36]" :key="index"
915
+ overflow-hidden rounded-8px
916
+ :h="variant === 'default' ? '14px' : '10px'"
917
+ :style="{ width: `${width}%` }"
918
+ >
919
+ <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
920
+ </div>
520
921
  </div>
521
922
  </div>
522
- <div flex="~ col" gap-16px>
523
- <div
524
- v-for="(width, index) in [70, 90, 56, 36]" :key="index"
525
- :h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
526
- :style="{ width: `${width}%` }"
527
- >
528
- <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
923
+ </div>
924
+
925
+ <!-- Document skeleton for non-PDF files -->
926
+ <div
927
+ v-else
928
+ :class="cn({
929
+ 'px-46px': variant === 'default',
930
+ 'px-32px': variant === 'minimal',
931
+ })"
932
+ >
933
+ <div
934
+ bg-white rounded-12px
935
+ flex="~ col" justify-between mx-auto relative
936
+ :class="cn({
937
+ 'w-446px min-h-631px px-32px py-28px': variant === 'default',
938
+ 'w-256px min-h-363px px-16px py-14px': variant === 'minimal',
939
+ })"
940
+ >
941
+ <div flex="~ col" gap-16px>
942
+ <div
943
+ v-for="(width, index) in [100, 88, 72, 64]" :key="index"
944
+ :h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
945
+ :style="{ width: `${width}%` }"
946
+ >
947
+ <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
948
+ </div>
529
949
  </div>
530
- </div>
531
- <div v-if="variant === 'default'" flex="~ col" gap-16px>
532
- <div
533
- v-for="(width, index) in [82, 56, 32, 20]" :key="index"
534
- h-16px overflow-hidden rounded-8px
535
- :style="{ width: `${width}%` }"
536
- >
537
- <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
950
+ <div flex="~ col" gap-16px>
951
+ <div
952
+ v-for="(width, index) in [72, 92, 64, 48]" :key="index"
953
+ :h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
954
+ :style="{ width: `${width}%` }"
955
+ >
956
+ <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
957
+ </div>
958
+ </div>
959
+ <div flex="~ col" gap-16px>
960
+ <div
961
+ v-for="(width, index) in [70, 90, 56, 36]" :key="index"
962
+ :h="variant === 'default' ? '16px' : '12px'" overflow-hidden rounded-8px
963
+ :style="{ width: `${width}%` }"
964
+ >
965
+ <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
966
+ </div>
967
+ </div>
968
+ <div v-if="variant === 'default'" flex="~ col" gap-16px>
969
+ <div
970
+ v-for="(width, index) in [82, 56, 32, 20]" :key="index"
971
+ h-16px overflow-hidden rounded-8px
972
+ :style="{ width: `${width}%` }"
973
+ >
974
+ <TelaSkeleton w-full h-full rounded-8px bg-gray-100 />
975
+ </div>
538
976
  </div>
539
977
  </div>
540
978
  </div>
541
- </div>
979
+ </template>
542
980
 
543
981
  <template v-else-if="error">
544
982
  <div flex="~ col" items-center justify-center h-400px>
@@ -688,8 +1126,10 @@ watch([() => props.highlightText, () => props.highlightPage, () => props.highlig
688
1126
  :key="pageNum"
689
1127
  :ref="(el) => setPageRef(pageNum, el as HTMLElement)"
690
1128
  data-pdf-page-card
1129
+ :data-page-num="pageNum"
691
1130
  relative bg-white rounded-12px overflow-hidden mx-auto
692
1131
  :class="cn(variant === 'minimal' && 'w-256px pdf-page-card--minimal')"
1132
+ :style="getPagePlaceholderStyle(pageNum)"
693
1133
  >
694
1134
  <canvas />
695
1135
  <div
@@ -35,6 +35,8 @@ const props = withDefaults(
35
35
  highlightPage?: number | null
36
36
  /** When true, uses exact matching instead of fuzzy word-based matching. */
37
37
  highlightExact?: boolean
38
+ /** When true, shows the loading skeleton. Pass when file content is being fetched externally. */
39
+ loading?: boolean
38
40
  }>(),
39
41
  {
40
42
  variant: 'default',
@@ -126,6 +128,7 @@ const fileReaderKey = computed(() => {
126
128
  :pdf-loader="pdfLoader"
127
129
  :labels="contentLabels"
128
130
  :variant="variant"
131
+ :loading="loading"
129
132
  :highlight-text="highlightText"
130
133
  :highlight-page="highlightPage"
131
134
  :highlight-exact="highlightExact"
@@ -69,5 +69,6 @@ export interface PdfDocumentHandle {
69
69
  highlightPage?: number | null
70
70
  highlightExact?: boolean
71
71
  }) => Promise<void>
72
+ getPageDimensions?: (opts: { pageNum: number, scale: number }) => Promise<{ width: number, height: number } | null>
72
73
  destroy: () => void
73
74
  }
@@ -2,6 +2,7 @@
2
2
  import { TooltipContent, TooltipPortal, useForwardPropsEmits } from 'reka-ui'
3
3
  import type { TooltipContentEmits, TooltipContentProps } from 'reka-ui'
4
4
  import { reactiveOmit } from '@vueuse/core'
5
+ import { inject } from 'vue'
5
6
  import type { HTMLAttributes } from 'vue'
6
7
 
7
8
  defineOptions({
@@ -19,10 +20,12 @@ const emits = defineEmits<TooltipContentEmits & {
19
20
  const delegatedProps = reactiveOmit(props, 'class')
20
21
 
21
22
  const forwarded = useForwardPropsEmits(delegatedProps, emits)
23
+
24
+ const disablePortal = inject<boolean>('tela-disable-tooltip-portal', false)
22
25
  </script>
23
26
 
24
27
  <template>
25
- <TooltipPortal>
28
+ <TooltipPortal :disabled="disablePortal">
26
29
  <TooltipContent
27
30
  v-bind="{ ...forwarded, ...$attrs }"
28
31
  :style="{ pointerEvents: 'auto' }"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.25.1",
3
+ "version": "1.25.3",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",