feat(renderer): PDF viewer with page nav & zoom controls (M10.2)
- PdfViewer.vue: lazy-loads pdfjs-dist, renders to canvas - Page navigation (arrows), zoom controls (50%–200%) - Keyboard navigation (arrow keys) - Integrated into content panel via openPdfViewer() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
edb4bbadcd
commit
7a3d874f5e
@@ -22,6 +22,7 @@
|
||||
"@aiui/core": "workspace:*",
|
||||
"@tanstack/vue-virtual": "^3.13.19",
|
||||
"markdown-it": "^14.1.1",
|
||||
"pdfjs-dist": "^5.5.207",
|
||||
"pinia": "^3.0.4",
|
||||
"plyr": "^3.8.4",
|
||||
"vue": "^3.5.29",
|
||||
|
||||
@@ -93,6 +93,12 @@
|
||||
:title="longFormArticle.title"
|
||||
@back="closeLongFormArticle"
|
||||
/>
|
||||
<PdfViewer
|
||||
v-else-if="pdfUrl"
|
||||
:url="pdfUrl.url"
|
||||
:title="pdfUrl.title"
|
||||
@back="closePdfViewer"
|
||||
/>
|
||||
|
||||
<!-- Grid views by active tab -->
|
||||
<component
|
||||
@@ -177,6 +183,7 @@ import PodcastDetail from './PodcastDetail.vue'
|
||||
import NewsGrid from './NewsGrid.vue'
|
||||
import ArticleDetail from './ArticleDetail.vue'
|
||||
import ArticleReader from '@/components/renderers/ArticleReader.vue'
|
||||
import PdfViewer from '@/components/renderers/PdfViewer.vue'
|
||||
import MagazineGrid from './MagazineGrid.vue'
|
||||
import ProjectGrid from './ProjectGrid.vue'
|
||||
import NostrGrid from './NostrGrid.vue'
|
||||
@@ -227,11 +234,13 @@ const {
|
||||
closeArticleDetail,
|
||||
longFormArticle,
|
||||
closeLongFormArticle,
|
||||
pdfUrl,
|
||||
closePdfViewer,
|
||||
closePanel,
|
||||
} = useContentPanel()
|
||||
|
||||
const hasDetailOpen = computed(() =>
|
||||
!!(selectedFilm.value || selectedBook.value || selectedTVSeries.value || selectedSong.value || selectedPodcast.value || selectedArticle.value || selectedDesignSystemItem.value || longFormArticle.value)
|
||||
!!(selectedFilm.value || selectedBook.value || selectedTVSeries.value || selectedSong.value || selectedPodcast.value || selectedArticle.value || selectedDesignSystemItem.value || longFormArticle.value || pdfUrl.value)
|
||||
)
|
||||
|
||||
const windowWidth = ref(window.innerWidth)
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="pdf-viewer h-full flex flex-col">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center gap-2 px-4 py-2 bg-black/60 backdrop-blur-md border-b border-white/5 shrink-0">
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-white/60 hover:text-white/80 hover:bg-white/10 transition-colors"
|
||||
title="Back"
|
||||
@click="$emit('back')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<span class="text-xs text-white/40 truncate flex-1">{{ title || 'PDF' }}</span>
|
||||
|
||||
<!-- Page nav -->
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-white/60 hover:text-white/80 hover:bg-white/10 transition-colors disabled:opacity-30"
|
||||
:disabled="currentPage <= 1"
|
||||
title="Previous page"
|
||||
@click="goToPage(currentPage - 1)"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-xs text-white/50 tabular-nums min-w-[60px] text-center">{{ currentPage }} / {{ totalPages }}</span>
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-white/60 hover:text-white/80 hover:bg-white/10 transition-colors disabled:opacity-30"
|
||||
:disabled="currentPage >= totalPages"
|
||||
title="Next page"
|
||||
@click="goToPage(currentPage + 1)"
|
||||
>
|
||||
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Zoom -->
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-white/60 hover:text-white/80 hover:bg-white/10 transition-colors disabled:opacity-30"
|
||||
:disabled="scale <= 0.5"
|
||||
title="Zoom out"
|
||||
@click="scale = Math.max(0.5, scale - 0.25)"
|
||||
>
|
||||
<span class="text-xs font-bold">−</span>
|
||||
</button>
|
||||
<span class="text-[10px] text-white/40 tabular-nums w-10 text-center">{{ Math.round(scale * 100) }}%</span>
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg text-white/60 hover:text-white/80 hover:bg-white/10 transition-colors disabled:opacity-30"
|
||||
:disabled="scale >= 2"
|
||||
title="Zoom in"
|
||||
@click="scale = Math.min(2, scale + 0.25)"
|
||||
>
|
||||
<span class="text-xs font-bold">+</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PDF canvas container -->
|
||||
<div ref="containerRef" class="flex-1 overflow-auto bg-black/20 p-4 flex justify-center">
|
||||
<div v-if="loading" class="flex items-center justify-center h-full">
|
||||
<div class="text-sm text-white/40">Loading PDF...</div>
|
||||
</div>
|
||||
<div v-else-if="error" class="flex items-center justify-center h-full">
|
||||
<div class="text-sm text-red-400/70">{{ error }}</div>
|
||||
</div>
|
||||
<canvas
|
||||
v-show="!loading && !error"
|
||||
ref="canvasRef"
|
||||
class="shadow-lg rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount, shallowRef } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
url: string
|
||||
title?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{ back: [] }>()
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const currentPage = ref(1)
|
||||
const totalPages = ref(0)
|
||||
const scale = ref(1)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
// Lazy-load pdfjs-dist
|
||||
type PDFDocumentProxy = import('pdfjs-dist').PDFDocumentProxy
|
||||
const pdfDoc = shallowRef<PDFDocumentProxy | null>(null)
|
||||
|
||||
async function loadPdf() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const pdfjsLib = await import('pdfjs-dist')
|
||||
// Set worker source
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString()
|
||||
|
||||
const doc = await pdfjsLib.getDocument(props.url).promise
|
||||
pdfDoc.value = doc
|
||||
totalPages.value = doc.numPages
|
||||
currentPage.value = 1
|
||||
await renderPage()
|
||||
} catch (e) {
|
||||
error.value = `Failed to load PDF: ${e instanceof Error ? e.message : 'Unknown error'}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPage() {
|
||||
const doc = pdfDoc.value
|
||||
const canvas = canvasRef.value
|
||||
if (!doc || !canvas) return
|
||||
|
||||
const page = await doc.getPage(currentPage.value)
|
||||
const viewport = page.getViewport({ scale: scale.value })
|
||||
|
||||
canvas.height = viewport.height
|
||||
canvas.width = viewport.width
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
await page.render({
|
||||
canvasContext: ctx,
|
||||
canvas: canvas,
|
||||
viewport,
|
||||
}).promise
|
||||
}
|
||||
|
||||
function goToPage(page: number) {
|
||||
if (page < 1 || page > totalPages.value) return
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
watch(currentPage, () => renderPage())
|
||||
watch(scale, () => renderPage())
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
goToPage(currentPage.value - 1)
|
||||
e.preventDefault()
|
||||
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
goToPage(currentPage.value + 1)
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPdf()
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
pdfDoc.value?.destroy()
|
||||
})
|
||||
</script>
|
||||
@@ -51,6 +51,7 @@ const activeTab = ref<ContentTab>('film')
|
||||
const availableTabs = ref<ContentTab[]>([])
|
||||
const selectedDesignSystemItem = ref<DesignSystemItem | null>(null)
|
||||
const longFormArticle = ref<{ content: string; title?: string } | null>(null)
|
||||
const pdfUrl = ref<{ url: string; title?: string } | null>(null)
|
||||
|
||||
export interface DesignSystemItem {
|
||||
id: string
|
||||
@@ -273,6 +274,7 @@ export function useContentPanel() {
|
||||
selectedMagazineSection.value = null
|
||||
selectedDesignSystemItem.value = null
|
||||
longFormArticle.value = null
|
||||
pdfUrl.value = null
|
||||
}
|
||||
|
||||
function openFilmDetail(film: Film) { clearAllSelections(); selectedFilm.value = film }
|
||||
@@ -296,6 +298,9 @@ export function useContentPanel() {
|
||||
function openLongFormArticle(content: string, title?: string) { clearAllSelections(); longFormArticle.value = { content, title }; panelOpen.value = true }
|
||||
function closeLongFormArticle() { longFormArticle.value = null }
|
||||
|
||||
function openPdfViewer(url: string, title?: string) { clearAllSelections(); pdfUrl.value = { url, title }; panelOpen.value = true }
|
||||
function closePdfViewer() { pdfUrl.value = null }
|
||||
|
||||
function openMagazineSectionDetail(section: MagazineSection, index: number) {
|
||||
clearAllSelections()
|
||||
selectedMagazineSection.value = section
|
||||
@@ -443,6 +448,9 @@ export function useContentPanel() {
|
||||
longFormArticle,
|
||||
openLongFormArticle,
|
||||
closeLongFormArticle,
|
||||
pdfUrl,
|
||||
openPdfViewer,
|
||||
closePdfViewer,
|
||||
enterDesignSystemMode,
|
||||
closePanel,
|
||||
showAllFilms,
|
||||
|
||||
Generated
+145
@@ -7,6 +7,10 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
pdfjs-dist:
|
||||
specifier: ^5.5.207
|
||||
version: 5.5.207
|
||||
devDependencies:
|
||||
turbo:
|
||||
specifier: ^2.8.12
|
||||
@@ -26,6 +30,9 @@ importers:
|
||||
markdown-it:
|
||||
specifier: ^14.1.1
|
||||
version: 14.1.1
|
||||
pdfjs-dist:
|
||||
specifier: ^5.5.207
|
||||
version: 5.5.207
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.8.3)(vue@3.5.29(typescript@5.8.3))
|
||||
@@ -867,6 +874,81 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||
resolution: {integrity: sha512-SqTh0wsYbetckMXEvHqmR7HKRJujVf1sYv1xdlhkifg6TlCSysz1opa49LlS3+xWuazcQcfRfmhA07HxxxGsAA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||
resolution: {integrity: sha512-F7jT0Syu+B9DGBUBcMk3qCRIxAWiDXmvEjamwbYfbZl7asI1pmXZUnCOoIu49Wt0RNooToYfRDxU9omD6t5Xuw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||
resolution: {integrity: sha512-54eb2Ho15RDjYGXO/harjRznBrAvu+j5nQ85Z4Qd6Qg3slR8/Ja+Yvvy9G4yo7rdX6NR9GPkZeSTf2UcKXwaXw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||
resolution: {integrity: sha512-hYaLCSLx5bmbnclzQc3ado3PgZ66blJWzjXp0wJmdwpr/kH+Mwhj6vuytJIomgksyJoCdIqIa4N6aiqBGJtJ5Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||
resolution: {integrity: sha512-J7VipONahKsmScPZsipHVQBqpbZx4favaD8/enWzzlGcjiwycOoymL7f4tNeqdjK0su19bDOUt6mjp9gsPWYlw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||
resolution: {integrity: sha512-PXy0UT1J/8MPG8UAkWp6Fd51ZtIZINFzIjGH909JjQrtCuJf3X6nanHYdz1A+Wq9o4aoPAw1YEUpFS1lelsVlg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||
resolution: {integrity: sha512-2IzCkW2RHRdcgF9W5/plHvYFpc6uikyjMb5SxjqmNxfyDFz9/HB89yhi8YQo0SNqrGRI7yBVDec7Pt+uMyRWsg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||
resolution: {integrity: sha512-OV/ol/OtcUr4qDhQg8G7SdViZX8XyQeKpPsVv/j3+7U178FGoU4M+yIocdVo1ih/A8GQ63+LjF4jDoEjaVU8Pw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||
resolution: {integrity: sha512-Z5KzqBK/XzPz5+SFHKz7yKqClEQ8pOiEDdgk5SlphBLVNb8JFIJkxhtJKSvnJyHh2rjVgiFmvtJzMF0gNwwKyQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||
resolution: {integrity: sha512-aj0YbRpe8qVJ4OzMsK7NfNQePgcf9zkGFzNZ9mSuaxXzhpLHmlF2GivNdCdNOg8WzA/NxV6IU4c5XkXadUMLeA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||
resolution: {integrity: sha512-GA8leTTCfdjuHi8reICTIxU0081PhXvl3lzIniLUjeLACx9GubUiyzkwFb+oyeKLS5IAGZFLKnzAf4wm2epRlA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.95':
|
||||
resolution: {integrity: sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@playwright/test@1.58.2':
|
||||
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2250,6 +2332,9 @@ packages:
|
||||
engines: {node: '>= 4.4.x'}
|
||||
hasBin: true
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@@ -2311,6 +2396,10 @@ packages:
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
pdfjs-dist@5.5.207:
|
||||
resolution: {integrity: sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==}
|
||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||
|
||||
perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
|
||||
@@ -3852,6 +3941,54 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.95':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.95':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.95
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.95
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.95
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.95
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.95
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.95
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.95
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.95
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.95
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.95
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.95
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.58.2':
|
||||
dependencies:
|
||||
playwright: 1.58.2
|
||||
@@ -5280,6 +5417,9 @@ snapshots:
|
||||
iconv-lite: 0.6.3
|
||||
sax: 1.5.0
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
nth-check@2.1.1:
|
||||
@@ -5341,6 +5481,11 @@ snapshots:
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pdfjs-dist@5.5.207:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.95
|
||||
node-readable-to-web-readable-stream: 0.4.2
|
||||
|
||||
perfect-debounce@1.0.0: {}
|
||||
|
||||
perfect-debounce@2.1.0: {}
|
||||
|
||||
Reference in New Issue
Block a user