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