@eternalheart/vue-file-preview 1.4.0 → 1.5.1

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.
Files changed (141) hide show
  1. package/README.md +490 -48
  2. package/README.zh-CN.md +477 -35
  3. package/lib/FilePreviewContent.vue.d.ts +4 -1
  4. package/lib/FilePreviewContent.vue.d.ts.map +1 -1
  5. package/lib/FilePreviewEmbed.vue.d.ts +4 -1
  6. package/lib/FilePreviewEmbed.vue.d.ts.map +1 -1
  7. package/lib/FilePreviewModal.vue.d.ts +3 -0
  8. package/lib/FilePreviewModal.vue.d.ts.map +1 -1
  9. package/lib/chunks/base.types-DaftHyg7.mjs +26 -0
  10. package/lib/chunks/base.types-DaftHyg7.mjs.map +1 -0
  11. package/lib/chunks/index-B3LuN6sW.mjs +88 -0
  12. package/lib/chunks/index-B3LuN6sW.mjs.map +1 -0
  13. package/lib/chunks/{index-B-LW-GJO.mjs → index-B4e9KG0m.mjs} +37 -35
  14. package/lib/chunks/index-B4e9KG0m.mjs.map +1 -0
  15. package/lib/chunks/{index-nFIiu1lA.mjs → index-BR7x_n30.mjs} +634 -632
  16. package/lib/chunks/{index-nFIiu1lA.mjs.map → index-BR7x_n30.mjs.map} +1 -1
  17. package/lib/chunks/{index-By6vGdnK.mjs → index-BpiLHisE.mjs} +39 -37
  18. package/lib/chunks/index-BpiLHisE.mjs.map +1 -0
  19. package/lib/chunks/{index-DGqDJMot.mjs → index-C61SbHQp.mjs} +28 -26
  20. package/lib/chunks/index-C61SbHQp.mjs.map +1 -0
  21. package/lib/chunks/index-CRdBttK7.mjs +116 -0
  22. package/lib/chunks/index-CRdBttK7.mjs.map +1 -0
  23. package/lib/chunks/index-CTZ97lMP.mjs +467 -0
  24. package/lib/chunks/index-CTZ97lMP.mjs.map +1 -0
  25. package/lib/chunks/index-CZhPBiFx.mjs +153 -0
  26. package/lib/chunks/index-CZhPBiFx.mjs.map +1 -0
  27. package/lib/chunks/index-CkZog6ZF.mjs +301 -0
  28. package/lib/chunks/index-CkZog6ZF.mjs.map +1 -0
  29. package/lib/chunks/index-Dp9yGewO.mjs +363 -0
  30. package/lib/chunks/index-Dp9yGewO.mjs.map +1 -0
  31. package/lib/chunks/{index-BcKI75RW.mjs → index-EWO4Zzkc.mjs} +102 -99
  32. package/lib/chunks/index-EWO4Zzkc.mjs.map +1 -0
  33. package/lib/chunks/index-P-QMUL-A.mjs +2325 -0
  34. package/lib/chunks/index-P-QMUL-A.mjs.map +1 -0
  35. package/lib/chunks/{index-BT9hXA6r.mjs → index-Q3NeAmey.mjs} +43 -41
  36. package/lib/chunks/index-Q3NeAmey.mjs.map +1 -0
  37. package/lib/chunks/index-QqHYhWLO.mjs +320 -0
  38. package/lib/chunks/index-QqHYhWLO.mjs.map +1 -0
  39. package/lib/chunks/index-Zs_EXPbO.mjs +111 -0
  40. package/lib/chunks/index-Zs_EXPbO.mjs.map +1 -0
  41. package/lib/chunks/{index-jcgLzyDR.mjs → index-c_911MDe.mjs} +135 -133
  42. package/lib/chunks/{index-jcgLzyDR.mjs.map → index-c_911MDe.mjs.map} +1 -1
  43. package/lib/chunks/{index-CoITLoYL.mjs → index-ce16D_gk.mjs} +20 -18
  44. package/lib/chunks/index-ce16D_gk.mjs.map +1 -0
  45. package/lib/chunks/index-d1cpacUP.mjs +388 -0
  46. package/lib/chunks/index-d1cpacUP.mjs.map +1 -0
  47. package/lib/chunks/{index-OOoaYpkl.mjs → index-gU_qBCO3.mjs} +1015 -1013
  48. package/lib/chunks/{index-OOoaYpkl.mjs.map → index-gU_qBCO3.mjs.map} +1 -1
  49. package/lib/chunks/useShikiHighlight-D6mUzKkq.mjs +41 -0
  50. package/lib/chunks/useShikiHighlight-D6mUzKkq.mjs.map +1 -0
  51. package/lib/composables/useShikiHighlight.d.ts +18 -0
  52. package/lib/composables/useShikiHighlight.d.ts.map +1 -0
  53. package/lib/index.cjs +38 -36
  54. package/lib/index.cjs.map +1 -1
  55. package/lib/index.css +1 -1
  56. package/lib/index.mjs +1 -1
  57. package/lib/renderers/Audio/index.vue.d.ts +5 -1
  58. package/lib/renderers/Audio/index.vue.d.ts.map +1 -1
  59. package/lib/renderers/Csv/index.vue.d.ts +5 -1
  60. package/lib/renderers/Csv/index.vue.d.ts.map +1 -1
  61. package/lib/renderers/Docx/index.vue.d.ts +5 -1
  62. package/lib/renderers/Docx/index.vue.d.ts.map +1 -1
  63. package/lib/renderers/Epub/index.vue.d.ts +4 -11
  64. package/lib/renderers/Epub/index.vue.d.ts.map +1 -1
  65. package/lib/renderers/Font/index.vue.d.ts +5 -1
  66. package/lib/renderers/Font/index.vue.d.ts.map +1 -1
  67. package/lib/renderers/Image/index.vue.d.ts +5 -12
  68. package/lib/renderers/Image/index.vue.d.ts.map +1 -1
  69. package/lib/renderers/Json/index.vue.d.ts +5 -1
  70. package/lib/renderers/Json/index.vue.d.ts.map +1 -1
  71. package/lib/renderers/Markdown/index.vue.d.ts +5 -2
  72. package/lib/renderers/Markdown/index.vue.d.ts.map +1 -1
  73. package/lib/renderers/Mobi/index.vue.d.ts +5 -12
  74. package/lib/renderers/Mobi/index.vue.d.ts.map +1 -1
  75. package/lib/renderers/Msg/index.vue.d.ts +5 -1
  76. package/lib/renderers/Msg/index.vue.d.ts.map +1 -1
  77. package/lib/renderers/Pdf/index.vue.d.ts +5 -14
  78. package/lib/renderers/Pdf/index.vue.d.ts.map +1 -1
  79. package/lib/renderers/Pptx/index.vue.d.ts +5 -1
  80. package/lib/renderers/Pptx/index.vue.d.ts.map +1 -1
  81. package/lib/renderers/Subtitle/index.vue.d.ts +5 -1
  82. package/lib/renderers/Subtitle/index.vue.d.ts.map +1 -1
  83. package/lib/renderers/Text/index.vue.d.ts +5 -6
  84. package/lib/renderers/Text/index.vue.d.ts.map +1 -1
  85. package/lib/renderers/Unsupported/index.vue.d.ts +5 -1
  86. package/lib/renderers/Unsupported/index.vue.d.ts.map +1 -1
  87. package/lib/renderers/Video/index.vue.d.ts +5 -1
  88. package/lib/renderers/Video/index.vue.d.ts.map +1 -1
  89. package/lib/renderers/Xlsx/index.vue.d.ts +5 -1
  90. package/lib/renderers/Xlsx/index.vue.d.ts.map +1 -1
  91. package/lib/renderers/Xml/index.vue.d.ts +5 -1
  92. package/lib/renderers/Xml/index.vue.d.ts.map +1 -1
  93. package/lib/renderers/Zip/index.vue.d.ts +10 -6
  94. package/lib/renderers/Zip/index.vue.d.ts.map +1 -1
  95. package/lib/renderers/base.types.d.ts +59 -0
  96. package/lib/renderers/base.types.d.ts.map +1 -0
  97. package/lib/renderers/registry.d.ts +36 -0
  98. package/lib/renderers/registry.d.ts.map +1 -0
  99. package/lib/renderers/toolbar.types.d.ts +1 -0
  100. package/lib/renderers/toolbar.types.d.ts.map +1 -1
  101. package/package.json +3 -3
  102. package/lib/chunks/index-8ETACZ6T.mjs +0 -267
  103. package/lib/chunks/index-8ETACZ6T.mjs.map +0 -1
  104. package/lib/chunks/index-B-LW-GJO.mjs.map +0 -1
  105. package/lib/chunks/index-BT9hXA6r.mjs.map +0 -1
  106. package/lib/chunks/index-BcKI75RW.mjs.map +0 -1
  107. package/lib/chunks/index-By6vGdnK.mjs.map +0 -1
  108. package/lib/chunks/index-C-Mu0fvH.mjs +0 -202
  109. package/lib/chunks/index-C-Mu0fvH.mjs.map +0 -1
  110. package/lib/chunks/index-CGSq7-Q-.mjs +0 -279
  111. package/lib/chunks/index-CGSq7-Q-.mjs.map +0 -1
  112. package/lib/chunks/index-CWpU8prZ.mjs +0 -74
  113. package/lib/chunks/index-CWpU8prZ.mjs.map +0 -1
  114. package/lib/chunks/index-CYa_XneH.mjs +0 -303
  115. package/lib/chunks/index-CYa_XneH.mjs.map +0 -1
  116. package/lib/chunks/index-CbJkkdEB.mjs +0 -114
  117. package/lib/chunks/index-CbJkkdEB.mjs.map +0 -1
  118. package/lib/chunks/index-CoITLoYL.mjs.map +0 -1
  119. package/lib/chunks/index-D9QQL7U6.mjs +0 -2506
  120. package/lib/chunks/index-D9QQL7U6.mjs.map +0 -1
  121. package/lib/chunks/index-DAdO2lkI.mjs +0 -388
  122. package/lib/chunks/index-DAdO2lkI.mjs.map +0 -1
  123. package/lib/chunks/index-DGqDJMot.mjs.map +0 -1
  124. package/lib/chunks/index-DTyG1Wzp.mjs +0 -81
  125. package/lib/chunks/index-DTyG1Wzp.mjs.map +0 -1
  126. package/lib/chunks/index-DglMeKJM.mjs +0 -130
  127. package/lib/chunks/index-DglMeKJM.mjs.map +0 -1
  128. package/lib/renderers/Epub/toolbar.d.ts +0 -16
  129. package/lib/renderers/Epub/toolbar.d.ts.map +0 -1
  130. package/lib/renderers/Image/toolbar.d.ts +0 -15
  131. package/lib/renderers/Image/toolbar.d.ts.map +0 -1
  132. package/lib/renderers/Markdown/toolbar.d.ts +0 -9
  133. package/lib/renderers/Markdown/toolbar.d.ts.map +0 -1
  134. package/lib/renderers/Mobi/toolbar.d.ts +0 -16
  135. package/lib/renderers/Mobi/toolbar.d.ts.map +0 -1
  136. package/lib/renderers/Pdf/toolbar.d.ts +0 -16
  137. package/lib/renderers/Pdf/toolbar.d.ts.map +0 -1
  138. package/lib/renderers/Text/toolbar.d.ts +0 -12
  139. package/lib/renderers/Text/toolbar.d.ts.map +0 -1
  140. package/lib/renderers/Zip/toolbar.d.ts +0 -13
  141. package/lib/renderers/Zip/toolbar.d.ts.map +0 -1
package/README.md CHANGED
@@ -4,23 +4,22 @@ English | [简体中文](./README.zh-CN.md)
4
4
 
5
5
  A modern, feature-rich file preview component for Vue 3 with support for images, videos, audio, PDFs, Office documents (Word, Excel, PowerPoint), Markdown, and code files.
6
6
 
7
- ## ✨ Features
8
-
9
- - 🎨 **Modern UI** - Apple-inspired minimalist design with glassmorphism effects
10
- - 📁 **Multi-format Support** - Supports 20+ file formats
11
- - 🪟 **Two Display Modes** - Full-screen modal **or** inline embedded preview
12
- - 🖼️ **Powerful Image Viewer** - Zoom, rotate, drag, mouse wheel zoom
13
- - 🎬 **Custom Video Player** - Built on Video.js, supports multiple video formats
14
- - 🎵 **Custom Audio Player** - Beautiful audio control interface
15
- - 📄 **PDF Viewer** - Pagination support
16
- - 📊 **Office Documents Support** - Word, Excel, PowerPoint file preview
17
- - 📝 **Markdown Rendering** - GitHub Flavored Markdown support
18
- - 💻 **Code Highlighting** - Supports 40+ programming languages
19
- - 📱 **Responsive Design** - Adapts to all screen sizes
20
- - ⌨️ **Keyboard Navigation** - Arrow keys and ESC support
21
- - 🎯 **Drag & Drop** - File upload via drag and drop
22
-
23
- ## 📦 Installation
7
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/2728.svg" width="20" height="20" alt="" /> Features
8
+
9
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f3a8.svg" width="16" height="16" alt="🎨" style="vertical-align: middle;" /> **Modern UI** - Clean and modern interface with smooth animations
10
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4c1.svg" width="16" height="16" alt="📁" style="vertical-align: middle;" /> **Multi-format Support** - Supports 20+ file formats
11
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1fa9f.svg" width="16" height="16" alt="🪟" style="vertical-align: middle;" /> **Two Display Modes** - Full-screen modal **or** inline embedded preview
12
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f5bc.svg" width="16" height="16" alt="🖼️" style="vertical-align: middle;" /> **Powerful Image Viewer** - Zoom, rotate, drag, mouse wheel zoom
13
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f3ac.svg" width="16" height="16" alt="🎬" style="vertical-align: middle;" /> **Custom Video Player** - Built on Video.js, supports multiple video formats
14
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f3b5.svg" width="16" height="16" alt="🎵" style="vertical-align: middle;" /> **Custom Audio Player** - Beautiful audio control interface
15
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4c4.svg" width="16" height="16" alt="📄" style="vertical-align: middle;" /> **PDF Viewer** - Pagination support
16
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4ca.svg" width="16" height="16" alt="📊" style="vertical-align: middle;" /> **Office Documents Support** - Word, Excel, PowerPoint file preview
17
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4dd.svg" width="16" height="16" alt="📝" style="vertical-align: middle;" /> **Markdown Rendering** - GitHub Flavored Markdown support
18
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4bb.svg" width="16" height="16" alt="💻" style="vertical-align: middle;" /> **Code Highlighting** - Supports 40+ programming languages
19
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4f1.svg" width="16" height="16" alt="📱" style="vertical-align: middle;" /> **Responsive Design** - Adapts to all screen sizes
20
+ - <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/2328.svg" width="16" height="16" alt="⌨️" style="vertical-align: middle;" /> **Keyboard Navigation** - Arrow keys and ESC support
21
+
22
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4e6.svg" width="20" height="20" alt="📦" /> Installation
24
23
 
25
24
  ```bash
26
25
  # Using npm
@@ -122,7 +121,7 @@ export default defineConfig({
122
121
 
123
122
  > Note: `@jsquash/avif` is only used as a fallback when the browser does not natively support AVIF (Chrome 85+, Firefox 93+, Safari 16+ all support it natively). If your target browsers cover the native list, you can also remove `@jsquash/avif` from your dependencies entirely.
124
123
 
125
- ## 🚀 Quick Start
124
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f680.svg" width="20" height="20" alt="🚀" /> Quick Start
126
125
 
127
126
  ### Basic Usage
128
127
 
@@ -169,14 +168,18 @@ import { ref } from 'vue';
169
168
  import { FilePreviewModal, type PreviewFileInput } from '@eternalheart/vue-file-preview';
170
169
  import '@eternalheart/vue-file-preview/style.css';
171
170
 
171
+ // Assume file1 comes from a File API source: <input type="file">,
172
+ // drag & drop, clipboard paste, or fetch().then(r => r.blob())
173
+ const file1 = new File(['content'], 'example.txt', { type: 'text/plain' });
174
+
172
175
  const files: PreviewFileInput[] = [
173
- // 1. Native File object
176
+ // 1. Native File object (auto revoked when unmounted)
174
177
  file1,
175
178
 
176
- // 2. HTTP URL string
179
+ // 2. HTTP URL string (loaded on demand)
177
180
  'https://example.com/image.jpg',
178
181
 
179
- // 3. File object with metadata
182
+ // 3. File object with metadata (recommended for remote resources)
180
183
  {
181
184
  name: 'document.pdf',
182
185
  type: 'application/pdf',
@@ -185,6 +188,9 @@ const files: PreviewFileInput[] = [
185
188
  },
186
189
  ];
187
190
 
191
+ // Memory note: If you generate URLs via URL.createObjectURL(),
192
+ // call URL.revokeObjectURL() when files are removed to avoid memory leaks.
193
+
188
194
  const isOpen = ref(true);
189
195
  </script>
190
196
 
@@ -239,7 +245,7 @@ Differences from `FilePreviewModal`:
239
245
  <FilePreviewEmbed :files="files" :width="800" :height="500" />
240
246
  ```
241
247
 
242
- ## 📖 Supported File Formats
248
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4d6.svg" width="20" height="20" alt="📖" /> Supported File Formats
243
249
 
244
250
  ### Images
245
251
  - **Formats**: JPG, PNG, GIF, WebP, SVG, BMP, ICO
@@ -288,7 +294,56 @@ Differences from `FilePreviewModal`:
288
294
  ### E-books
289
295
  - **EPUB**: Chapter navigation, pagination
290
296
 
291
- ## 🎮 API Reference
297
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/26a0.svg" width="20" height="20" alt="⚠️" /> Limitations &amp; Performance
298
+
299
+ ### Support Levels
300
+
301
+ **✅ Full Support (Production Ready)**
302
+ - Images (JPG, PNG, GIF, WebP, SVG, BMP, ICO)
303
+ - Videos (MP4, WebM, OGG)
304
+ - Audio (MP3, WAV, OGG)
305
+ - PDF
306
+ - Markdown
307
+ - Code files (40+ languages via Shiki, lazy-loaded)
308
+ - JSON, CSV, XML
309
+
310
+ **⚠️ Partial Support (Preview Only)**
311
+ - **Office (DOCX, XLSX, PPTX)**: Basic layout and text rendering. Complex formatting (charts, macros, embedded objects) may not render accurately.
312
+ - **ZIP**: Directory tree browsing + inline preview for text/code/images. Large archives (&gt;100MB) may cause performance issues.
313
+ - **Fonts (TTF, OTF, WOFF)**: Metadata + character preview. Full font feature testing not supported.
314
+
315
+ **🧪 Experimental**
316
+ - **MSG (Outlook Email)**: Headers and plain text body. Complex HTML body may not render correctly.
317
+ - **EPUB**: Basic chapter navigation. CSS styling may differ from native readers. DRM-protected files not supported.
318
+ - **Subtitle formats (SRT, ASS, TTML, LRC)**: Text display only. No video sync or advanced styling.
319
+
320
+ ### Performance Boundaries
321
+
322
+ | File Size | Status | Notes |
323
+ |-----------|--------|-------|
324
+ | &lt; 50MB | ✅ Recommended | Smooth preview experience |
325
+ | 50-100MB | ⚠️ May lag | UI may become unresponsive during load |
326
+ | &gt; 100MB | ❌ Not recommended | Browser memory limits may be exceeded |
327
+
328
+ **Special Cases:**
329
+ - **ZIP archives**: Performance depends on file count, not just size
330
+ - **Office documents**: Complex files (&gt;200 pages, heavy images) may timeout
331
+ - **Code highlighting**: Files &gt;5MB may take 3-5s to highlight
332
+
333
+ ### Browser Compatibility
334
+
335
+ **Minimum Requirements:**
336
+ - Chrome 90+ / Edge 90+
337
+ - Firefox 88+
338
+ - Safari 14+
339
+
340
+ **Known Limitations:**
341
+ - **Safari iOS**: Video autoplay requires user interaction
342
+ - **Firefox**: AVIF support requires Firefox 93+ (fallback decoder included)
343
+ - **Office formats**: Rendering quality varies across browsers
344
+ - **EPUB**: Some CSS features unsupported in older browsers
345
+
346
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f3ae.svg" width="20" height="20" alt="🎮" /> API Reference
292
347
 
293
348
  ### FilePreviewModal Props
294
349
 
@@ -302,6 +357,7 @@ Differences from `FilePreviewModal`:
302
357
  | `messages` | `Partial<Record<Locale, Partial<Messages>>>` | ❌ | Custom translation overrides |
303
358
  | `headless` | `boolean` | ❌ | Headless mode — hides toolbar and navigation arrows |
304
359
  | `theme` | `Theme` | ❌ | Theme mode: `'auto' \| 'dark' \| 'light'` (default `'dark'`) |
360
+ | `showDownload` | `boolean` | ❌ | Whether to show the download button (default `true`) |
305
361
 
306
362
  ### FilePreviewModal Events
307
363
 
@@ -323,6 +379,7 @@ Differences from `FilePreviewModal`:
323
379
  | `messages` | `Partial<Record<Locale, Partial<Messages>>>` | ❌ | - | Custom translation overrides |
324
380
  | `headless` | `boolean` | ❌ | `false` | Headless mode — hides toolbar and navigation arrows |
325
381
  | `theme` | `Theme` | ❌ | `'dark'` | Theme mode: `'auto' \| 'dark' \| 'light'` |
382
+ | `showDownload` | `boolean` | ❌ | `true` | Whether to show the download button |
326
383
 
327
384
  ### FilePreviewEmbed Events
328
385
 
@@ -365,39 +422,424 @@ interface PreviewFileLink {
365
422
  const url: string = 'https://example.com/file.pdf';
366
423
  ```
367
424
 
368
- ## ⌨️ Keyboard Shortcuts
425
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f9e9.svg" width="20" height="20" alt="🧩" /> Custom Renderers
369
426
 
370
- - `ESC` - Close preview
371
- - `←` - Previous file
372
- - `→` - Next file
373
- - `Mouse Wheel` - Zoom image (image preview only)
427
+ The library supports custom renderers for handling file types not built-in. Custom renderers can optionally provide toolbar configurations and integrate with the library's architecture.
374
428
 
375
- ## 📚 Documentation
429
+ ### Event-Driven Toolbar Updates
376
430
 
377
- - [Full Documentation](https://wh131462.github.io/file-preview/docs/)
378
- - [Vue Demo](https://wh131462.github.io/file-preview/vue/)
379
- - [React Demo](https://wh131462.github.io/file-preview/)
431
+ Custom renderers can implement real-time toolbar updates using Vue 3's reactivity system:
380
432
 
381
- ## 📦 Package Information
433
+ **Benefits:**
434
+ - **Real-time updates**: Toolbar reflects state changes immediately via Vue reactivity
435
+ - **Better performance**: No polling overhead, leverages Vue's efficient change detection
436
+ - **Type-safe**: Full TypeScript support with proper interfaces
382
437
 
383
- ### Peer Dependencies
438
+ **Implementation:**
384
439
 
385
- - `vue`: ^3.4.0
440
+ ```vue
441
+ <script setup lang="ts">
442
+ import { ref, computed, watch } from 'vue';
443
+ import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
444
+ import type { ToolbarGroup } from '@eternalheart/vue-file-preview';
386
445
 
387
- ### Exports
446
+ interface Props {
447
+ url: string;
448
+ }
388
449
 
389
- ```json
390
- {
391
- ".": {
392
- "types": "./lib/index.d.ts",
393
- "import": "./lib/index.mjs",
394
- "require": "./lib/index.cjs"
395
- },
396
- "./style.css": "./lib/index.css"
450
+ const props = defineProps<Props>();
451
+ const emit = defineEmits<{
452
+ pageChange: [current: number, total: number];
453
+ }>();
454
+
455
+ const currentPage = ref(1);
456
+ const totalPages = ref(10);
457
+
458
+ // Emit page changes
459
+ watch([currentPage, totalPages], () => {
460
+ emit('pageChange', currentPage.value, totalPages.value);
461
+ });
462
+
463
+ const getToolbarGroups = (): ToolbarGroup[] => [
464
+ {
465
+ items: [
466
+ {
467
+ type: 'button',
468
+ icon: ChevronLeft,
469
+ tooltip: 'Previous Page',
470
+ action: () => currentPage.value = Math.max(1, currentPage.value - 1),
471
+ disabled: currentPage.value <= 1
472
+ },
473
+ {
474
+ type: 'text',
475
+ content: `${currentPage.value} / ${totalPages.value}`,
476
+ minWidth: '4rem'
477
+ },
478
+ {
479
+ type: 'button',
480
+ icon: ChevronRight,
481
+ tooltip: 'Next Page',
482
+ action: () => currentPage.value = Math.min(totalPages.value, currentPage.value + 1),
483
+ disabled: currentPage.value >= totalPages.value
484
+ }
485
+ ]
486
+ }
487
+ ];
488
+
489
+ // Expose for parent component
490
+ defineExpose({
491
+ getToolbarGroups
492
+ });
493
+ </script>
494
+
495
+ <template>
496
+ <div>Your custom renderer UI</div>
497
+ </template>
498
+ ```
499
+
500
+ **Main component usage:**
501
+
502
+ ```vue
503
+ <script setup>
504
+ import { CustomRenderer } from './CustomRenderer.vue';
505
+
506
+ const files = [
507
+ { name: 'custom.xyz', type: 'application/custom', url: '/path/to/file' }
508
+ ];
509
+ </script>
510
+
511
+ <template>
512
+ <FilePreviewModal
513
+ :files="files"
514
+ :custom-renderers="[
515
+ {
516
+ test: (file) => file.type === 'application/custom',
517
+ component: CustomRenderer
518
+ }
519
+ ]"
520
+ />
521
+ </template>
522
+ ```
523
+
524
+ The main component automatically tracks reactive changes in `getToolbarGroups()` via Vue's reactivity system. No manual subscription needed.
525
+
526
+ ### Renderer Lazy Loading
527
+
528
+ All built-in renderers use code-splitting via `defineAsyncComponent` to minimize the main bundle size and improve initial load performance.
529
+
530
+ **Architecture:**
531
+
532
+ - **Registration**: Renderers register in `src/renderers/lazy.ts` using `defineAsyncComponent` wrappers
533
+ - **Loading**: Each renderer is a separate chunk, loaded on-demand when needed
534
+ - **Fallback**: `RendererLoading` component handles the loading state
535
+
536
+ **Bundle Size Impact:**
537
+
538
+ - Main entry point: gzip ≤ 60 KB (strictly enforced by CI)
539
+ - Each renderer: separate async chunk
540
+ - Total library: gzip ≤ 500 KB (all renderers combined)
541
+
542
+ **Implementation Example:**
543
+
544
+ ```ts
545
+ // src/renderers/lazy.ts
546
+ import { defineAsyncComponent } from 'vue';
547
+
548
+ const wrap = (loader: () => Promise<any>) =>
549
+ defineAsyncComponent({
550
+ loader,
551
+ loadingComponent: RendererLoading,
552
+ delay: 0
553
+ });
554
+
555
+ export const CustomRenderer = wrap(() => import('./Custom/index.vue'));
556
+ ```
557
+
558
+ ```vue
559
+ <!-- src/FilePreviewContent.vue -->
560
+ <script setup>
561
+ import { CustomRenderer } from './renderers/lazy'; // ✅ Lazy import
562
+ // NOT: import CustomRenderer from './renderers/Custom/index.vue'; // ❌ Direct import breaks code-splitting
563
+ </script>
564
+
565
+ <template>
566
+ <CustomRenderer
567
+ v-if="fileType === 'custom'"
568
+ ref="rendererRef"
569
+ :url="currentFile.url"
570
+ />
571
+ </template>
572
+ ```
573
+
574
+ **For Custom Renderers:**
575
+
576
+ If you want your custom renderer to benefit from code-splitting, use the same pattern:
577
+
578
+ ```vue
579
+ <script setup>
580
+ import { defineAsyncComponent } from 'vue';
581
+
582
+ const MyCustomRenderer = defineAsyncComponent(() => import('./MyCustomRenderer.vue'));
583
+
584
+ const files = [...];
585
+ </script>
586
+
587
+ <template>
588
+ <FilePreviewModal
589
+ :files="files"
590
+ :custom-renderers="[
591
+ {
592
+ test: (file) => file.type === 'application/custom',
593
+ component: MyCustomRenderer
594
+ }
595
+ ]"
596
+ />
597
+ </template>
598
+ ```
599
+
600
+ ### i18n Integration
601
+
602
+ Custom renderers can access the library's i18n system via the `useTranslator()` composable for consistent multilingual support.
603
+
604
+ **Architecture:**
605
+
606
+ - **Dictionary Source**: `file-preview-core/src/i18n/messages/` (zh-CN.ts, en-US.ts)
607
+ - **No Hardcoding**: All user-visible text must use translation keys
608
+ - **Automatic Locale Switching**: Follows the `locale` prop passed to `FilePreviewModal`
609
+
610
+ **Usage in Custom Renderers:**
611
+
612
+ ```vue
613
+ <script setup lang="ts">
614
+ import { ref } from 'vue';
615
+ import { useTranslator } from '@eternalheart/vue-file-preview';
616
+
617
+ interface Props {
618
+ url: string;
619
+ }
620
+
621
+ const props = defineProps<Props>();
622
+ const { t } = useTranslator();
623
+ const error = ref<string | null>(null);
624
+ </script>
625
+
626
+ <template>
627
+ <div v-if="error" class="vfp-text-fg-primary">
628
+ {{ t('custom.load_failed') }}: {{ error }}
629
+ </div>
630
+ <div v-else>
631
+ <button>{{ t('common.download') }}</button>
632
+ <span>{{ t('custom.loading') }}</span>
633
+ </div>
634
+ </template>
635
+ ```
636
+
637
+ **Note on Usage:**
638
+
639
+ - In `<template>`: Use `t('key')` directly (automatically unwrapped)
640
+ - In `<script>`: Use `t.value('key')` for imperative calls
641
+
642
+ **Adding Custom Translation Keys:**
643
+
644
+ For custom renderers, extend translations via the `messages` prop (do NOT modify source files in `node_modules`):
645
+
646
+ ```vue
647
+ <template>
648
+ <FilePreviewModal
649
+ :files="files"
650
+ locale="en-US"
651
+ :messages="{
652
+ 'en-US': {
653
+ 'custom.load_failed': 'Failed to load custom file',
654
+ 'custom.file_size': 'File size: {size} KB'
655
+ },
656
+ 'zh-CN': {
657
+ 'custom.load_failed': '自定义文件加载失败',
658
+ 'custom.file_size': '文件大小: {size} KB'
659
+ }
660
+ }"
661
+ :custom-renderers="[...]"
662
+ />
663
+ </template>
664
+ ```
665
+
666
+ **Guidelines:**
667
+ - Use `<scope>.<snake_name>` format (e.g., `custom.load_failed`, `custom.parse_error`)
668
+ - Provide translations for all enabled locales (`zh-CN` and `en-US`)
669
+ - Common keys already available: `common.loading`, `common.download`, `common.close`, `toolbar.*`
670
+
671
+ **Parameterized Translations:**
672
+
673
+ ```vue
674
+ <template>
675
+ <!-- Dictionary: 'custom.file_size': 'File size: {size} KB' -->
676
+ <span>{{ t('custom.file_size', { size: 1024 }) }}</span>
677
+ <!-- → "File size: 1024 KB" -->
678
+ </template>
679
+ ```
680
+
681
+ **Toolbar Integration:**
682
+
683
+ Toolbar items should also use translated strings:
684
+
685
+ ```ts
686
+ const getToolbarGroups = (): ToolbarGroup[] => [
687
+ {
688
+ items: [
689
+ {
690
+ type: 'button',
691
+ icon: Download,
692
+ tooltip: t.value('common.download'), // ✅ Translated (use .value in script)
693
+ action: handleDownload
694
+ }
695
+ ]
696
+ }
697
+ ];
698
+ ```
699
+
700
+ ### Theme Adaptation
701
+
702
+ Custom renderers must use semantic color tokens to support the library's `'auto' | 'dark' | 'light'` theme system.
703
+
704
+ **Semantic Token System:**
705
+
706
+ All colors are defined as CSS variables (`--fp-*`) and exposed via Tailwind classes with the `vfp-` prefix:
707
+
708
+ | Usage | Class | Description |
709
+ |-------|-------|-------------|
710
+ | **Text (fg)** | | |
711
+ | Primary text | `vfp-text-fg-primary` | Highest contrast |
712
+ | Body text | `vfp-text-fg-secondary` | Default text |
713
+ | Secondary text | `vfp-text-fg-tertiary` | Captions, counters |
714
+ | Muted text | `vfp-text-fg-muted` | Placeholders |
715
+ | Disabled text | `vfp-text-fg-disabled` | Disabled buttons |
716
+ | **Background (surface)** | | |
717
+ | Surface 1 | `vfp-bg-surface-1` | Cards, weakest |
718
+ | Surface 2 | `vfp-bg-surface-2` | Hover states |
719
+ | Surface 3 | `vfp-bg-surface-3` | Emphasis |
720
+ | Toolbar | `vfp-bg-surface-toolbar` | Top toolbar |
721
+ | **Borders** | | |
722
+ | Weak border | `vfp-border-line-weak` | Subtle lines |
723
+ | Standard border | `vfp-border-line` | Default borders |
724
+ | Strong border | `vfp-border-line-strong` | Emphasis |
725
+ | **Code** | | |
726
+ | Code background | `vfp-bg-code-bg` | Dark: #1e1e1e / Light: #f6f8fa |
727
+ | Code text | `vfp-text-code-fg` | Follows theme |
728
+ | **Accent** | | |
729
+ | Accent background | `vfp-bg-accent` | Primary buttons |
730
+ | Accent hover | `vfp-bg-accent-hover` | Hover state |
731
+
732
+ **✅ Correct Usage:**
733
+
734
+ ```vue
735
+ <template>
736
+ <div class="vfp-bg-surface-1 vfp-border vfp-border-line-weak vfp-rounded">
737
+ <h2 class="vfp-text-fg-primary vfp-text-lg">Title</h2>
738
+ <p class="vfp-text-fg-secondary">Body text</p>
739
+ <button class="vfp-bg-surface-2 hover:vfp-bg-surface-3 vfp-text-fg-primary">
740
+ Click me
741
+ </button>
742
+ <pre class="vfp-bg-code-bg vfp-text-code-fg">{{ code }}</pre>
743
+ </div>
744
+ </template>
745
+ ```
746
+
747
+ **For `<style scoped>` blocks**, use CSS variables:
748
+
749
+ ```vue
750
+ <style scoped>
751
+ .my-block {
752
+ color: var(--fp-fg-primary);
753
+ background: var(--fp-surface-2);
754
+ border: 1px solid var(--fp-line);
755
+ }
756
+
757
+ .my-code {
758
+ background: var(--fp-code-bg);
759
+ color: var(--fp-code-fg);
397
760
  }
761
+ </style>
398
762
  ```
399
763
 
400
- ## 🛠️ Development
764
+ **❌ Incorrect Usage (DO NOT USE):**
765
+
766
+ ```vue
767
+ <!-- ❌ Literal color classes — breaks theme switching -->
768
+ <div class="vfp-text-white/90 vfp-bg-white/10 vfp-border-white/15">
769
+ <div class="vfp-text-gray-700 vfp-bg-gray-100">
770
+
771
+ <!-- ❌ Inline literal colors -->
772
+ <div :style="{ color: '#ffffff', background: '#1f2937' }">
773
+
774
+ <!-- ❌ Hardcoded dark-only colors in scoped style -->
775
+ <style scoped>
776
+ .foo { color: rgba(255, 255, 255, 0.75); } /* Use var(--fp-fg-secondary) */
777
+ .foo { background: #1e1e1e; } /* Use var(--fp-code-bg) */
778
+ </style>
779
+ ```
780
+
781
+ **Theme-Aware Third-Party Libraries:**
782
+
783
+ For libraries with theme props (e.g., `shiki`), use `useResolvedTheme()`:
784
+
785
+ ```vue
786
+ <script setup lang="ts">
787
+ import { ref, watch } from 'vue';
788
+ import { codeToHtml } from 'shiki';
789
+ import { useResolvedTheme } from '@eternalheart/vue-file-preview';
790
+
791
+ const props = defineProps<{ code: string; lang: string }>();
792
+ const resolvedTheme = useResolvedTheme(); // Ref<'dark' | 'light'>
793
+ const highlighted = ref('');
794
+
795
+ const highlightCode = async () => {
796
+ highlighted.value = await codeToHtml(props.code, {
797
+ lang: props.lang,
798
+ theme: resolvedTheme.value === 'light' ? 'github-light' : 'dark-plus'
799
+ });
800
+ };
801
+
802
+ // Re-highlight when theme changes
803
+ watch(resolvedTheme, highlightCode, { immediate: true });
804
+ </script>
805
+
806
+ <template>
807
+ <div v-html="highlighted"></div>
808
+ </template>
809
+ ```
810
+
811
+ **Testing:**
812
+
813
+ Always test your custom renderer in both Light and Dark themes:
814
+
815
+ ```vue
816
+ <FilePreviewModal
817
+ :files="files"
818
+ theme="light" // Switch between 'light', 'dark', 'auto'
819
+ :custom-renderers="[...]"
820
+ />
821
+ ```
822
+
823
+ Verify:
824
+ - Text is readable in both themes (no white-on-white or black-on-black)
825
+ - Borders and dividers are visible
826
+ - Hover states have sufficient contrast
827
+ - Code blocks follow theme (not always dark)
828
+
829
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/2328.svg" width="20" height="20" alt="⌨️" /> Keyboard Shortcuts
830
+
831
+ - `ESC` - Close preview
832
+ - `←` - Previous file
833
+ - `→` - Next file
834
+ - `Mouse Wheel` - Zoom image (image preview only)
835
+
836
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4da.svg" width="20" height="20" alt="📚" /> Documentation
837
+
838
+ - [Full Documentation](https://wh131462.github.io/file-preview/docs/)
839
+ - [Vue Demo](https://wh131462.github.io/file-preview/vue/)
840
+ - [React Demo](https://wh131462.github.io/file-preview/)
841
+
842
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f6e0.svg" width="20" height="20" alt="🛠️" /> Development
401
843
 
402
844
  ```bash
403
845
  # Clone repository
@@ -413,15 +855,15 @@ pnpm dev:vue-example
413
855
  pnpm build:vue
414
856
  ```
415
857
 
416
- ## 📄 License
858
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f4c4.svg" width="20" height="20" alt="📄" /> License
417
859
 
418
860
  [MIT](./LICENSE) © [EternalHeart](https://github.com/wh131462)
419
861
 
420
- ## 🤝 Contributing
862
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f91d.svg" width="20" height="20" alt="🤝" /> Contributing
421
863
 
422
864
  Issues and Pull Requests are welcome!
423
865
 
424
- ## 🔗 Links
866
+ ## <img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/1f517.svg" width="20" height="20" alt="🔗" /> Links
425
867
 
426
868
  - [GitHub](https://github.com/wh131462/file-preview)
427
869
  - [npm](https://www.npmjs.com/package/@eternalheart/vue-file-preview)